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
useSnapshotEditor.save now validates all simple/priced lines in-memory
before any DB write, then delegates to a new saveSnapshotAtomic helper
that wraps INSERT snapshot + INSERT lines in an explicit BEGIN/COMMIT
transaction (ROLLBACK on catch). Pattern matches categorizationService.
Migration v11 cleans existing orphan snapshots in profiles that hit the
old race; new orphans are no longer possible thanks to the transaction.
Resolves#176
SQLite raised "misuse of aggregate function MIN()" because MIN was used
in the WHERE clause of a scalar subquery. Replace with ROW_NUMBER()
OVER (PARTITION BY account_id ORDER BY snapshot_date ASC) filtered on
rn = 1.
Adds vitest coverage and a regression test for /balance load.
Resolves#175
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
- prices.fetchPrice wraps invoke('fetch_price', ...) with local rate-limit (1/2s), in-flight dedup, exp backoff on 5xx (2/4/8s, max 3 retries), no retry on 4xx/429, hard 100/session cap
- 9 vitest tests with vi.useFakeTimers() (happy, 401/403/404, 429 no-retry, 5xx retries, dedup, pacing, session cap)
- Annexe B i18n mapping wired (PriceError → balance.priceFetching.errors.* keys)
- Session cap checked before rate-limit/dedup; failures do not consume budget (MEDIUM decision)
Closes#156
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
Source-level structural test on `TransactionTable.tsx` to lock down the
inlined transfer icon contract introduced in #142. Without RTL or jsdom
in the dev-deps, the test reads the component source and asserts:
- the icon is gated by `linkedTransfersByTxId?.has(row.id)`,
- optional-chaining short-circuits cleanly when the prop is omitted
(zero-impact on pre-#142 callers),
- the prop is declared OPTIONAL on the component interface,
- the `Link2` glyph comes from lucide-react,
- tooltip + aria-label go through `transactions.transferIcon.*` i18n
keys,
- the row's description cell layout (truncate span + title) stays
shared between linked and non-linked rows.
Catches the specific regression vectors: someone removing the gate,
renaming the prop, or breaking the optional-chaining pattern that
guarantees the page renders identically when no transfers are linked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end happy path through the full Bilan stack: account → priced
category → priced snapshot → linked transfer → return. Drives every
service against the existing in-memory FakeDb harness used by
category-migration tests so SQL shape (table names + parameters) can be
asserted alongside service outputs.
Currency lock: USD / EUR / GBP / JPY / AUD all rejected up-front by the
service with a typed `currency_unsupported` code, no DB hit. The CAD
default is verified to land in the INSERT params explicitly.
Priced-kind safety: a snapshot save with one out-of-tolerance line must
NOT clear pre-existing lines (the DELETE is gated behind the validation
loop). A drift just within ε is accepted unchanged.
computeAccountReturn wiring: malformed dates are rejected client-side
without invoking the Rust command; missing active profile yields a typed
`transfer_active_profile_unknown`; partial-period payloads are forwarded
unchanged (null fields preserved).
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>
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>
Issue #142 / Bilan #4 — TS bridge for the Modified Dietz command + plain
CRUD for transfer linking.
Types (`src/shared/types/index.ts`):
- `BalanceTransferDirection` ('in' | 'out')
- `BalanceAccountTransfer` (raw row) +
`BalanceAccountTransferWithTransaction` (joined view)
- `AccountReturn` (mirrors the Rust struct, ready to receive the invoke
payload as-is)
Service (`src/services/balance.service.ts`):
- `computeAccountReturn(accountId, periodStart, periodEnd)`: resolves the
active profile's `db_filename` from `loadProfiles()` and calls the
`compute_account_return` Tauri command.
- `linkTransfer(accountId, transactionId, direction, notes?)`: INSERT
with duplicate guard (typed `transfer_already_linked` error instead of
raw SQL UNIQUE failure).
- `unlinkTransfer(accountId, transactionId)`: DELETE with
`transfer_not_linked` guard for stale-UI calls.
- `listAccountTransfers(accountId, dateRange?)`: joined SELECT for
modal/list rendering.
- `listLinkedTransactionIds()`: returns a `Set<number>` for the
transaction icon (one query, in-memory `.has()` lookups thereafter).
- `listAllLinkedTransfersForTooltip()`: returns
`Map<transactionId, links[]>` for tooltip rendering.
- `suggestTransferDirection(amount)`: pure helper for the modal — maps
negative bank amounts to 'in', positive to 'out'.
- `isLinkedTransactionFkError(error)`: detects the canonical SQLite "FK
constraint failed" text so `transactionService.deleteTransaction` can
surface a clear i18n message.
- 5 new error codes added to `BalanceErrorCode`.
Tests (`balance.service.test.ts`): 22 new vitest cases bringing the file
to 85 passed. Mocks `@tauri-apps/api/core` `invoke` and
`./profileService` `loadProfiles`.
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>
Scoped useReducer hook backing BalancePage. Tracks:
- period (3M / 6M / 1A / 3A / all) — defaults to 1A
- chartMode (line / stacked) — defaults to line
- evolutionTotals + evolutionByCategory + accountsLatest +
accountsPeriodAnchor (parallel-fetched on mount and on every
period change via Promise.all)
- isLoading + error
Exposes computeBalanceDateRange(period, today) as a pure helper
so the date math is unit-testable without mocking time. Anchors
on `today` rather than the latest snapshot — keeps the chart's
right edge stable as the user enters new snapshots.
Refs: #141
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add four service helpers used by the upcoming `/balance` overview:
- getSnapshotTotalsByDate(range?) — SUM(value) GROUP BY snapshot_date
with an optional inclusive [from, to] range. LEFT JOIN preserves
empty snapshots as zero rows so the chart shows continuity.
- getSnapshotTotalsByCategoryAndDate(range?) — same aggregation broken
down by balance_categories.key, returned as one row per snapshot
date with a `byCategory` map. Powers the stacked-area variant.
- getAccountsLatestSnapshot() — one row per active account with the
value of its most-recent snapshot line (NULL when none exists).
Filters archived accounts via WHERE is_active = 1 AND archived_at
IS NULL, matches the listBalanceAccounts default.
- getAccountsPeriodAnchor(range) — earliest snapshot_date >= from
per account, with the value at that date — the anchor used to
compute the per-account Δ% column on the accounts table.
Tests cover empty DB, single/multi snapshot, archived exclusion via
SQL inspection, date-range params (from-only, both bounds, open).
Refs: #141
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- AccountsPage Categories tab now uses the new AccountForm 'category'
variant for creation (with kind selector).
- Delete button is disabled when the category has linked accounts;
the disabled tooltip surfaces the count.
- Clicking the delete button on a category with linked accounts now
shows a dismissable error banner listing up to the first 3 names
(with ellipsis when more) so the user knows exactly which accounts
to archive first. The service-level FK RESTRICT remains the
ultimate guard.
Refs #140
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>
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>
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>
Adds the snapshots + lines section of the balance service for Issue #146
(Bilan #1b). Simple-kind only — quantity / unit_price are forced to NULL
both at the SQL CHECK level (already in v9) and at the service level
(`upsertSnapshotLines` validates ahead of time). Priced-kind upsert lands
in #140.
New service exports:
- listSnapshots / getSnapshotByDate / getSnapshotById / getPreviousSnapshot
- createSnapshot (throws snapshot_date_taken when UNIQUE per date violated
so the UI can redirect to edit mode)
- updateSnapshot / deleteSnapshot (cascade lines via FK)
- listLinesBySnapshot / upsertSnapshotLines (rewrite-all strategy)
New BalanceErrorCode entries: snapshot_date_required, snapshot_date_taken,
snapshot_not_found, snapshot_value_invalid, snapshot_priced_unsupported.
New shared types: BalanceSnapshot, BalanceSnapshotLine.
22 new vitest cases cover: invalid-date guards, unique-per-date violation,
simple-kind null invariant on inserts, NaN/Infinity rejection,
clear+rewrite line semantics, getPreviousSnapshot strict-before ordering.
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>
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>
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.
Pure function that computes a v2 → v1 category migration plan from a
snapshot of the profile data. The 4-pass algorithm (keyword → supplier
propagation → default fallback → needs review) produces a MigrationPlan
with confidence badges (high/medium/low/none) per row, exposes split
targets for categories that ventilate across multiple v1 leaves (e.g.
Transport en commun → Autobus + Train), and preserves user-custom
categories in a dedicated bucket for later placement under
"Catégories personnalisées (migration)".
- Mapping tables encoded from
.spikes/archived/seed-standard/code/mapping-old-to-new.md
- No DB I/O: the caller hands us categories, keywords, transactions and
optional suppliers; the service stays testable and side-effect-free.
- 20 unit tests cover every pass, custom preservation, split exposure,
stats aggregation and pass priority.
Prepares the ground for #121 (migration writer UI).
Source of truth for the v1 IPC taxonomy on the TS side. Loads the bundled
JSON, exposes typed helpers (findById, findByPath, getLeaves, getParentById)
used by the upcoming Guide (#117) and Migration (#121) pages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Wrapper around dataExportService that creates and verifies a full SREF
backup before the v2->v1 categories migration. Throws on any failure to
ensure migration aborts cleanly.
- Generates filename <ProfileName>_avant-migration-<ISO8601>.sref
- Writes to ~/Documents/Simpl-Resultat/backups/ (creates dir if missing)
- Verifies integrity via re-read + SHA-256 checksum
- Reuses profile PIN for encryption when protected
- Adds two minimal Tauri commands: ensure_backup_dir, get_file_size
- Stable error codes (BackupError) to map to i18n keys in the UI layer
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>
Expense budgets are stored signed-negative by budgetService. The Cartes
budget-adherence card used raw values in its filter (monthBudget > 0),
its in-target comparison (|actual| <= monthBudget), and its overrun
calculation — all of which silently rejected every expense row. Route
every amount through Math.abs() so the card reflects real budget data.
Test: regression fixture with a signed-negative monthBudget that must
pass the filter and count as in-target or overrun based on absolute
values.
Fixes#112
- Extract shared defaultReferencePeriod helper (src/utils/referencePeriod.ts)
- useHighlights now reads ?refY=YYYY&refM=MM, defaults to previous month
- getHighlights signature: (referenceYear, referenceMonth, ytdYear, windowDays, ...)
- YTD tile pinned to Jan 1 of current civil year, independent of reference month
- CompareReferenceMonthPicker surfaced on /reports/highlights
- Hub highlights panel inherits the same default via useHighlights
- useCartes and useCompare now delegate their default-period helpers to the shared util