From 9608fd3618ed3a5a7af09834c94bf975198f4f30 Mon Sep 17 00:00:00 2001 From: le king fu Date: Sat, 6 Jun 2026 13:55:58 -0400 Subject: [PATCH] feat(balance): detail-account wizard (toggle to detailed at pivot date) (#215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../balance/BalanceAccountsTable.tsx | 29 +++- .../balance/DetailAccountWizard.test.ts | 35 ++++ .../balance/DetailAccountWizard.tsx | 163 ++++++++++++++++++ src/i18n/locales/en.json | 18 +- src/i18n/locales/fr.json | 18 +- src/pages/BalancePage.tsx | 17 ++ 6 files changed, 266 insertions(+), 14 deletions(-) create mode 100644 src/components/balance/DetailAccountWizard.test.ts create mode 100644 src/components/balance/DetailAccountWizard.tsx diff --git a/src/components/balance/BalanceAccountsTable.tsx b/src/components/balance/BalanceAccountsTable.tsx index ef93e42..c36585a 100644 --- a/src/components/balance/BalanceAccountsTable.tsx +++ b/src/components/balance/BalanceAccountsTable.tsx @@ -28,6 +28,7 @@ import { AlertTriangle, ChevronDown, ChevronRight, + ListTree, } from "lucide-react"; import type { AccountLatestSnapshot, @@ -86,6 +87,12 @@ interface BalanceAccountsTableProps { periodAnchor: AccountPeriodAnchor[]; onArchiveAccount?: (account: AccountLatestSnapshot) => void; onLinkTransfers?: (account: AccountLatestSnapshot) => void; + /** + * Open the "détailler en titres" wizard for a *simple* account (Issue #215). + * The action is only offered on `kind === 'simple'` rows; once detailed, the + * flip is one-way (service backstop #212) so no inverse action is exposed. + */ + onDetailAccount?: (account: AccountLatestSnapshot) => void; /** * Earliest snapshot date across the whole profile, used to anchor the * "depuis création" horizon. Falls back to "1A" range if not provided @@ -116,6 +123,7 @@ export default function BalanceAccountsTable({ periodAnchor, onArchiveAccount, onLinkTransfers, + onDetailAccount, sinceCreationDate, latentGainByAccount = {}, latentGainRollup, @@ -555,14 +563,19 @@ export default function BalanceAccountsTable({ {openMenuFor === acc.account_id && (
- + {onDetailAccount && acc.kind === "simple" && ( + + )} {onLinkTransfers && ( +
+ +
+

{t("balance.detailWizard.intro")}

+ +
    +
  • {t("balance.detailWizard.pointFrozen")}
  • +
  • {t("balance.detailWizard.pointNextSnapshot")}
  • +
  • {t("balance.detailWizard.pointPivot", { date: pivot })}
  • +
+ +
+ + {t("balance.detailWizard.irreversible")} +
+ + {error && ( +
{error}
+ )} +
+ +
+ + +
+ + , + document.body + ); +} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 7a4976a..eb01702 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1550,9 +1550,7 @@ "latestValue": "Latest value", "periodDelta": "Δ% over period", "noAccounts": "No active accounts. Create a balance account to get started.", - "accountsTitle": "Accounts", - "detailAction": "Details", - "detailComingSoon": "Available in a future release." + "accountsTitle": "Accounts" }, "period": { "legend": "Analysis period", @@ -1861,6 +1859,20 @@ "vehicle": { "none": "No envelope" }, + "detailWizard": { + "action": "Detail into securities", + "title": "Detail “{{account}}” into securities", + "intro": "This account will switch to per-security detailed entry. Securities are entered at the next regular snapshot — this wizard captures none right now.", + "pointFrozen": "Past aggregated history stays frozen read-only: older snapshots keep their single total value.", + "pointNextSnapshot": "From today on, every new snapshot will require the per-security breakdown (with cost basis).", + "pointPivot": "Pivot date: {{date}}.", + "irreversible": "One-way action: once securities are entered, this account can no longer return to aggregated entry.", + "confirm": "Detail", + "confirming": "Switching…", + "errors": { + "account_kind_detailed_has_holdings": "This account already has securities entered and can no longer return to aggregated entry." + } + }, "transfers": { "linkAction": "Link transfers", "direction": { diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 07a9860..0727c9f 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -1550,9 +1550,7 @@ "latestValue": "Dernière valeur", "periodDelta": "Δ% sur la période", "noAccounts": "Aucun compte actif. Commencez par créer un compte de bilan.", - "accountsTitle": "Comptes", - "detailAction": "Détail", - "detailComingSoon": "Disponible dans une prochaine version." + "accountsTitle": "Comptes" }, "period": { "legend": "Période d'analyse", @@ -1861,6 +1859,20 @@ "vehicle": { "none": "Sans enveloppe" }, + "detailWizard": { + "action": "Détailler en titres", + "title": "Détailler « {{account}} » en titres", + "intro": "Ce compte passera en saisie détaillée par titre. Les titres se saisissent au prochain instantané (snapshot) normal — cet assistant ne capture aucun titre maintenant.", + "pointFrozen": "L'historique agrégé passé reste figé en lecture seule : les anciens instantanés conservent leur valeur globale.", + "pointNextSnapshot": "À partir d'aujourd'hui, chaque nouvel instantané exigera le détail par titre (avec coût d'acquisition).", + "pointPivot": "Date de bascule (pivot) : {{date}}.", + "irreversible": "Action à sens unique : une fois des titres saisis, ce compte ne pourra plus revenir en saisie agrégée.", + "confirm": "Détailler", + "confirming": "Bascule…", + "errors": { + "account_kind_detailed_has_holdings": "Ce compte a déjà des titres saisis et ne peut plus revenir en saisie agrégée." + } + }, "transfers": { "linkAction": "Lier transferts", "direction": { diff --git a/src/pages/BalancePage.tsx b/src/pages/BalancePage.tsx index 838d366..f17565a 100644 --- a/src/pages/BalancePage.tsx +++ b/src/pages/BalancePage.tsx @@ -34,6 +34,7 @@ import BalanceOnboardingCard from "../components/balance/BalanceOnboardingCard"; import BalanceEvolutionChart from "../components/balance/BalanceEvolutionChart"; import BalanceAccountsTable from "../components/balance/BalanceAccountsTable"; import LinkTransfersModal from "../components/balance/LinkTransfersModal"; +import DetailAccountWizard from "../components/balance/DetailAccountWizard"; import StarterAccountsModal from "../components/balance/StarterAccountsModal"; import { getPreference, setPreference } from "../services/userPreferenceService"; import { renderCategoryLabelFromAccount } from "../utils/renderCategoryLabel"; @@ -52,6 +53,10 @@ export default function BalancePage() { const [linkTarget, setLinkTarget] = useState( null ); + // Issue #215 — "détailler en titres" wizard target (a simple account being + // flipped to detailed entry mode). + const [detailTarget, setDetailTarget] = + useState(null); const [categories, setCategories] = useState([]); const [transfersByAccount, setTransfersByAccount] = useState< Map @@ -324,6 +329,7 @@ export default function BalancePage() { vehicleLabels={vehicleLabels} onArchiveAccount={(acc) => handleArchiveAccount(acc.account_id)} onLinkTransfers={(acc) => setLinkTarget(acc)} + onDetailAccount={(acc) => setDetailTarget(acc)} /> @@ -348,6 +354,17 @@ export default function BalancePage() { }} /> )} + + {detailTarget && ( + setDetailTarget(null)} + onDetailed={() => { + void reload(); + }} + /> + )} ); } -- 2.45.2