diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 0043bd4..7a536b4 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -2,6 +2,10 @@ ## [Non publié] +### Modifié + +- Bilan : remplacement de l'état vide de /balance par une carte d'onboarding à 2 étapes (Créer un compte → Saisir un snapshot) pour éviter l'écran « aucun snapshot » déroutant avant qu'un compte n'existe. Le bouton « + Nouveau snapshot » est masqué tant qu'aucun compte n'existe. La copie de l'état vide de /balance/snapshot clarifie la différence entre un compte et un snapshot (#178). + ### Corrigé - Bilan : correction de l'erreur SQLite « misuse of aggregate function MIN() » au chargement de /balance avec des snapshots existants ; remplacement du pattern aggregate-in-WHERE par une window function ROW_NUMBER() dans getAccountsPeriodAnchor (#175). diff --git a/CHANGELOG.md b/CHANGELOG.md index e7c0c29..9635c56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Bilan: replaced empty /balance state with a 2-step onboarding card (Create an account → Enter a snapshot) so users no longer see a confusing "no snapshot" screen before any account exists. The "+ New snapshot" button is hidden until at least one account exists. The /balance/snapshot empty-state copy now clarifies what an account is vs. what a snapshot is (#178). + ### Fixed - Bilan: fix SQLite "misuse of aggregate function MIN()" error when loading /balance with existing snapshots; replaced aggregate-in-WHERE pattern with ROW_NUMBER() window function in getAccountsPeriodAnchor (#175). diff --git a/docs/architecture.md b/docs/architecture.md index a6082ef..5c43820 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -28,7 +28,7 @@ simpl-resultat/ ├── src/ # Frontend React/TypeScript │ ├── components/ # 58 composants organisés par domaine │ │ ├── adjustments/ # 3 composants -│ │ ├── balance/ # 7 composants Bilan (AccountForm, BalanceAccountsTable, BalanceEvolutionChart, BalanceOverviewCard, LinkTransfersModal, SnapshotEditor, SnapshotLineRow) +│ │ ├── balance/ # 8 composants Bilan (AccountForm, BalanceAccountsTable, BalanceEvolutionChart, BalanceOnboardingCard, BalanceOverviewCard, LinkTransfersModal, SnapshotEditor, SnapshotLineRow) │ │ ├── budget/ # 5 composants │ │ ├── categories/ # 5 composants │ │ ├── dashboard/ # 2 composants diff --git a/src/components/balance/BalanceOnboardingCard.test.tsx b/src/components/balance/BalanceOnboardingCard.test.tsx new file mode 100644 index 0000000..a9aecd3 --- /dev/null +++ b/src/components/balance/BalanceOnboardingCard.test.tsx @@ -0,0 +1,41 @@ +// BalanceOnboardingCard — unit tests (issue #178) +// +// NOTE: This project does not have @testing-library/react or jsdom configured +// (logged as MEDIUM in autopilot decisions-log). Tests cover the pure +// `deriveOnboardingSteps` helper that drives the visual state of each step. +// All React rendering is bypassed. + +import { describe, it, expect } from "vitest"; +import { deriveOnboardingSteps } from "./BalanceOnboardingCard"; + +describe("BalanceOnboardingCard — deriveOnboardingSteps", () => { + it("0 accounts, 0 snapshots → step1 active, step2 disabled", () => { + const r = deriveOnboardingSteps(0, 0); + expect(r.step1).toBe("active"); + expect(r.step2).toBe("disabled"); + }); + + it(">=1 account, 0 snapshots → step1 done, step2 active", () => { + const r = deriveOnboardingSteps(1, 0); + expect(r.step1).toBe("done"); + expect(r.step2).toBe("active"); + + const r2 = deriveOnboardingSteps(5, 0); + expect(r2.step1).toBe("done"); + expect(r2.step2).toBe("active"); + }); + + it(">=1 account, >=1 snapshot → both done (defensive — card normally hidden)", () => { + const r = deriveOnboardingSteps(2, 3); + expect(r.step1).toBe("done"); + expect(r.step2).toBe("done"); + }); + + it("guard: 0 accounts but >=1 snapshot (anomaly) → step1 active, step2 done", () => { + // This combination should not happen in practice (a snapshot requires at + // least one account), but the helper handles it conservatively. + const r = deriveOnboardingSteps(0, 1); + expect(r.step1).toBe("active"); + expect(r.step2).toBe("done"); + }); +}); diff --git a/src/components/balance/BalanceOnboardingCard.tsx b/src/components/balance/BalanceOnboardingCard.tsx new file mode 100644 index 0000000..3f88402 --- /dev/null +++ b/src/components/balance/BalanceOnboardingCard.tsx @@ -0,0 +1,210 @@ +// BalanceOnboardingCard — empty-state onboarding for /balance. +// +// Issue #178. Replaces the BalanceOverviewCard when the user has no accounts +// or no snapshots yet. Two vertical steps: +// 1. Create an account → /balance/accounts +// 2. Enter a snapshot → /balance/snapshot +// +// Each step has 3 states: +// - "active": primary CTA, currently the next thing to do +// - "done": marked with a checkmark, no CTA +// - "disabled": grayed out (e.g. step 2 when 0 accounts), CTA disabled +// +// The whole card is replaced by BalanceOverviewCard once at least one +// snapshot exists, so step 2 in practice is rendered as "active" or +// "disabled"; the "done" branch is supported for completeness/tests. + +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; +import type { TFunction } from "i18next"; +import { Wallet, FileText, Check, ArrowRight } from "lucide-react"; + +interface BalanceOnboardingCardProps { + /** Number of active (non-archived) accounts. */ + accountsCount: number; + /** Number of snapshots saved (any date). */ + snapshotsCount: number; +} + +export type StepState = "active" | "done" | "disabled"; + +/** + * Pure helper exposed for unit tests — derives the state of each onboarding + * step from the (accountsCount, snapshotsCount) pair. + * + * - Step 1 is "done" once at least one account exists, "active" otherwise. + * - Step 2 is "done" once any snapshot exists, "active" once at least one + * account exists, "disabled" otherwise. In practice the parent guard on + * /balance only renders this card when snapshotsCount === 0, so the + * "done" branch for step 2 is mostly defensive. + */ +export function deriveOnboardingSteps( + accountsCount: number, + snapshotsCount: number +): { step1: StepState; step2: StepState } { + const step1: StepState = accountsCount >= 1 ? "done" : "active"; + const step2: StepState = + snapshotsCount >= 1 + ? "done" + : accountsCount >= 1 + ? "active" + : "disabled"; + return { step1, step2 }; +} + +export default function BalanceOnboardingCard({ + accountsCount, + snapshotsCount, +}: BalanceOnboardingCardProps) { + const { t } = useTranslation(); + + const { step1: step1State, step2: step2State } = deriveOnboardingSteps( + accountsCount, + snapshotsCount + ); + + return ( +
+ {t("balance.onboarding.subtitle")} +
+ +{description}
+ {isDisabled && disabledHint && ( ++ {disabledHint} +
+ )} +