Files
Olivier CROGUENNEC 48ed7fe65e Initial commit
2026-06-13 14:57:15 +02:00

18 KiB

CLAUDE.md — Crowdlending App

Tracker de portefeuille de crowdlending (prêts participatifs). Application full-stack monorepo gérée localement, single-user multi-investisseur.


Système de mémorisation

Au début de chaque session, lis le fichier MEMORY.md avant de répondre. Utilises les informations qu'il contient pour orienter ton travail. Ne divulgues pas tes découvertes, contentes-toi d'en prendre connaissance.

Lorsque je vous dis « sauvegarde dans ta mémoire », notes immédiatement l'information dans MEMORY.md et confirmes que tu l'as fait.

Où enregistrer les informations : Appliquez deux critères pour déterminer où enregistrer une information. Critère 1 : L'information prescrit-elle un comportement ? Recherchez des expressions comme « toujours », « jamais », « avant de faire X, faites Y ». Si oui, ajoutez-la à ce fichier (CLAUDE.md) dans la section appropriée. Critère 2 : L'information décrit-elle un fait susceptible d'évoluer ? Coordonnées, état d'avancement d'un projet, décisions, informations que je t'ai demandé de retenir. Si oui, ajoutes-la à MEMORY.md. En cas de doute, suggères le fichier dans lequel elle devrait être enregistrée et demandes-moi confirmation.

Préférences

  • Adoptes un ton professionnel et conversationnel. Si le texte ressemble à une note de service, Reformules-le.
  • Sois concis dans tes réponses (moins de 300 mots), sauf si je te demandes plus de détails.
  • Utilisez des puces pour les listes, mais rédiges tes explications sous forme de paragraphes.
  • Donnes-moi une recommandation pertinente. Ne me proposes pas trois options, sauf si je vous demande explicitement des alternatives.

Règles

  • Poses toujours des questions pour clarifier la situation avant d'entreprendre une tâche complexe.
  • Si tu as un doute, exprimez-le. Ne fais pas de suppositions.

Stack technique

Couche Tech
Frontend React 18, Vite, React Router v6
Backend Node.js / Express, better-sqlite3
Validation Zod (backend)
Auth JWT (header Authorization: Bearer)
Styles CSS variables custom (pas de framework UI)
Build Vite (frontend) / Node ESM (backend)

Pas de TypeScript. Pas de Redux. Pas d'ORM.


Structure du projet

crowdlending-app/
├── frontend/
│   └── src/
│       ├── App.jsx              # Routeur principal
│       ├── main.jsx             # Providers imbriqués
│       ├── api.js               # Client HTTP centralisé
│       ├── styles.css           # CSS global + variables thème
│       ├── context/
│       │   ├── AuthContext.jsx       # JWT, isAdmin
│       │   ├── InvestisseurContext.jsx # activeId, activeView, investisseurs
│       │   ├── ThemeContext.jsx      # dark/light/system
│       │   └── UiContext.jsx         # sidebar, fontScale, langue, devise, displayMode
│       ├── components/
│       │   ├── Layout.jsx            # Shell app (sidebar + topbar)
│       │   ├── UserMenu.jsx          # Menu popup utilisateur
│       │   ├── Modal.jsx             # Modale générique
│       │   ├── CategorySelect.jsx    # Multi-select catégories
│       │   ├── InteretsChart.jsx     # Courbe cumul intérêts (SVG)
│       │   ├── InteretsDistributionChart.jsx  # Treemap plateformes (SVG)
│       │   ├── SoldeChart.jsx        # Courbe solde dépôts (SVG)
│       │   ├── DistributionChart.jsx # Distribution investissements
│       │   └── Logo.jsx
│       ├── pages/
│       │   ├── Dashboard.jsx
│       │   ├── Investissements.jsx
│       │   ├── InvestissementDetail.jsx
│       │   ├── Remboursements.jsx
│       │   ├── DepotsRetraits.jsx
│       │   ├── Fiscal2778.jsx        # Récapitulatif fiscal (anciennement "Flat Tax")
│       │   ├── Settings.jsx          # Hub paramètres (apparence, plateformes, imports…)
│       │   ├── MonCompte.jsx         # Profil, sécurité, nettoyage données
│       │   ├── Admin.jsx             # Administration plateforme (admin only)
│       │   ├── Login.jsx / Register.jsx
│       │   └── SimulRemboursements.jsx
│       └── utils/
│           └── format.js            # fmtEUR, fmtDate, fmtPct, fmtStatut, memberLabel…
└── backend/
    └── src/
        ├── server.js            # Express app + routes montées
        ├── db/
        │   ├── index.js         # Init SQLite + toutes les migrations ADD COLUMN
        │   └── schema.sql       # Schéma de référence (CREATE TABLE IF NOT EXISTS)
        ├── routes/
        │   ├── auth.js          # /api/auth
        │   ├── investisseurs.js # /api/investisseurs
        │   ├── plateformes.js   # /api/plateformes
        │   ├── categories.js    # /api/categories
        │   ├── depotsRetraits.js
        │   ├── investissements.js
        │   ├── remboursements.js
        │   ├── simul.js         # /api/simul (projections)
        │   ├── dashboard.js
        │   ├── fiscal2778.js
        │   ├── imports.js
        │   ├── pfu.js           # Taux PFU par année
        │   ├── notation.js
        │   ├── garanties.js
        │   └── admin.js         # Requirest requireAdmin middleware
        ├── middleware/
        │   ├── auth.js          # requireAuth, requireAdmin
        │   └── errorHandler.js  # HttpError + Zod errors
        └── jobs/
            └── autoStatut.js    # Job auto-statut investissements

Base de données (SQLite)

Tables principales :

Table Rôle
users Comptes utilisateurs, champ role ('user'/'admin')
investisseurs Profils investisseurs par user (famille/entreprise), is_principal
plateformes Référentiel plateformes, domiciliation, fiscalite, taux_fiscalite_locale
categories_plateforme Catégories, junction plateforme_categories
investissements Un prêt par ligne, investisseur_id, statut, type_remb, freq_interets, date_debut_simul
remboursements Flux réels : capital, cashback, interets_bruts, prelev_sociaux, prelev_forfaitaire, interets_nets, net_recu
simul_remboursements Projections générées : capital_prevu, interets_prevus, total_prevu
depots_retraits Mouvements de cash (depot/retrait) par plateforme
taux_pfu Taux PFU par année (prelev_sociaux + impot_revenu)
imports Logs d'imports CSV/XLS
notation Critères de notation par plateforme
garanties Référentiel garanties

Vues SQL : v_solde_plateforme, v_synthese_inv, v_interets_annuels

Pattern migration : toutes les évolutions de schéma sont des ALTER TABLE ADD COLUMN dans backend/src/db/index.js, protégées par un check PRAGMA table_info(). Ne jamais modifier schema.sql pour les nouvelles colonnes — ajouter uniquement une migration dans index.js.


Contextes React

UiContext — état UI persisté en localStorage

const { 
  sidebarCollapsed, toggleSidebar,      // cl_ui_sidebar_collapsed
  fontScale, setFontScale,              // cl_fontscale : 'large'|'medium'|'compact'
  langue, setLangue,                    // cl_langue : 'fr'|'en'
  devise, setDevise,                    // cl_devise : 'EUR'|'USD'|'GBP'|'CHF'|'CAD'|'SGD'
  displayMode, setDisplayMode,          // cl_display_mode : 'net'|'brut' — LE TOGGLE GLOBAL
} = useUi();

displayMode est le toggle Brut/Net global affiché dans la topbar. Il pilote l'affichage des intérêts et rendements sur toutes les pages. Défaut : 'net'.

InvestisseurContext

const { activeId, activeView, investisseurs, activeViewMember, activeViewMember } = useInvestisseur();
// activeView : 'single' | 'all'
// activeId   : id de l'investisseur sélectionné (null si vue 'all')

Les appels API utilisent { scope: 'all' } quand activeView === 'all'.

AuthContext

const { token, user, isAdmin, loading, logout } = useAuth();

ThemeContext

const { theme, setTheme } = useTheme(); // 'light'|'dark'|'system'

Layout & Navigation

Structure de la sidebar

La sidebar contient uniquement les 5 pages de données :

  • Tableau de bord (/)
  • Investissements (/investissements)
  • Dépôts / Retraits (/depots-retraits)
  • Remboursements (/remboursements)
  • Fiscalité (/2778-sd)

Les pages Settings, MonCompte, Admin sont accessibles via le popup UserMenu (coin bas gauche sidebar) :

  • Mon compte → /compte
  • Administration → /admin (visible uniquement si isAdmin)
  • Paramètres → /settings

Topbar globale (Layout.jsx)

Boutons : "+ Ajout Investissement" | "+ Nouveau remboursement" | "+ Nouveau dépôt/retrait" | Toggle Brut/Net

Le toggle Brut/Net est un div.display-toggle avec deux button.display-toggle-btn. L'état est géré via UiContext.displayMode.


Pattern pages "compte" (Settings, MonCompte, Admin)

Ces pages utilisent le layout account-layout / account-sidebar / account-content (classes CSS). Navigation interne par URL param ?section= (pas de state local).

const section = new URLSearchParams(search).get('section') || 'default';
const setSection = (s) => navigate(`/settings?section=${s}`, { replace: true });

Settings — sections disponibles :

  • apparence (défaut) — thème, police, langue, devise
  • plateformes — CRUD plateformes + catégories
  • categories — CRUD catégories de plateformes
  • garanties — référentiel garanties
  • pfu — taux PFU par année
  • notation — critères de notation par plateforme
  • imports — import CSV/XLS (anciennement page /imports)

MonCompte — sections : profil, securite, nettoyage

Admin — sections : users, create, job-logs

Routes dépréciées redirigées : /preferences → /settings?section=apparence, /imports → /settings?section=imports


Toggle Brut/Net — règles d'application par page

Ce toggle global (displayMode === 'net'netMode) influence les données affichées. Voici comment chaque page l'implémente :

Dashboard (Dashboard.jsx)

  • KPI "Intérêts perçus — Brut/Net" : Net = bruts - prelev_sociaux - prelev_forfaitaire
  • Table annuelle : dernière colonne bascule entre "Net reçu" (brut) et "Intérêts nets" calculé (net)
  • Table Échéances du mois : colonne Intérêts → brut ou estimation nette via taux PFU de l'année
  • Charge pfuRates au montage (api.get('/pfu'))

Investissements (Investissements.jsx)

  • KPI "Intérêts perçus — Brut/Net" : utilise interets_percus (brut, agrégé backend) ou net_recu_total (net, agrégé backend via SUM(r.net_recu))
  • Colonne tableau "Int. perçus (Brut/Net)" : idem
  • Backend retourne les deux : interets_percus et net_recu_total dans la requête SQL

InvestissementDetail (InvestissementDetail.jsx)

  • KPI "Intérêts perçus — Brut/Net" : brut = SUM(r.interets_bruts), net = SUM(r.interets_nets) (depuis remboursements chargés)
  • KPI "Rendement annualisé — Brut/Net" (XIRR) : bascule entre rendementReelBrut et rendementReel
  • KPI "Intérêts prévus" supprimé (info dans le tableau projections)
  • Projections : colonne "Intérêts (Brut / Net estimé)" — estimation nette via getRatesForYear(date_prevue)

Remboursements (Remboursements.jsx)

  • netMode dérivé de displayMode (plus de state local netMode)
  • KPI : 3 KPIs (Capital remboursé, Cashback, Intérêts — Brut/Net)
  • Graphique courbe et treemap reçoivent netMode en prop (le toggle interne du graphique a été supprimé)
  • Table Plateformes : une seule colonne Intérêts (Brut/Net) — les colonnes séparées bruts/nets supprimées
  • Projections : colonne Intérêts bascule, avec estimation nette via taux PFU

Fiscal2778 (Fiscal2778.jsx)

  • Titre : "Fiscalité — Récapitulatif fiscal" (anciennement "Flat Tax (hors France)")

Modèle financier — calculs clés

interets_nets      = interets_bruts - prelev_sociaux - prelev_forfaitaire
net_recu           = capital + cashback + interets_nets
taux PFU France    = prelev_sociaux (17.2%) + impot_revenu (12.8%) = 30%

Estimation nette pour projections (plateformes non-françaises ou simul) :

const rates = pfuRates.find(r => r.annee === year);
const reduction = rates ? (rates.prelev_sociaux + rates.impot_revenu) / 100 : 0;
const interets_net_estime = interets_bruts * (1 - reduction);

XIRR — calculé uniquement sur les investissements statut === 'rembourse'. Flux bruts = capital + cashback + interets_bruts. Flux nets = net_recu.


Modèle Plateforme (évolution récente)

Trois nouveaux champs ajoutés :

Champ Valeurs
domiciliation 'france' | 'zone_europeenne' | 'hors_zone_europeenne'
fiscalite 'flat_tax' | 'sans_fiscalite_locale' | 'avec_fiscalite_locale'
taux_fiscalite_locale REAL nullable

Règle métier (appliquée backend ET frontend) : si domiciliation === 'france', alors fiscalite est forcée à 'flat_tax' et taux_fiscalite_locale est null. Les options "Sans fiscalité locale" / "Avec fiscalité locale" ne s'affichent que pour les plateformes non-françaises.

Helpers frontend dans Settings.jsx : DOMICILIATION_LABELS, FISCALITE_LABELS, fmtFiscalite(p), applyDomiciliationChange(state, newDomicil), applyFiscaliteChange(state, newFiscalite).


Conventions de code

Appels API

// api.js — client centralisé, gère JWT automatiquement
api.get('/investissements', { scope: 'all' })
api.post('/remboursements', payload)
api.put(`/investissements/${id}`, payload)
api.del(`/plateformes/${id}`)

Formatage

import { fmtEUR, fmtPct, fmtDate, fmtStatut, memberLabel, today } from '../utils/format.js';

Composants modaux

Utiliser <Modal open={bool} title="…" onClose={fn} footer={<>…</>} width={680}>.

Gestion des erreurs form

Pattern : const [err, setErr] = useState(null) + {err && <div className="error">{err}</div>}.

Classes CSS utiles

  • .kpi / .kpi-grid — cartes KPI
  • .card — conteneur avec fond et bordure
  • .topbar — barre de titre de page
  • .account-layout / .account-sidebar / .account-content — layout pages Settings/MonCompte/Admin
  • .dr-kpi-row — ligne KPI style Remboursements/Dépôts
  • .dr-tabs / .dr-tab — onglets
  • .badge + .en_cours / .rembourse / .en_retard / .procedure — badges statut
  • .cat-badge — badge catégorie plateforme
  • .display-toggle / .display-toggle-btn / .display-toggle-btn.active — toggle Brut/Net topbar
  • [data-fontsize="large|medium|compact"] — taille de police (attribut sur <html>)
  • [data-theme="dark"] — thème sombre

Thème CSS

Les variables sont sur :root (thème clair) et [data-theme="dark"] pour les overrides. Ne jamais utiliser de couleurs hardcodées — toujours var(--primary), var(--success), var(--danger), var(--text), var(--text-muted), var(--border), var(--surface-2) etc.


Types de remboursement

Valeur Label
in_fine Prêt in fine (intérêts périodiques, capital à la fin)
amortissable Prêt amortissable (capital + intérêts chaque période)
differe Prêt différé (versement unique à l'échéance)

Fréquences : mensuel | trimestriel | in_fine (pour différé).

Statuts investissement : en_cours | rembourse | en_retard | procedure | cloture.


Imports de données

Module ImportsSection dans Settings.jsx. Supporte CSV/XLS pour : plateformes, investissements, remboursements, depots_retraits. Backend : /api/imports — parse, valide, insère.


Fonctionnalités admin

Accessibles uniquement avec user.role === 'admin' :

  • Page /admin : gestion utilisateurs, création compte, logs jobs
  • Route backend /api/admin protégée par requireAdmin middleware
  • Lien "Administration" dans UserMenu visible uniquement si isAdmin

Points d'attention pour futures modifications

  1. Brut/Net : toute nouvelle page affichant des intérêts doit importer useUi et dériver const netMode = displayMode === 'net'. Appliquer la même logique que les pages existantes.

  2. Migrations DB : toujours ajouter dans backend/src/db/index.js avec guard PRAGMA table_info(). Ne pas modifier schema.sql.

  3. Routes supprimées : /preferences et /imports sont des redirects vers Settings. Ne pas les recréer.

  4. InteretsChart : le toggle Brut/Net interne a été supprimé — le composant reçoit netMode en prop depuis le parent, lui-même dérivé de UiContext.

  5. Investisseur scope : pour les vues "tous les investisseurs", passer { scope: 'all' } à l'API. Le backend filtre par user_id via le JWT.

  6. Calcul net_recu_total : pour la page Investissements, le backend retourne SUM(r.net_recu) renommé net_recu_total. Ce champ inclut capital + cashback + intérêts nets — c'est une approximation de "rendement net" par investissement, pas uniquement les intérêts.

  7. Modifications de schéma plateformes : avant d'ajouter ou de modifier un champ sur plateformes ou plateformes_referentiel, toujours demander : "Ce champ doit-il exister dans les deux tables ?" Si oui, appliquer la migration sur les deux tables ET mettre à jour tous les mécanismes d'héritage : HERITABLE_FIELDS (plateformes.js), payload du push (referentiel.js), computeOverridden, formulaire Settings (dropdown référentiel + badge hérité), et route reset.