feat(docs): sub-sections in the user guide and bank CSV export guides (#355) #381

Merged
maximus merged 1 commit from issue-355-docs-bank-guides into main 2026-10-04 12:51:44 +00:00
6 changed files with 470 additions and 8 deletions
Showing only changes of commit e0ee09bca7 - Show all commits

View file

@ -19,13 +19,14 @@ Simpl'Résultat vous aide à suivre vos finances personnelles en important des r
### Démarrage rapide
1. Au premier lancement, choisissez ou créez un profil — chaque profil a sa propre base de données
2. Allez dans Paramètres et définissez votre dossier d'import — créez un sous-dossier par compte bancaire
3. Placez vos relevés bancaires CSV dans le sous-dossier correspondant
4. Ouvrez la page Import et configurez votre source (mapping des colonnes, délimiteur, format de date)
5. Importez vos transactions, puis allez dans Catégories pour configurer les règles de mots-clés
6. Utilisez l'auto-catégorisation sur la page Transactions pour appliquer vos règles en masse
7. Configurez votre Budget et suivez votre progression via les Rapports
Les six étapes ci-dessous sont celles de la liste de démarrage affichée sur le tableau de bord d'un profil neuf ; chaque case se coche d'elle-même quand l'étape est faite.
1. Créer un profil — chaque profil a sa propre base de données
2. Choisir un dossier d'import — le dossier où vous déposerez vos relevés, un sous-dossier par compte bancaire
3. Configurer une source d'import — la première ouverture d'un sous-dossier détecte le format du fichier
4. Importer un premier fichier — après avoir lu l'aperçu et son récapitulatif signé
5. Classer une transaction à la main — le seed de mots-clés fait le gros du travail, une correction manuelle affine le reste
6. Saisir un premier budget — édition Base et plus ; suivez ensuite votre progression via les Rapports
### Astuces
@ -113,7 +114,7 @@ Importez des relevés bancaires à partir de fichiers CSV à l'aide d'un assista
### Comment faire
1. Définissez votre dossier d'import via le sélecteur de dossier en haut de la page
2. Créez un sous-dossier pour chaque banque/source et placez-y les fichiers CSV
2. Créez un sous-dossier pour chaque banque/source et placez-y les fichiers CSV — tant que le dossier est vide, le bouton « Créer un dossier d'exemple » dépose deux sous-dossiers fictifs (« Exemple Desjardins », « Exemple Tangerine ») pour essayer l'assistant sans attendre vos vrais relevés ; supprimez-les ensuite
3. Cliquez sur une source pour ouvrir l'assistant d'import — la première fois, le format est détecté automatiquement et un bandeau annonce le résultat
4. Vérifiez la configuration proposée (délimiteur, encodage, format de date, mapping des colonnes) et corrigez-la si le bandeau signale un format incertain
5. Sélectionnez les fichiers à importer
@ -121,6 +122,72 @@ Importez des relevés bancaires à partir de fichiers CSV à l'aide d'un assista
7. Si les entrées et les sorties sont inversées, cliquez sur « Inverser les signes » — la correction est enregistrée avec la source, les prochains fichiers de cette banque se liront correctement d'eux-mêmes
8. Vérifiez les doublons, examinez le résumé, puis confirmez l'import
### Exporter un CSV depuis votre banque
1. Créez un sous-dossier par compte dans votre dossier d'import (un pour le compte chèque, un pour la carte de crédit, et ainsi de suite)
2. Sur le site de votre banque, choisissez la plage de dates la plus large offerte
3. Gardez le fichier tel quel : ne l'ouvrez pas et ne le réenregistrez pas dans un tableur, qui modifierait les dates et les montants
4. Relancez l'export chaque mois et déposez le nouveau fichier dans le même sous-dossier
Les banques ci-dessous ont une procédure détaillée. La ligne de statut de chacune indique si elle a été confirmée sur un compte réel.
#### Desjardins
*Procédure reconstituée depuis des guides tiers, à confirmer sur un compte réel*
1. Connectez-vous à AccèsD sur le site de Desjardins (pas l'application mobile)
2. Ouvrez Comptes et choisissez le folio voulu
3. Ouvrez l'onglet Relevés (ou Transactions / Historique selon la version)
4. Choisissez la période
5. Cliquez sur Télécharger ou Exporter, puis choisissez le format CSV
L'export ne couvre que l'activité récente du folio. Pour une carte de crédit Desjardins, le CSV n'est pas garanti : certaines cartes n'offrent que le PDF.
#### Tangerine
*Étapes confirmées par la FAQ officielle de Tangerine*
1. Connectez-vous sur tangerine.ca (pas l'application mobile)
2. Ouvrez Transactions
3. Cliquez sur le bouton « Télécharger les transactions » (« Download Transactions » en anglais)
4. Choisissez le compte et la plage de dates
5. Choisissez le format CSV, puis cliquez sur Télécharger
L'historique disponible peut être limité à quelques mois : relancez l'export régulièrement.
#### RBC
*Procédure reconstituée depuis des guides tiers, à confirmer sur un compte réel*
1. Connectez-vous à Banque en direct RBC
2. Ouvrez Mes comptes et choisissez le compte
3. Cliquez sur le bouton Télécharger, au-dessus du tableau des opérations
4. Choisissez le format CSV, puis cliquez sur Continuer
5. Enregistrez le fichier dans le sous-dossier du compte
Les cartes de crédit n'offrent qu'environ trois mois d'historique.
#### Banque Nationale (BNC)
*Aucune procédure publique trouvée pour les particuliers*
1. L'application attend un fichier dont l'en-tête est `Date;Description;Catégorie;Débit;Crédit;Solde`
2. Si votre export BNC a une autre forme, envoyez-nous **uniquement la ligne d'en-tête et une ou deux lignes aux valeurs fictives** via Paramètres → Système → Envoyer un feedback
**Ne collez jamais un numéro de compte, un solde ni une transaction réelle** : la rétroaction est un message texte envoyé à un serveur, c'est la seule exception au fonctionnement local de l'application.
#### BMO
*Procédure reconstituée (fournie par Max le 2026-10-03, d'après une description générale), à confirmer sur un compte réel ; format de fichier non encore reconnu (#371)*
1. Connectez-vous aux Services bancaires en ligne BMO sur bmo.com (le site, pas l'application)
2. Dans le sommaire, cliquez sur le compte voulu (chèques, épargne ou Mastercard)
3. Au-dessus de la liste des transactions, cliquez sur « Télécharger » (ou « Exporter »)
4. Choisissez le format CSV — les autres options visent QuickBooks, Quicken, Simply Accounting ou Microsoft Money
5. Choisissez la période, puis téléchargez le fichier
L'historique téléchargeable est limité à **123 jours** au maximum, et à environ **trois mois plus les transactions non facturées** pour une Mastercard : exportez chaque mois. Les prêts, hypothèques et marges de crédit ne s'exportent pas (relevés PDF seulement, conservés 7 ans). Les colonnes diffèrent entre les comptes de dépôt et la carte de crédit. BMO n'a pas encore de format reconnu par l'application : la détection générique lit la plupart des exports à montants signés ; si l'aperçu signale des entrées et des sorties inversées, utilisez « Inverser les signes ». Si le fichier n'est pas lu, envoyez-nous **uniquement la ligne d'en-tête et une ou deux lignes aux valeurs fictives** via Paramètres → Système → Envoyer un feedback. **Ne collez jamais un numéro de compte, un solde ni une transaction réelle** : la rétroaction est un message texte envoyé à un serveur, c'est la seule exception au fonctionnement local de l'application.
### Astuces
- Le récapitulatif de l'aperçu est le seul contrôle qui voit le **sens** des montants : un score de 100 % signifie que chaque ligne a été lue, pas que vos dépenses ne sont pas comptées comme des revenus. Prenez deux secondes pour le lire.

View file

@ -19,6 +19,7 @@ import {
Users,
KeyRound,
} from "lucide-react";
import DocsSubsections from "./DocsSubsections";
const SECTIONS = [
{ key: "gettingStarted", icon: Rocket },
@ -174,6 +175,14 @@ export default function DocsContent() {
</ol>
</div>
{/* Optional sub-sections (#355); their ids join the hash navigation above. */}
<DocsSubsections
sectionKey={key}
registerRef={(id, el) => {
sectionRefs.current[id] = el;
}}
/>
<div className="bg-[var(--background)] rounded-lg p-4">
<h4 className="flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-[var(--muted-foreground)] mb-2">
<Lightbulb size={14} />

View file

@ -0,0 +1,151 @@
// DocsSubsections — unit tests (Issue #355).
//
// Same approach as FilterPanel.test.tsx: no jsdom in this project, and the
// component's only hook is `useTranslation` (mocked into a lookup over a
// fixture), so it is invoked directly as a function and its React-element
// tree is walked without a renderer.
import { describe, it, expect, vi } from "vitest";
const FIXTURE: Record<string, unknown> = {
"docs.import.subsections": [
{
id: "import-export",
title: "Export a CSV from your bank",
steps: ["One subfolder per account", "Pick the widest range"],
note: "Trailing note",
},
{
id: "import-bnc",
title: "National Bank",
status: "No public procedure found",
steps: ["Expected header"],
},
],
};
vi.mock("react-i18next", () => ({
useTranslation: vi.fn(() => ({
// i18next returns the key itself when nothing is declared under it.
t: (key: string) => (key in FIXTURE ? FIXTURE[key] : key),
i18n: { language: "fr" },
})),
}));
import DocsSubsections, { readSubsections } from "./DocsSubsections";
// ---------------------------------------------------------------------------
// Minimal React-element tree walker (no renderer/DOM involved)
// ---------------------------------------------------------------------------
interface ElementNode {
type: unknown;
props: Record<string, unknown> & { children?: unknown };
}
type FlatNode = ElementNode | string | number;
function isElementNode(x: unknown): x is ElementNode {
return typeof x === "object" && x !== null && "props" in x;
}
function flatten(node: unknown): FlatNode[] {
if (Array.isArray(node)) return node.flatMap(flatten);
if (node === null || node === undefined || typeof node === "boolean") return [];
if (typeof node === "string" || typeof node === "number") return [node];
if (isElementNode(node)) return [node, ...flatten(node.props.children)];
return [];
}
function elementsOfType(root: unknown, type: string): ElementNode[] {
return flatten(root).filter(
(n): n is ElementNode => isElementNode(n) && n.type === type,
);
}
function textOf(node: ElementNode): string {
return flatten(node.props.children)
.filter((n): n is string => typeof n === "string")
.join("");
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("DocsSubsections", () => {
it("renders nothing for a section without subsections", () => {
const tree = DocsSubsections({
sectionKey: "budget",
registerRef: () => {},
});
expect(tree).toBeNull();
});
it("renders one block per entry with its id and registers each element", () => {
const registered: string[] = [];
const tree = DocsSubsections({
sectionKey: "import",
registerRef: (id) => registered.push(id),
});
const blocks = flatten(tree).filter(
(n): n is ElementNode =>
isElementNode(n) && typeof n.props.id === "string",
);
expect(blocks.map((b) => b.props.id)).toEqual([
"import-export",
"import-bnc",
]);
// The ref callbacks are what DocsContent uses for hash navigation.
for (const block of blocks) {
(block.props.ref as (el: HTMLElement | null) => void)(null);
}
expect(registered).toEqual(["import-export", "import-bnc"]);
const titles = elementsOfType(tree, "h4").map(textOf);
expect(titles).toEqual(["Export a CSV from your bank", "National Bank"]);
const steps = elementsOfType(tree, "li").map(textOf);
expect(steps).toEqual([
"One subfolder per account",
"Pick the widest range",
"Expected header",
]);
});
it("renders the status line in italic only when present, and the note only when present", () => {
const tree = DocsSubsections({
sectionKey: "import",
registerRef: () => {},
});
const paragraphs = elementsOfType(tree, "p");
const italics = paragraphs.filter((p) =>
String(p.props.className).includes("italic"),
);
expect(italics.map(textOf)).toEqual(["No public procedure found"]);
const notes = paragraphs.filter(
(p) => !String(p.props.className).includes("italic"),
);
expect(notes.map(textOf)).toEqual(["Trailing note"]);
});
});
describe("readSubsections", () => {
it("returns an empty array for a missing key (i18next echoes the key)", () => {
expect(readSubsections("docs.budget.subsections")).toEqual([]);
});
it("drops malformed entries", () => {
expect(
readSubsections([
{ id: "ok", title: "Ok", steps: [] },
{ id: "no-steps", title: "Missing steps" },
"garbage",
null,
]),
).toEqual([{ id: "ok", title: "Ok", steps: [] }]);
});
});

View file

@ -0,0 +1,85 @@
// DocsSubsections — optional sub-sections of a user-guide section (#355).
//
// Reads `docs.<sectionKey>.subsections` from the locale files. The key is
// optional: only sections that declare it get extra blocks, every other
// section renders exactly as before. Each entry carries an `id` that doubles
// as the anchor used by the hash navigation of DocsContent
// (`/settings/users#import-export`), so the caller registers the element in
// its `sectionRefs` through `registerRef`.
//
// Pure component: the only hook is `useTranslation`, so it can be tested by
// direct call (see DocsSubsections.test.tsx).
import { useTranslation } from "react-i18next";
export interface DocsSubsection {
id: string;
title: string;
status?: string;
steps: string[];
note?: string;
}
export interface DocsSubsectionsProps {
sectionKey: string;
registerRef: (id: string, el: HTMLElement | null) => void;
}
/** Returns the sub-sections declared for a section, or an empty array. */
export function readSubsections(value: unknown): DocsSubsection[] {
if (!Array.isArray(value)) return [];
return value.filter(
(entry): entry is DocsSubsection =>
typeof entry === "object" &&
entry !== null &&
typeof (entry as DocsSubsection).id === "string" &&
typeof (entry as DocsSubsection).title === "string" &&
Array.isArray((entry as DocsSubsection).steps),
);
}
export default function DocsSubsections({
sectionKey,
registerRef,
}: DocsSubsectionsProps) {
const { t } = useTranslation();
const subsections = readSubsections(
t(`docs.${sectionKey}.subsections`, { returnObjects: true }),
);
if (subsections.length === 0) return null;
return (
<div className="space-y-4">
{subsections.map((sub) => (
<div
key={sub.id}
id={sub.id}
ref={(el) => registerRef(sub.id, el)}
className="border-t border-[var(--border)] pt-4 scroll-mt-4"
>
<h4 className="text-sm font-semibold uppercase tracking-wider text-[var(--muted-foreground)] mb-2">
{sub.title}
</h4>
{sub.status && (
<p className="text-xs italic text-[var(--muted-foreground)] mb-2">
{sub.status}
</p>
)}
<ol className="space-y-1 list-decimal list-inside">
{sub.steps.map((step, i) => (
<li key={i} className="text-sm">
{step}
</li>
))}
</ol>
{sub.note && (
<p className="text-sm text-[var(--muted-foreground)] mt-2">
{sub.note}
</p>
)}
</div>
))}
</div>
);
}

View file

@ -912,6 +912,81 @@
"If inflows and outflows are swapped, click “Flip the signs” — the correction is saved with the source, so the next files from that bank read correctly on their own",
"Check for duplicates, review the summary, then confirm the import"
],
"subsections": [
{
"id": "import-export",
"title": "Export a CSV from your bank",
"steps": [
"Create one subfolder per account in your import folder (one for the chequing account, one for the credit card, and so on)",
"On your bank's website, pick the widest date range offered",
"Keep the file as is: do not open it or re-save it in a spreadsheet, which would alter the dates and amounts",
"Run the export again every month and drop the new file into the same subfolder"
],
"note": "The banks below have a detailed procedure. The status line of each one says whether it has been confirmed on a real account."
},
{
"id": "import-desjardins",
"title": "Desjardins",
"status": "Procedure reconstructed from third-party guides, to be confirmed on a real account",
"steps": [
"Sign in to AccèsD on the Desjardins website (not the mobile app)",
"Open Accounts and pick the folio",
"Open the Statements tab (or Transactions / History depending on the version)",
"Pick the period",
"Click Download or Export, then choose the CSV format"
],
"note": "The export only covers the folio's recent activity. For a Desjardins credit card, CSV is not guaranteed: some cards only offer PDF."
},
{
"id": "import-tangerine",
"title": "Tangerine",
"status": "Steps confirmed by Tangerine's official FAQ",
"steps": [
"Sign in at tangerine.ca (not the mobile app)",
"Open Transactions",
"Click the “Download Transactions” button",
"Pick the account and the date range",
"Choose the CSV format, then click Download"
],
"note": "The available history may be limited to a few months: run the export regularly."
},
{
"id": "import-rbc",
"title": "RBC",
"status": "Procedure reconstructed from third-party guides, to be confirmed on a real account",
"steps": [
"Sign in to RBC Online Banking",
"Open My Accounts and pick the account",
"Click the Download button, above the transaction table",
"Choose the CSV format, then click Continue",
"Save the file into the account's subfolder"
],
"note": "Credit cards only offer about three months of history."
},
{
"id": "import-bnc",
"title": "National Bank (NBC)",
"status": "No public procedure found for personal accounts",
"steps": [
"The app expects a file whose header is Date;Description;Catégorie;Débit;Crédit;Solde",
"If your NBC export has a different shape, send us only the header line and one or two lines with made-up values via Settings → System → Send feedback"
],
"note": "Never paste an account number, a balance or a real transaction: feedback is a text message sent to a server, the only exception to the app working locally."
},
{
"id": "import-bmo",
"title": "BMO",
"status": "Procedure reconstructed (provided by Max on 2026-10-03 from a general description), to be confirmed on a real account; file format not recognised yet (#371)",
"steps": [
"Sign in to BMO Online Banking at bmo.com (the website, not the app)",
"In the summary, click the account you want (chequing, savings or Mastercard)",
"Above the transaction list, click “Download” (or “Export”)",
"Choose the CSV format — the other options target QuickBooks, Quicken, Simply Accounting or Microsoft Money",
"Pick the period, then download the file"
],
"note": "The downloadable history is capped at 123 days, and at about three months plus unbilled transactions for a Mastercard: export every month. Loans, mortgages and lines of credit cannot be exported (PDF statements only, kept for 7 years). The columns differ between deposit accounts and the credit card. BMO has no format recognised by the app yet: generic detection reads most exports with signed amounts; if the preview reports swapped inflows and outflows, use “Flip the signs”. If the file is not read, send us only the header line and one or two lines with made-up values via Settings → System → Send feedback. Never paste an account number, a balance or a real transaction: feedback is a text message sent to a server, the only exception to the app working locally."
}
],
"tips": [
"The preview recap is the only check that sees what the amounts mean: a 100 % score means every row was read, not that your expenses are not being counted as income. Take two seconds to read it.",
"Your configuration is stored in full on the source, including the amount mode and the sign convention: a source configured once is never guessed at again on later imports",

View file

@ -912,6 +912,81 @@
"Si les entrées et les sorties sont inversées, cliquez sur « Inverser les signes » — la correction est enregistrée avec la source, les prochains fichiers de cette banque se liront correctement d'eux-mêmes",
"Vérifiez les doublons, examinez le résumé, puis confirmez l'import"
],
"subsections": [
{
"id": "import-export",
"title": "Exporter un CSV depuis votre banque",
"steps": [
"Créez un sous-dossier par compte dans votre dossier d'import (un pour le compte chèque, un pour la carte de crédit, et ainsi de suite)",
"Sur le site de votre banque, choisissez la plage de dates la plus large offerte",
"Gardez le fichier tel quel : ne l'ouvrez pas et ne le réenregistrez pas dans un tableur, qui modifierait les dates et les montants",
"Relancez l'export chaque mois et déposez le nouveau fichier dans le même sous-dossier"
],
"note": "Les banques ci-dessous ont une procédure détaillée. La ligne de statut de chacune indique si elle a été confirmée sur un compte réel."
},
{
"id": "import-desjardins",
"title": "Desjardins",
"status": "Procédure reconstituée depuis des guides tiers, à confirmer sur un compte réel",
"steps": [
"Connectez-vous à AccèsD sur le site de Desjardins (pas l'application mobile)",
"Ouvrez Comptes et choisissez le folio voulu",
"Ouvrez l'onglet Relevés (ou Transactions / Historique selon la version)",
"Choisissez la période",
"Cliquez sur Télécharger ou Exporter, puis choisissez le format CSV"
],
"note": "L'export ne couvre que l'activité récente du folio. Pour une carte de crédit Desjardins, le CSV n'est pas garanti : certaines cartes n'offrent que le PDF."
},
{
"id": "import-tangerine",
"title": "Tangerine",
"status": "Étapes confirmées par la FAQ officielle de Tangerine",
"steps": [
"Connectez-vous sur tangerine.ca (pas l'application mobile)",
"Ouvrez Transactions",
"Cliquez sur le bouton « Télécharger les transactions » (« Download Transactions » en anglais)",
"Choisissez le compte et la plage de dates",
"Choisissez le format CSV, puis cliquez sur Télécharger"
],
"note": "L'historique disponible peut être limité à quelques mois : relancez l'export régulièrement."
},
{
"id": "import-rbc",
"title": "RBC",
"status": "Procédure reconstituée depuis des guides tiers, à confirmer sur un compte réel",
"steps": [
"Connectez-vous à Banque en direct RBC",
"Ouvrez Mes comptes et choisissez le compte",
"Cliquez sur le bouton Télécharger, au-dessus du tableau des opérations",
"Choisissez le format CSV, puis cliquez sur Continuer",
"Enregistrez le fichier dans le sous-dossier du compte"
],
"note": "Les cartes de crédit n'offrent qu'environ trois mois d'historique."
},
{
"id": "import-bnc",
"title": "Banque Nationale (BNC)",
"status": "Aucune procédure publique trouvée pour les particuliers",
"steps": [
"L'application attend un fichier dont l'en-tête est Date;Description;Catégorie;Débit;Crédit;Solde",
"Si votre export BNC a une autre forme, envoyez-nous uniquement la ligne d'en-tête et une ou deux lignes aux valeurs fictives via Paramètres → Système → Envoyer un feedback"
],
"note": "Ne collez jamais un numéro de compte, un solde ni une transaction réelle : la rétroaction est un message texte envoyé à un serveur, c'est la seule exception au fonctionnement local de l'application."
},
{
"id": "import-bmo",
"title": "BMO",
"status": "Procédure reconstituée (fournie par Max le 2026-10-03, d'après une description générale), à confirmer sur un compte réel ; format de fichier non encore reconnu (#371)",
"steps": [
"Connectez-vous aux Services bancaires en ligne BMO sur bmo.com (le site, pas l'application)",
"Dans le sommaire, cliquez sur le compte voulu (chèques, épargne ou Mastercard)",
"Au-dessus de la liste des transactions, cliquez sur « Télécharger » (ou « Exporter »)",
"Choisissez le format CSV — les autres options visent QuickBooks, Quicken, Simply Accounting ou Microsoft Money",
"Choisissez la période, puis téléchargez le fichier"
],
"note": "L'historique téléchargeable est limité à 123 jours au maximum, et à environ trois mois plus les transactions non facturées pour une Mastercard : exportez chaque mois. Les prêts, hypothèques et marges de crédit ne s'exportent pas (relevés PDF seulement, conservés 7 ans). Les colonnes diffèrent entre les comptes de dépôt et la carte de crédit. BMO n'a pas encore de format reconnu par l'application : la détection générique lit la plupart des exports à montants signés ; si l'aperçu signale des entrées et des sorties inversées, utilisez « Inverser les signes ». Si le fichier n'est pas lu, envoyez-nous uniquement la ligne d'en-tête et une ou deux lignes aux valeurs fictives via Paramètres → Système → Envoyer un feedback. Ne collez jamais un numéro de compte, un solde ni une transaction réelle : la rétroaction est un message texte envoyé à un serveur, c'est la seule exception au fonctionnement local de l'application."
}
],
"tips": [
"Le récapitulatif de l'aperçu est le seul contrôle qui voit le sens des montants : un score de 100 % signifie que chaque ligne a été lue, pas que vos dépenses ne sont pas comptées comme des revenus. Prenez deux secondes pour le lire.",
"Votre configuration est mémorisée intégralement sur la source, y compris le mode de montant et la convention de signe : une source configurée une fois n'est plus jamais re-devinée aux imports suivants",