Commit graph

27 commits

Author SHA1 Message Date
le king fu
2a3cc07a0e Merge PR #251: feat(balance) CSV import of holdings in detailed snapshot (#245) 2026-07-05 17:03:00 -04:00
le king fu
c8c765e2f0 feat(balance): CSV import of holdings in detailed snapshot
All checks were successful
PR Check / rust (pull_request) Successful in 22m45s
PR Check / frontend (pull_request) Successful in 2m25s
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>
2026-07-04 18:04:12 -04:00
le king fu
82550d6d38 feat(balance): tile-based landing hub + reachable account management
All checks were successful
PR Check / rust (pull_request) Successful in 25m34s
PR Check / frontend (pull_request) Successful in 2m30s
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
2026-07-04 17:43:43 -04:00
le king fu
8cb7e53698 fix(db): serialize DB access to fix "database is locked"; live-update log console
All checks were successful
PR Check / rust (pull_request) Successful in 24m30s
PR Check / frontend (pull_request) Successful in 2m38s
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>
2026-06-30 16:50:39 -04:00
le king fu
9608fd3618 feat(balance): detail-account wizard (toggle to detailed at pivot date) (#215)
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>
2026-06-06 13:55:58 -04:00
le king fu
76ddad66c9 feat(balance): per-security drill-down + latent gain in accounts table (#216)
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>
2026-06-06 13:49:57 -04:00
le king fu
4846120b0f feat(balance): multi-security snapshot entry UI + SecurityPicker (#214)
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>
2026-06-06 13:39:42 -04:00
le king fu
737654579f feat(balance): rewrite snapshot editor reducer for holdings + dispatch on account.kind (#213)
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>
2026-06-06 13:29:48 -04:00
le king fu
0104e9223a feat(balance): chart vehicle/class toggle + collapsible returns (#204)
All checks were successful
PR Check / rust (pull_request) Successful in 22m13s
PR Check / frontend (pull_request) Successful in 2m21s
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>
2026-06-01 21:05:00 -04:00
le king fu
344c27ee6d feat(balance): account form vehicle field + category rename via custom_label (#203)
All checks were successful
PR Check / rust (pull_request) Successful in 22m29s
PR Check / frontend (pull_request) Successful in 2m23s
- AccountForm: optional vehicle_type dropdown (account mode, 6 fiscal
  envelopes + none) wired into create/update; custom_label field (category
  mode) written to custom_label, never i18n_key.
- AccountsPage rename: writes custom_label, leaves i18n_key intact (fixes
  bug I where renaming clobbered the translation key).
- New shared renderCategoryLabel helper (+ shape adapters) applied at the 5
  sites: AccountsPage, AccountForm, SnapshotEditor, BalanceAccountsTable,
  BalancePage.
- Hide v13-deactivated seeds: useBalanceAccounts passes includeInactive=false
  (keeps #202 behavior-neutral default + tests green).
- BALANCE_VEHICLE_TYPES exported from shared/types as single source of truth
  (service reuses it).
- i18n FR+EN: vehicleType.*, category.form.customLabel*, errors.vehicle_type_invalid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 20:50:40 -04:00
le king fu
5861346eb3 feat(balance): data layer — vehicle_type + custom_label migrations, starters, service (#202)
All checks were successful
PR Check / rust (pull_request) Successful in 22m31s
PR Check / frontend (pull_request) Successful in 2m26s
Bilan axe véhicule (Étape 1) data foundation: separate the fiscal envelope
(now balance_accounts.vehicle_type) from the asset class (the category).

- Migration v12 (additive): add vehicle_type (fiscal enum, nullable) to
  balance_accounts + custom_label to balance_categories; backfill the envelope
  onto ex-tfsa/rrsp accounts (cash stays NULL); defensive recovery of any seed
  i18n_key overwritten by free text (bug I).
- Migration v13 (reclass, conditional/idempotent): re-link ex-tfsa/rrsp accounts
  to the `other` asset class and deactivate the two envelope seeds.
- consolidated_schema.sql: 2 new columns, 5 asset-class seeds (no tfsa/rrsp),
  CELI/REER starters re-pointed to `other` + vehicle_type (avoids NULL FK).
- Types: BalanceVehicleType, custom_label / vehicle_type / category_custom_label.
- Service: normalizeVehicleType + vehicle_type_invalid; CRUD writes the new
  columns; SELECT/JOINs read them back; STARTER_ACCOUNTS + proposeStarterAccounts
  (is_active=1 + vehicle_type) + getStarterCollisions adjusted.
- Tests: Rust chain v9→v13 (snapshot_lines identical, transfers intact, archived
  ex-tfsa covered, seeds deactivated, v13 EXISTS guard, idempotence, CHECK reject)
  + consolidated complete (5 categories, 4 starters, 0 NULL FK); TS service +
  StarterAccountsModal specs. No diff to migrations <= v11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 20:37:56 -04:00
a73bf2ebb0 feat(balance): audit quick wins — terminology, optional symbol, movable snapshot date (#201) 2026-05-31 21:05:13 +00:00
le king fu
4095aec453 Merge remote-tracking branch 'origin/main' into issue-187-balance-cleanup-post-184-185
All checks were successful
PR Check / rust (pull_request) Successful in 22m28s
PR Check / frontend (pull_request) Successful in 2m33s
# Conflicts:
#	CHANGELOG.fr.md
#	CHANGELOG.md
2026-05-03 19:41:55 -04:00
le king fu
a7daabdf70 refactor(balance): use useTranslation directly in BalanceOnboardingCard.Step (S5)
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>
2026-05-03 16:29:12 -04:00
le king fu
8c3a64d172 fix(balance): re-check collisions in-transaction in proposeStarterAccounts (S3)
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>
2026-05-03 16:27:16 -04:00
le king fu
2eeac78b40 fix(balance): exclude archived accounts from starter collisions (S4)
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>
2026-05-03 16:26:23 -04:00
le king fu
3b9badb726 fix(ui): apply WebKitGTK date picker workaround to remaining 7 inputs (#188)
All checks were successful
PR Check / rust (pull_request) Successful in 22m55s
PR Check / frontend (pull_request) Successful in 2m29s
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>
2026-05-03 16:19:20 -04:00
le king fu
cd0a2b826f feat(balance): starter accounts + opt-in modal + ADR 0012
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
2026-05-02 11:59:45 -04:00
le king fu
eac2a516b5 feat(balance): 2-step onboarding card on /balance empty state
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
2026-05-02 11:48:57 -04:00
le king fu
3963f552ae feat(balance): add asset_type column to balance_categories
All checks were successful
PR Check / rust (push) Successful in 23m42s
PR Check / frontend (push) Successful in 2m26s
PR Check / rust (pull_request) Successful in 22m55s
PR Check / frontend (pull_request) Successful in 2m24s
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
2026-04-28 19:54:04 -04:00
le king fu
043e9bf622 feat(prices): PriceFetchControl component + consent modal + best-effort UX
All checks were successful
PR Check / rust (push) Successful in 30m45s
PR Check / frontend (push) Successful in 3m12s
PR Check / rust (pull_request) Successful in 28m50s
PR Check / frontend (pull_request) Successful in 3m15s
- 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
2026-04-27 08:36:23 -04:00
le king fu
faa09614a3 feat(balance): add transfer markers on evolution chart
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>
2026-04-25 16:38:55 -04:00
le king fu
a45e5c3cd0 feat(balance): add LinkTransfersModal + return columns in accounts table
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>
2026-04-25 16:38:24 -04:00
le king fu
ffefa90fd0 feat(balance): add BalancePage with chart + accounts table
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>
2026-04-25 16:07:04 -04:00
le king fu
6288a3fe23 feat(balance): support priced kind in AccountForm + SnapshotLineRow
- 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>
2026-04-25 15:01:38 -04:00
le king fu
fdc6cc6c38 feat(balance): add useSnapshotEditor hook + SnapshotEditPage + components
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>
2026-04-25 14:49:33 -04:00
le king fu
fccc8e4fa2 feat(balance): add useBalanceAccounts hook + AccountsPage + AccountForm
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>
2026-04-25 14:37:30 -04:00