feat(balance): i18n + CHANGELOG for returns/transfers
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>
This commit is contained in:
parent
faa09614a3
commit
ca275821bc
4 changed files with 110 additions and 0 deletions
|
|
@ -3,6 +3,7 @@
|
|||
## [Non publié]
|
||||
|
||||
### Ajouté
|
||||
- **Bilan — rendements Modified Dietz et liaison de transferts** (route `/balance`) : le rendement par compte arrive enfin. Nouveau module Rust `commands/return_calculator.rs` qui implémente la formule Modified Dietz `R = (V_fin − V_début − ΣCF_i) / (V_début + ΣW_i × CF_i)` avec pondération des apports à la précision du jour `W_i = (T − t_i) / T`, et annualisation `(1 + R)^(365/T) − 1`. Les cas limites — snapshot d'extrémité manquant, aucun flux taggé sur la période, compte créé en cours de période, vidé puis rechargé, période de durée nulle — sont surfacés via les flags explicites `is_partial` / `has_no_transfers_warning` pour que l'UI affiche un tiret + tooltip clair plutôt qu'un nombre incompréhensible. Nouvelle commande Tauri `compute_account_return(account_id, period_start, period_end)` qui exécute trois lectures SQL courtes contre la BD du profil actif (dernier snapshot ≤ début de période, dernier snapshot ≤ fin de période, transferts joints aux transactions filtrés sur la période) puis alimente le calculateur. Sept tests Rust co-localisés en TDD couvrent chaque cas avant l'implémentation. Le tableau des comptes sur `/balance` affiche désormais quatre colonnes supplémentaires côte à côte : 3M / 1A / Depuis création (Modified Dietz) plus une colonne *Non ajusté* qui calcule simplement `(V_fin − V_début) / V_début` pour qu'on voie d'un coup d'œil quelle part du rendement vient de la pondération des apports. Le menu d'actions de chaque ligne reçoit l'item *Lier transferts* qui ouvre une modal de sélection multiple avec filtres période / catégorie / recherche texte ; la modal propose automatiquement le sens (`in` pour les montants bancaires négatifs, `out` pour les positifs) et l'utilisateur peut inverser ligne par ligne avant de soumettre. Les transactions liées à un ou plusieurs comptes de bilan affichent maintenant une petite icône `Link2` à côté de la description dans la page *Transactions*, avec un tooltip listant les noms et sens des comptes. Les chemins de suppression en lot (par fichier importé et tout effacer) pré-vérifient l'existence d'un lien dans `balance_account_transfers` et surfacent l'erreur typée `TransactionLinkedToBalanceError` (« Cette transaction est liée au compte de bilan X — déliez-la avant de supprimer ») au lieu de laisser fuiter l'erreur SQLite brute. Le graphique d'évolution sur `/balance` superpose désormais des lignes verticales de référence à chaque date de transfert lié (vert pour `in`, rouge pour `out`). Nouvelles clés i18n sous `balance.returns.*`, `balance.accountsTable.*`, `balance.transfers.*`, `balance.evolution.*`, `transactions.transferIcon.*` (FR + EN) (#142)
|
||||
- **Bilan — page `/balance` avec graphique d'évolution et entrée sidebar** (route `/balance`) : quatrième tranche de la feature *Bilan*, qui la rend enfin accessible depuis la navigation. La nouvelle page compose (1) une carte d'aperçu avec la valeur nette agrégée du dernier snapshot, le Δ% par rapport au snapshot chronologiquement précédent (affiché « — » quand il n'existe qu'un seul snapshot), un avertissement de fraîcheur quand le dernier snapshot date de plus de 60 jours, et un CTA *Nouveau snapshot* qui pointe vers `/balance/snapshot` ; (2) un sélecteur de période (3 mois / 6 mois / 1 an / 3 ans / Tout) qui recharge toutes les séries en parallèle ; (3) un graphique d'évolution avec deux modes — *Ligne* (une seule série `SUM(value) GROUP BY snapshot_date`) et *Empilé par catégorie* (une `<Area stackId>` Recharts par `balance_categories.key`) ; (4) un tableau des comptes listant chaque compte actif avec sa dernière valeur snapshot, le Δ% par compte sur la période active (valeur la plus récente vs valeur du premier snapshot dans la fenêtre — null si pas d'ancrage, affiché « — »), et un menu d'actions (Détail désactivé en attendant la #142, Archiver). Les colonnes de rendement (3M / 1A / depuis création / non ajusté) sont réservées pour une version ultérieure avec un commentaire `TODO`. La sidebar expose désormais l'entrée *Bilan* (icône `Wallet`) entre *Rapports* et *Paramètres*. Le service gagne trois helpers de série temporelle : `getSnapshotTotalsByDate(range?)`, `getSnapshotTotalsByCategoryAndDate(range?)`, `getAccountsLatestSnapshot()` ainsi qu'un calcul d'ancrage par compte `getAccountsPeriodAnchor(range)` — tous couverts par des tests unitaires. Nouveau hook `useBalanceOverview` (`useReducer` scoped) qui pilote l'état de la page. Nouvelles clés i18n sous `balance.overview.*`, `balance.period.*`, `balance.chart.*` plus `nav.balance` (FR + EN) (#141)
|
||||
- **Bilan — type coté (quantité × prix unitaire)** (routes `/balance/accounts` et `/balance/snapshot`) : troisième tranche de la feature *Bilan*. Les catégories exposent désormais un sélecteur de *type* à la création : `simple` (saisie d'un montant direct) ou `coté` (`quantité × prix_unitaire`). Les comptes liés à une catégorie cotée exigent un symbole. L'éditeur de snapshot bascule selon le type de la catégorie du compte : les comptes simples conservent leur unique champ de valeur ; les comptes cotés affichent trois champs — `quantité`, `prix unitaire` (les deux obligatoires) et un champ `valeur` en lecture seule calculé en temps réel à partir de `quantité × prix unitaire` (arrondi à 2 décimales). Une étiquette d'attribution `[Manuel]` apparaît sur chaque ligne cotée ; la future étiquette `[via Maximus le AAAA-MM-JJ]` arrivera avec la récupération automatique des prix. Le bouton *Pré-remplir depuis le précédent* copie maintenant les quantités pour les comptes cotés mais laisse les prix unitaires vides (un prix frais doit être saisi à chaque fois). Le service valide les lignes cotées avant la CHECK SQL : invariants de type (les lignes cotées doivent porter à la fois quantité et prix unitaire ; les lignes simples ne doivent porter ni l'un ni l'autre) et invariant de valeur `|valeur − quantité × prix unitaire| ≤ 0,01` (un centime de tolérance pour absorber les arrondis flottants). La suppression d'une catégorie est désormais mieux guardée : une catégorie liée à un ou plusieurs comptes affiche un bandeau d'erreur listant le nombre et jusqu'à trois noms de comptes pour que l'utilisateur sache exactement lesquels archiver d'abord ; les catégories standard restent protégées côté service avec leur bouton désactivé dans l'interface. Nouvelles clés i18n `balance.category.kind.*`, `balance.category.form.kindLabel/kindHint*`, `balance.category.error.has_accounts`, `balance.snapshot.priced.*` (FR + EN) (#140)
|
||||
- **Bilan — éditeur de snapshot (type simple)** (route `/balance/snapshot`) : deuxième tranche de la feature *Bilan*. La nouvelle page permet de créer ou modifier un snapshot daté de votre patrimoine : choisissez une date (par défaut aujourd'hui), saisissez la valeur de chaque compte actif groupé par catégorie, puis enregistrez. Le mode est piloté par le paramètre `?date=` de l'URL — si un snapshot existe déjà à cette date, la page bascule automatiquement en mode édition (la contrainte UNIQUE sur `balance_snapshots.snapshot_date` garantit un snapshot par jour). La date d'un snapshot existant est immuable : pour la changer, supprimez puis recréez. Un bouton *Pré-remplir depuis le précédent* copie les valeurs du snapshot antérieur le plus récent (comptes simples uniquement — les comptes cotés seront pris en charge quand l'éditeur coté arrivera). Un bouton *Supprimer* affiche une modal de double confirmation qui exige de retaper la date du snapshot avant d'activer l'action destructive. Seules les valeurs de type simple sont acceptées à ce stade (`quantity` et `unit_price` sont laissés `NULL`) ; l'éditeur coté (quantité × prix unitaire + récupération de prix) arrivera dans une prochaine version. Nouveau hook `useSnapshotEditor` (`useReducer` couvrant tout le cycle de vie) et deux nouveaux composants `SnapshotEditor` + `SnapshotLineRow`. i18n FR/EN sous `balance.snapshot.*` (#146)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Balance sheet — Modified Dietz returns and transfer linking** (route `/balance`): per-account performance now ships. New Rust module `commands/return_calculator.rs` implements the Modified Dietz formula `R = (V_end − V_start − ΣCF_i) / (V_start + ΣW_i × CF_i)` with day-precision contribution weights `W_i = (T − t_i) / T`, plus `(1 + R)^(365/T) − 1` annualization. Edge cases — missing endpoint snapshot, no flows tagged in the period, account created mid-period, depleted-then-refilled, zero-length period — are surfaced with explicit `is_partial` / `has_no_transfers_warning` flags so the UI shows a clean dash + tooltip instead of a confusing number. The new Tauri command `compute_account_return(account_id, period_start, period_end)` runs three short SQL reads against the active profile DB (latest snapshot ≤ period start, latest snapshot ≤ period end, transfers JOINed with transactions filtered to the period) and feeds the calculator. Seven co-located TDD tests cover every case before the implementation. The accounts table on `/balance` now shows four extra columns side-by-side: 3M / 1Y / Since-inception (Modified Dietz) plus an *Unadjusted* column showing the simple `(V_end − V_start) / V_start` so the user can see at a glance how much of the return came from contribution timing. Each row's actions menu gains a *Link transfers* item that opens a multi-select modal with date range / category / free-text filters; the modal auto-proposes the direction (`in` for negative bank amounts, `out` for positive) and the user can flip it per row before submitting. Transactions linked to one or more balance accounts now show a small `Link2` icon next to the description in the *Transactions* page, with a tooltip listing the account name(s) and direction(s). Bulk transaction-deletion paths (per-imported-file and clear-all) now pre-check for any link in `balance_account_transfers` and surface a typed `TransactionLinkedToBalanceError` ("This transaction is linked to balance account X — unlink it before deleting") instead of leaking the raw SQLite FK error. The evolution chart on `/balance` now overlays vertical reference lines at every linked-transfer date (green for `in`, red for `out`). New i18n keys under `balance.returns.*`, `balance.accountsTable.*`, `balance.transfers.*`, `balance.evolution.*`, `transactions.transferIcon.*` (FR + EN) (#142)
|
||||
- **Balance sheet — `/balance` overview page, evolution chart and sidebar entry** (route `/balance`): fourth slice of the *Bilan* feature finally surfaces it in the navigation. The new page composes (1) an overview card with the latest aggregate net worth, the Δ% versus the previous chronological snapshot (rendered as "—" when only one snapshot exists), a 60-day staleness warning when the latest snapshot is older than that threshold, and a *New snapshot* CTA pointing at `/balance/snapshot`; (2) a period selector (3 months / 6 months / 1 year / 3 years / All) that re-fetches every series in parallel; (3) an evolution chart with two modes — *Line* (single series of `SUM(value) GROUP BY snapshot_date`) and *Stacked by category* (one Recharts `<Area stackId>` per `balance_categories.key`); (4) an accounts table listing every active account with its latest snapshot value, the per-account Δ% over the active period (latest value vs the value at the earliest snapshot inside the window — null when no anchor exists, rendered as "—"), and an actions menu (Details placeholder, Archive). Return-metric columns (3M / 1Y / since-creation / unadjusted) are reserved for a later release with a `TODO` marker. The sidebar now exposes the *Balance sheet* entry (`Wallet` icon) between *Reports* and *Settings*. The service grows three time-series helpers: `getSnapshotTotalsByDate(range?)`, `getSnapshotTotalsByCategoryAndDate(range?)`, `getAccountsLatestSnapshot()` and a per-account anchor query `getAccountsPeriodAnchor(range)` — all guarded by unit tests. New `useBalanceOverview` hook (scoped `useReducer`) drives the page state. New i18n keys under `balance.overview.*`, `balance.period.*`, `balance.chart.*` plus `nav.balance` (FR + EN) (#141)
|
||||
- **Balance sheet — priced kind (quantity × unit price)** (routes `/balance/accounts` and `/balance/snapshot`): third slice of the *Bilan* feature. Categories now expose a *kind* selector at creation: `simple` (direct value entry) or `priced` (`quantity × unit_price`). Accounts linked to a priced category require a symbol. The snapshot editor dispatches on the account's category kind: simple accounts keep their single value field, priced accounts get three inputs — `quantity`, `unit_price` (both required) and a read-only `value` field computed live from `quantity × unit_price` (rounded to 2 decimals). A `[Manual]` / `[Manuel]` attribution tag is shown on each priced row; the future `[via Maximus on YYYY-MM-DD]` tag will land with automatic price-fetching. The *Prefill from previous* button now copies quantities for priced accounts but leaves unit prices blank (a fresh price must be entered each time). The service validates priced lines ahead of the SQL CHECK: kind invariants (priced lines must carry both quantity and unit_price; simple lines must carry neither) and a value-match invariant `|value − quantity × unit_price| ≤ 0.01` (one cent tolerance to absorb floating-point drift). Category deletion now blocks earlier and surfaces a richer error: a category linked to one or more accounts shows a dismissable banner listing the count and up to three account names so the user knows exactly which accounts to archive first; seeded categories remain protected at the service layer with their button disabled in the UI. New i18n keys `balance.category.kind.*`, `balance.category.form.kindLabel/kindHint*`, `balance.category.error.has_accounts`, `balance.snapshot.priced.*` (FR + EN) (#140)
|
||||
- **Balance sheet — snapshot editor (simple kind)** (route `/balance/snapshot`): second slice of the *Bilan* feature. The new page lets you create or edit a dated snapshot of your balance: pick a date (defaulting to today), enter the value of each active account grouped by category, and save. The mode is driven by the `?date=` query parameter — when a snapshot already exists at that date the page automatically flips into edit mode (the underlying `balance_snapshots.snapshot_date` UNIQUE constraint guarantees one snapshot per day). The date of an existing snapshot is immutable: to change it, delete the snapshot and create a new one. A *Prefill from previous snapshot* button copies values from the most recent earlier snapshot (simple-kind accounts only — priced accounts will be handled when the priced editor lands in a later release). A *Delete* button surfaces a double-confirmation modal that requires retyping the snapshot date before the destructive action is enabled. Only simple-kind values are accepted at this stage (`quantity` and `unit_price` are kept `NULL`); the priced editor (quantity × unit price + price fetch) ships in a later release. New `useSnapshotEditor` hook (scoped `useReducer` covering the full lifecycle) and two new components `SnapshotEditor` + `SnapshotLineRow`. FR/EN i18n under `balance.snapshot.*` (#146)
|
||||
|
|
|
|||
|
|
@ -253,6 +253,10 @@
|
|||
"Assign categories by clicking the category dropdown on each row",
|
||||
"Auto-categorize uses your keyword rules to categorize transactions in bulk"
|
||||
]
|
||||
},
|
||||
"transferIcon": {
|
||||
"tooltip": "Linked to a balance account",
|
||||
"ariaLabel": "Transaction linked to a balance account"
|
||||
}
|
||||
},
|
||||
"categories": {
|
||||
|
|
@ -1638,6 +1642,56 @@
|
|||
"snapshot_priced_unit_price_required": "Unit price is required for priced accounts.",
|
||||
"snapshot_priced_value_mismatch": "The entered value does not match quantity × unit price.",
|
||||
"snapshot_simple_must_be_scalar": "A simple value must not carry quantity or price."
|
||||
},
|
||||
"returns": {
|
||||
"partialTooltip": "Partial return: a snapshot is missing for the selected period.",
|
||||
"noTransfersWarning": "No transfers tagged — performance may be skewed if contributions weren't tagged."
|
||||
},
|
||||
"accountsTable": {
|
||||
"return3m": "3M",
|
||||
"return3mTooltip": "Modified Dietz return over the last 90 days.",
|
||||
"return1y": "1Y",
|
||||
"return1yTooltip": "Modified Dietz return over the last 365 days.",
|
||||
"sinceCreation": "Since inception",
|
||||
"sinceCreationTooltip": "Modified Dietz return since the first snapshot.",
|
||||
"unadjusted": "Unadjusted",
|
||||
"unadjustedTooltip": "Simple return (V_end − V_start) / V_start, with no contribution weighting."
|
||||
},
|
||||
"transfers": {
|
||||
"linkAction": "Link transfers",
|
||||
"direction": {
|
||||
"in": "In",
|
||||
"out": "Out"
|
||||
},
|
||||
"modal": {
|
||||
"title": "Link transfers to {{account}}",
|
||||
"subtitle": "Select transactions to attribute to this balance account. The direction is suggested based on the amount sign.",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"category": "Category",
|
||||
"anyCategory": "Any category",
|
||||
"search": "Search",
|
||||
"searchPlaceholder": "Keyword in description…",
|
||||
"loading": "Loading…",
|
||||
"noTransactions": "No transactions match the filters.",
|
||||
"direction": "Direction",
|
||||
"toggleDirection": "Click to flip direction",
|
||||
"summary": "{{selected}} selected of {{total}} shown",
|
||||
"linkSelection": "Link {{count}} transaction(s)",
|
||||
"linking": "Linking…",
|
||||
"partialFailure": "{{linked}}/{{total}} linked successfully"
|
||||
},
|
||||
"errors": {
|
||||
"transfer_direction_invalid": "Invalid transfer direction (expected in/out).",
|
||||
"transfer_already_linked": "This transaction is already linked to this account.",
|
||||
"transfer_not_linked": "This transaction is not linked to this account.",
|
||||
"transfer_active_profile_unknown": "No active profile — cannot compute return.",
|
||||
"transaction_linked_to_balance_account": "This transaction is linked to balance account {{account}} — unlink it before deleting."
|
||||
}
|
||||
},
|
||||
"evolution": {
|
||||
"transferIn": "In",
|
||||
"transferOut": "Out"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,6 +253,10 @@
|
|||
"Assignez une catégorie via le menu déroulant sur chaque ligne",
|
||||
"L'auto-catégorisation utilise vos règles de mots-clés pour catégoriser en masse"
|
||||
]
|
||||
},
|
||||
"transferIcon": {
|
||||
"tooltip": "Liée à un compte de bilan",
|
||||
"ariaLabel": "Transaction liée à un compte de bilan"
|
||||
}
|
||||
},
|
||||
"categories": {
|
||||
|
|
@ -1638,6 +1642,56 @@
|
|||
"snapshot_priced_unit_price_required": "Le prix unitaire est obligatoire pour les comptes cotés.",
|
||||
"snapshot_priced_value_mismatch": "La valeur saisie ne correspond pas à quantité × prix unitaire.",
|
||||
"snapshot_simple_must_be_scalar": "Une valeur simple ne doit pas comporter de quantité ou de prix."
|
||||
},
|
||||
"returns": {
|
||||
"partialTooltip": "Rendement partiel : un snapshot manque pour calculer la performance sur cette période.",
|
||||
"noTransfersWarning": "Aucun transfert lié — la performance peut être faussée si des apports n'ont pas été tagués."
|
||||
},
|
||||
"accountsTable": {
|
||||
"return3m": "3M",
|
||||
"return3mTooltip": "Rendement Modified Dietz sur les 90 derniers jours.",
|
||||
"return1y": "1A",
|
||||
"return1yTooltip": "Rendement Modified Dietz sur les 365 derniers jours.",
|
||||
"sinceCreation": "Depuis création",
|
||||
"sinceCreationTooltip": "Rendement Modified Dietz depuis le premier snapshot.",
|
||||
"unadjusted": "Non ajusté",
|
||||
"unadjustedTooltip": "Rendement simple (V_fin − V_début) / V_début, sans pondération des apports."
|
||||
},
|
||||
"transfers": {
|
||||
"linkAction": "Lier transferts",
|
||||
"direction": {
|
||||
"in": "Entrée",
|
||||
"out": "Sortie"
|
||||
},
|
||||
"modal": {
|
||||
"title": "Lier des transferts à {{account}}",
|
||||
"subtitle": "Sélectionnez les transactions à attribuer à ce compte de bilan. La direction est proposée d'après le signe du montant.",
|
||||
"from": "Du",
|
||||
"to": "Au",
|
||||
"category": "Catégorie",
|
||||
"anyCategory": "Toutes les catégories",
|
||||
"search": "Rechercher",
|
||||
"searchPlaceholder": "Mot-clé dans la description…",
|
||||
"loading": "Chargement…",
|
||||
"noTransactions": "Aucune transaction ne correspond aux filtres.",
|
||||
"direction": "Sens",
|
||||
"toggleDirection": "Cliquer pour inverser le sens",
|
||||
"summary": "{{selected}} sélectionnée(s) sur {{total}} affichée(s)",
|
||||
"linkSelection": "Lier {{count}} transaction(s)",
|
||||
"linking": "Liaison…",
|
||||
"partialFailure": "{{linked}}/{{total}} liées avec succès"
|
||||
},
|
||||
"errors": {
|
||||
"transfer_direction_invalid": "Direction de transfert invalide (in/out attendu).",
|
||||
"transfer_already_linked": "Cette transaction est déjà liée à ce compte.",
|
||||
"transfer_not_linked": "Cette transaction n'est pas liée à ce compte.",
|
||||
"transfer_active_profile_unknown": "Aucun profil actif — impossible de calculer le rendement.",
|
||||
"transaction_linked_to_balance_account": "Cette transaction est liée au compte de bilan {{account}} — déliez-la avant de supprimer."
|
||||
}
|
||||
},
|
||||
"evolution": {
|
||||
"transferIn": "Entrée",
|
||||
"transferOut": "Sortie"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue