Initial commit

This commit is contained in:
Olivier CROGUENNEC
2026-06-13 14:57:15 +02:00
commit 48ed7fe65e
209 changed files with 49979 additions and 0 deletions
+378
View File
@@ -0,0 +1,378 @@
# 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
```js
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`
```js
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`
```js
const { token, user, isAdmin, loading, logout } = useAuth();
```
### `ThemeContext`
```js
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).
```jsx
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) :
```js
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
```js
// 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
```js
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`.