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>
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>
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>