Initial commit
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
# MEMORY.md — Crowdlending Tracker
|
||||
*Dernière mise à jour: 2026-05-09 (session 6)*
|
||||
|
||||
---
|
||||
|
||||
## Résumé du projet
|
||||
|
||||
Application web **mono-utilisateur multi-investisseur** de suivi de crowdlending (prêts participatifs). Saisie manuelle + import Excel. Usage personnel/familial, non déployé publiquement.
|
||||
|
||||
- **Backend** : Node.js / Express + SQLite (better-sqlite3), port 4000
|
||||
- **Frontend** : React 18 + Vite, port 5173 (dev) / 8080 (Docker)
|
||||
- **Auth** : JWT 7j, header `Authorization: Bearer`
|
||||
- **DB** : `backend/data/crowdlending.db` (WAL mode)
|
||||
- **Docker** : `docker compose up -d --build` → frontend :8080, backend :4000
|
||||
|
||||
---
|
||||
|
||||
## État des migrations DB (dernière colonne ajoutée)
|
||||
|
||||
Les migrations sont dans `backend/src/db/index.js` (pattern `PRAGMA table_info()` + `ALTER TABLE ADD COLUMN`). Ne jamais toucher `schema.sql` pour les évolutions.
|
||||
|
||||
| Migration | Colonne / opération |
|
||||
|---|---|
|
||||
| investisseurs | `prenom TEXT`, `type TEXT` (famille/entreprise), `is_principal INTEGER` |
|
||||
| remboursements | `cashback REAL`, `interets_nets REAL` (suppression `autres_taxes`) |
|
||||
| investissements | rename `date_debut` → `date_premiere_echeance`, `date_echeance` → `date_cible`, `freq_interets TEXT`, `date_debut_simul TEXT` |
|
||||
| users | `role TEXT` ('user'/'admin') |
|
||||
| plateformes | `domiciliation TEXT`, `fiscalite TEXT`, `taux_fiscalite_locale REAL` |
|
||||
| investissements | `categorie_id INTEGER` (FK → categories_plateforme, ON DELETE SET NULL) |
|
||||
| remboursements | **Recréation de table** : `investissement_id` rendu nullable, ajout `bonus_plateforme_id`, `bonus_investisseur_id`, `type TEXT DEFAULT 'normal'` |
|
||||
| plateformes | `methode_remboursement TEXT NOT NULL DEFAULT 'portefeuille'` |
|
||||
| remboursements | `methode_remboursement TEXT NOT NULL DEFAULT 'portefeuille'` |
|
||||
| depots_retraits | `remboursement_id INTEGER` (lien vers le remboursement source pour les retraits auto) |
|
||||
|
||||
---
|
||||
|
||||
## Fonctionnalités clés actuelles
|
||||
|
||||
### Toggle Brut/Net global
|
||||
- Géré via `UiContext.displayMode` ('net' | 'brut'), persisté localStorage `cl_display_mode`
|
||||
- Affiché dans la **topbar** (`Layout.jsx`) — widget `.display-toggle`
|
||||
- Appliqué sur : Dashboard, Investissements, InvestissementDetail, Remboursements
|
||||
- **Règle** : toute nouvelle page avec intérêts → `const netMode = displayMode === 'net'`
|
||||
- Le toggle interne de `InteretsChart` a été **supprimé** — reçoit `netMode` en prop
|
||||
|
||||
### Modèle fiscal
|
||||
```
|
||||
interets_nets = interets_bruts - prelev_sociaux - prelev_forfaitaire
|
||||
net_recu = capital + cashback + interets_nets
|
||||
PFU France = 30% (17.2% PS + 12.8% IR)
|
||||
```
|
||||
Estimation nette projections : `interets_bruts * (1 - (prelev_sociaux + impot_revenu) / 100)`
|
||||
|
||||
### Modèle plateforme (évolution récente)
|
||||
- `domiciliation` : `france` | `zone_europeenne` | `hors_zone_europeenne`
|
||||
- `fiscalite` : `flat_tax` | `sans_fiscalite_locale` | `avec_fiscalite_locale`
|
||||
- `methode_remboursement` : `portefeuille` | `compte_courant` | `choix_investisseur`
|
||||
- `portefeuille` (défaut) : remboursement sur le porte-monnaie de la plateforme (méthode fermée)
|
||||
- `compte_courant` : remboursement sur le compte courant de l'investisseur (méthode fermée)
|
||||
- `choix_investisseur` : l'investisseur choisit sur la plateforme (méthode ouverte)
|
||||
- Règle : si `domiciliation === 'france'` → `fiscalite` forcé à `flat_tax`, `taux_fiscalite_locale` null
|
||||
- Helpers dans `Settings.jsx` : `applyDomiciliationChange`, `applyFiscaliteChange`, `fmtFiscalite`, `METHODE_REMB_LABELS`
|
||||
|
||||
### Navigation
|
||||
- Sidebar : 5 pages data uniquement (Dashboard, Investissements, Dépôts/Retraits, Remboursements, Fiscalité)
|
||||
- Settings / MonCompte / Admin → **UserMenu popup** (coin bas gauche)
|
||||
- Pages "compte" : layout `account-layout`, navigation par `?section=` URL param
|
||||
- Routes dépréciées redirigées : `/preferences → /settings?section=apparence`, `/imports → /settings?section=imports`
|
||||
|
||||
### Investisseur scope
|
||||
- `activeView` : `'single'` | `'all'`
|
||||
- Vue "tous" → passer `{ scope: 'all' }` à l'API
|
||||
- Backend filtre toujours par `user_id` via JWT
|
||||
|
||||
---
|
||||
|
||||
## Patterns à respecter
|
||||
|
||||
### API calls
|
||||
```js
|
||||
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'
|
||||
```
|
||||
|
||||
### Erreurs form
|
||||
```js
|
||||
const [err, setErr] = useState(null)
|
||||
// {err && <div className="error">{err}</div>}
|
||||
```
|
||||
|
||||
### Modales
|
||||
```jsx
|
||||
<Modal open={bool} title="…" onClose={fn} footer={<>…</>} width={680}>
|
||||
```
|
||||
|
||||
### Pas de couleurs hardcodées
|
||||
Toujours `var(--primary)`, `var(--success)`, `var(--danger)`, `var(--text)`, `var(--text-muted)`, `var(--border)`, `var(--surface-2)`, etc.
|
||||
|
||||
---
|
||||
|
||||
## Types / énumérations de référence
|
||||
|
||||
| Entité | Valeurs |
|
||||
|---|---|
|
||||
| `investissements.statut` | `en_cours` \| `rembourse` \| `en_retard` \| `procedure` \| `cloture` |
|
||||
| `investissements.type_remb` | `in_fine` \| `amortissable` \| `differe` |
|
||||
| `investissements.freq_interets` | `mensuel` \| `trimestriel` \| `in_fine` |
|
||||
| `remboursements.type` | `normal` \| `bonus_parrainage` \| `bonus_plateforme` |
|
||||
| `remboursements.statut` | `paye` \| `retard` \| `partiel` \| `impaye` |
|
||||
| `depots_retraits.type` | `depot` \| `retrait` |
|
||||
| `investisseurs.type` | `famille` \| `entreprise` |
|
||||
| `users.role` | `user` \| `admin` |
|
||||
|
||||
---
|
||||
|
||||
## Sections Settings
|
||||
|
||||
| Section (`?section=`) | Contenu |
|
||||
|---|---|
|
||||
| `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 notation par plateforme |
|
||||
| `imports` | import CSV/XLS |
|
||||
|
||||
---
|
||||
|
||||
## Endpoints API principaux
|
||||
|
||||
```
|
||||
POST /api/auth/register | login | me
|
||||
GET/POST/PUT/DELETE /api/investisseurs
|
||||
GET/POST/PUT/DELETE /api/plateformes
|
||||
GET/POST/PUT/DELETE /api/depots-retraits
|
||||
GET/POST/PUT/DELETE /api/investissements
|
||||
GET/POST/PUT/DELETE /api/remboursements
|
||||
GET/POST/DELETE /api/simul
|
||||
POST /api/simul/generate
|
||||
GET /api/dashboard
|
||||
GET /api/fiscal-2778?annee=YYYY
|
||||
GET /api/fiscal-2778/export?annee=YYYY
|
||||
GET/POST /api/pfu
|
||||
GET/POST/PUT/DELETE /api/notation
|
||||
GET/POST/PUT/DELETE /api/garanties
|
||||
GET/POST/PUT/DELETE /api/categories
|
||||
POST /api/imports/preview | apply
|
||||
GET /api/imports/history
|
||||
GET/POST/PUT/DELETE /api/admin/users (requireAdmin)
|
||||
```
|
||||
|
||||
Headers requis (hors `/auth/*`) : `Authorization: Bearer <jwt>` + `X-Investisseur-Id: <id>` pour routes scopées.
|
||||
|
||||
---
|
||||
|
||||
## Démarrage local
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
cd backend && npm install && npm run dev # :4000
|
||||
|
||||
# Frontend
|
||||
cd frontend && npm install && npm run dev # :5173 (proxy /api → 4000)
|
||||
```
|
||||
|
||||
Variables d'env : copier `.env.example` → `.env`, renseigner `JWT_SECRET`.
|
||||
|
||||
---
|
||||
|
||||
## Fichiers clés à connaître
|
||||
|
||||
| Fichier | Rôle |
|
||||
|---|---|
|
||||
| `backend/src/db/index.js` | Init DB + **toutes les migrations** |
|
||||
| `backend/src/db/schema.sql` | Schéma de référence (CREATE TABLE) — ne pas modifier pour les évolutions |
|
||||
| `frontend/src/api.js` | Client HTTP centralisé (gère JWT) |
|
||||
| `frontend/src/utils/format.js` | `fmtEUR`, `fmtDate`, `fmtPct`, `fmtStatut`, `memberLabel`, `today` |
|
||||
| `frontend/src/context/UiContext.jsx` | `displayMode` (brut/net), sidebar, fontScale, langue, devise |
|
||||
| `frontend/src/components/Layout.jsx` | Shell app, topbar, toggle Brut/Net |
|
||||
| `frontend/src/pages/Settings.jsx` | Hub paramètres, helpers fiscalité plateforme |
|
||||
|
||||
---
|
||||
|
||||
## Layout deux colonnes — pattern Settings (session 2026-05-08)
|
||||
|
||||
Les sections **Plateformes** et **Catégories d'investissement** de `Settings.jsx` utilisent désormais le même pattern deux colonnes que `DepotsRetraits.jsx` :
|
||||
|
||||
- Classes CSS réutilisées : `.dr-mouvements-layout`, `.dr-mouvements-list`, `.dr-mouvements-detail`, `.dr-row`, `.dr-row-selected`, `.dr-detail`, `.dr-detail-empty`, `.dr-detail-title`, `.dr-detail-fields`, `.dr-detail-field`, `.dr-detail-label`, `.dr-detail-value`, `.dr-detail-footer`, `.dr-detail-edit-btn`
|
||||
- **Plateformes** : tableau réduit (Nom + Catégories d'invest. + Nb invest.), panneau détail droite avec `PlatDetailPanel`, création via modale `showNewPlat`, **suppression dans la modale d'édition** (bouton "Supprimer" dans le footer gauche de la modale — logique inline avec `confirm()` + early return si annulé)
|
||||
- **Catégories** : tableau réduit (Nom + Nb plateformes), panneau détail droite avec `CatDetailPanel`, création via modale `showNewCat`, suppression contextuelle dans le panneau (bouton rouge activé si non-utilisée, grisé si utilisée), export CSV/JSON
|
||||
- Auto-sélection du premier item : `useEffect` avec `setSelectedX(prev => prev ? prev : items[0])`
|
||||
- `nb_investissements` ajouté au backend dans `GET /api/plateformes` via sous-requête SQL (pas de nouvel endpoint)
|
||||
- **Pas de PUT /api/categories** → pas de renommage de catégorie possible côté UI
|
||||
|
||||
---
|
||||
|
||||
## CategorySelect — dropdown position:fixed (session 2026-05-08)
|
||||
|
||||
`CategorySelect.jsx` a été réécrit pour corriger le clipping du dropdown dans les modales (`overflow: auto` clippe les descendants `position: absolute`).
|
||||
|
||||
Solution : dropdown en `position: fixed` avec position calculée via `getBoundingClientRect()` :
|
||||
- `triggerRef` sur le bouton déclencheur
|
||||
- `useLayoutEffect` mesure `rect.bottom + 4` / `rect.left` / `rect.width` à chaque ouverture
|
||||
- Dropdown rendu **en dehors du `.cat-select-wrap`** (comme sibling dans le `<>...</>`) avec `id="cat-select-dropdown-portal"` et `zIndex: 9999`
|
||||
- Click-outside exclut à la fois `wrapRef` et le portal
|
||||
- `scroll` (capture phase) + `resize` ferment le dropdown
|
||||
|
||||
---
|
||||
|
||||
## Nomenclature — Catégories (session 2026-05-08)
|
||||
|
||||
- **Labels utilisateur** : "Catégories d'investissement" (section Settings, modales, boutons, export)
|
||||
- **Identifiants code** : inchangés (`categories`, `categorie_id`, `categories_plateforme`, `/api/categories`)
|
||||
- Dans les tableaux/badges : abréviation "Catégories d'invest." acceptable
|
||||
|
||||
---
|
||||
|
||||
## Catégorie d'investissement sur les investissements (session 2026-05-08)
|
||||
|
||||
### Modèle de données
|
||||
- Colonne `categorie_id INTEGER` ajoutée sur `investissements` (migration dans `db/index.js`), FK vers `categories_plateforme(id) ON DELETE SET NULL`
|
||||
- Backend retourne `categorie_nom` via `LEFT JOIN categories_plateforme cp ON cp.id = i.categorie_id` dans `GET /investissements` et `GET /investissements/:id`
|
||||
- Schema Zod : `categorie_id: z.number().int().positive().nullable().optional()`
|
||||
|
||||
### Logique de sélection par défaut (à respecter partout)
|
||||
```js
|
||||
// 0 catégories liées à la plateforme → CategorySelect libre (toutes cats, mode single-select)
|
||||
// 1 catégorie → auto-sélectionnée, select disabled
|
||||
// 2+ catégories → select activé, défaut = la plus utilisée dans rows (user-wide), sinon première
|
||||
function defaultCategorieId(platId, plats, rows = []) {
|
||||
const plat = plats.find(p => p.id === Number(platId));
|
||||
if (!plat || !plat.categories || plat.categories.length === 0) return '';
|
||||
if (plat.categories.length === 1) return plat.categories[0].id;
|
||||
const counts = {};
|
||||
for (const r of rows) {
|
||||
if (r.categorie_id && plat.categories.some(c => c.id === r.categorie_id))
|
||||
counts[r.categorie_id] = (counts[r.categorie_id] || 0) + 1;
|
||||
}
|
||||
if (Object.keys(counts).length > 0)
|
||||
return Number(Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0]);
|
||||
return plat.categories[0].id;
|
||||
}
|
||||
```
|
||||
- Dans `InvestissementDetail.jsx` : version sans `rows` (pas de liste disponible), 2+ → première de la liste
|
||||
- `InvestissementDetail.defaultCategorieId` : signature `(platId, plats)` seulement
|
||||
|
||||
### Ouverture de modale édition — règle critique
|
||||
**Toujours** initialiser `categorie_id` avec `row.categorie_id || defaultCategorieId(...)` et NON `row.categorie_id || ''`. Sans ça, les anciens investissements (categorie_id null en base) affichent la bonne catégorie visuellement (select disabled sur 1 seul choix) mais envoient `null` au backend car l'état React reste à `''`.
|
||||
|
||||
### CategorySelect en mode single-select
|
||||
Quand CategorySelect est utilisé pour un champ à valeur unique (ex : categorie_id) :
|
||||
```jsx
|
||||
selected={form.categorie_id ? [Number(form.categorie_id)] : []}
|
||||
onChange={ids => setForm(f => ({ ...f, categorie_id: ids[ids.length - 1] ?? '' }))}
|
||||
```
|
||||
`ids[ids.length - 1]` fonctionne car `toggle()` ajoute à la fin — unchecking donne `[]`, switching donne `[ancien, nouveau]`.
|
||||
|
||||
### Fichiers modifiés
|
||||
| Fichier | Changement |
|
||||
|---|---|
|
||||
| `backend/src/db/index.js` | Migration `categorie_id` sur `investissements` |
|
||||
| `backend/src/routes/investissements.js` | Schema + GET/POST/PUT avec categorie_id/categorie_nom |
|
||||
| `frontend/src/pages/Investissements.jsx` | Import CategorySelect, état categories, defaultCategorieId, champ dynamique modale |
|
||||
| `frontend/src/pages/InvestissementDetail.jsx` | Idem + affichage dans "Informations du projet" |
|
||||
|
||||
---
|
||||
|
||||
## Bonus Parrainage / Bonus Plateforme (session 3 — 2026-05-08)
|
||||
|
||||
### Modèle de données
|
||||
- `remboursements.type` : `'normal'` | `'bonus_parrainage'` | `'bonus_plateforme'`
|
||||
- Pour type `normal` : `investissement_id` obligatoire, `bonus_plateforme_id` null
|
||||
- Pour les bonus : `bonus_plateforme_id` obligatoire, `investissement_id` null, `bonus_investisseur_id` pour le scope
|
||||
- Seul champ saisi pour les bonus : `cashback` (stocké aussi dans `net_recu`)
|
||||
|
||||
### Frontend — sentinelles dans la modale Remboursements
|
||||
```js
|
||||
const BONUS_VALUES = ['BONUS_PARRAINAGE', 'BONUS_PLATEFORME'];
|
||||
const BONUS_TYPE_MAP = { BONUS_PARRAINAGE: 'bonus_parrainage', BONUS_PLATEFORME: 'bonus_plateforme' };
|
||||
const isBonus = BONUS_VALUES.includes(form.investissement_id);
|
||||
```
|
||||
- Options ajoutées au select investissement : `value="BONUS_PARRAINAGE"` et `value="BONUS_PLATEFORME"`
|
||||
- Quand `isBonus` : affiche select plateforme (requis) + select investisseur optionnel (vue 'all'), masque capital/intérêts/prélèvements
|
||||
- Payload envoyé : `{ type, bonus_plateforme_id, bonus_investisseur_id, date_remb, cashback, statut, notes }`
|
||||
- `openEdit` : mappe `r.type` → sentinel pour pré-remplir le select
|
||||
|
||||
### Backend — route remboursements
|
||||
- GET : `LEFT JOIN investissements` + `COALESCE` pour résoudre `plateforme_id` et `investisseur_id`
|
||||
- **Alias critique** : `COALESCE(i.plateforme_id, r.bonus_plateforme_id) AS plateforme_id` (pas `resolved_plateforme_id` — le frontend utilise `r.plateforme_id` partout pour l'agrégation et les filtres)
|
||||
- `nom_projet` : `CASE r.type WHEN 'bonus_parrainage' THEN '— Bonus Parrainage' WHEN 'bonus_plateforme' THEN '— Bonus Plateforme'`
|
||||
- `capital_restant_du` : correlated subquery (pas window function) pour gérer les NULL
|
||||
- Vue `v_interets_annuels` : filtre `AND r.type = 'normal'` pour exclure les bonus
|
||||
|
||||
### Migration DB (db/index.js)
|
||||
- Recréation complète de la table `remboursements` via table temporaire `__repair_remboursements`
|
||||
- Guard : `!rembColsBonus.includes('bonus_plateforme_id')`
|
||||
- `PRAGMA foreign_keys = OFF` avant, `ON` après
|
||||
- Drop/recreate de la vue `v_interets_annuels` (qui référence `investissement_id`)
|
||||
|
||||
---
|
||||
|
||||
## Problèmes de compatibilité Node.js rencontrés (session 3)
|
||||
|
||||
- **`||=` (logical OR assignment)** dans `plateformes.js` causait `SyntaxError: Unexpected token ']'` sur Node v20.17.0 → remplacé par `if (!map[x]) map[x] = []`
|
||||
- **`db/index.js` tronqué** : les outils Edit/Write et `cat >>` tronquent parfois les fichiers longs. Solution fiable : reconstruire avec `python3` (lecture + écriture du fichier entier)
|
||||
- **Read tool vs disque** : le Read tool peut afficher du contenu en cache qui ne correspond pas au fichier réel — toujours vérifier avec `bash tail` ou `wc -l`
|
||||
- **Node v22.22** : version actuelle après upgrade depuis v20.17 — résout les problèmes de syntaxe
|
||||
- **Octets nuls en fin de fichier** : les opérations `cat >>` successives peuvent laisser des `\x00` à la fin d'un fichier → `SyntaxError: Invalid or unexpected token` à la ligne N+1. Fix : `python3 -c "content=open(f,'rb').read(); open(f,'wb').write(content.rstrip(b'\\x00'))"`
|
||||
|
||||
---
|
||||
|
||||
## Corrections session 4 — Remboursements (2026-05-09)
|
||||
|
||||
### Page Remboursements — onglet "Remboursements"
|
||||
- **Colonne "Type" ajoutée** au tableau : badge "Parrainage" (bleu, `var(--primary)`) pour `bonus_parrainage`, badge "Bonus" (vert, `var(--success)`) pour `bonus_plateforme`. Colonnes Capital/Intérêts/Prélèvements affichent `—` pour les bonus.
|
||||
- **Filtre "Type" ajouté** dans la barre de filtres : Tous / Remboursements / Bonus Parrainage / Bonus Plateforme. Désactive le filtre "Projet" quand un type bonus est sélectionné.
|
||||
- **Logique filtre `tableRows`** : quand un filtre projet est actif, les lignes bonus sont exclues (comportement explicite). Quand un filtre type bonus est actif, le filtre projet est ignoré.
|
||||
- **Modal "Nouveau remboursement"** : suppression du blocage `investissementsActifs.length > 0` — la modale s'ouvre toujours, même si tous les investissements sont remboursés (utile pour saisir un bonus cashback).
|
||||
|
||||
### Bug investisseur — nom dupliqué dans les modales
|
||||
- **Cause** : `inv.nom` contient déjà le nom complet (ex. "Olivier CROGUENNEC"), et `inv.prenom` vaut "Olivier". La concaténation `${inv.prenom} ${inv.nom}` donnait "Olivier Olivier CROGUENNEC".
|
||||
- **Fix** : utiliser `memberLabel(inv)` (import depuis `../utils/format.js`) à la place de la concaténation manuelle. `memberLabel` retourne simplement `m.nom`.
|
||||
- **Règle à retenir** : toujours utiliser `memberLabel(inv)` pour afficher le nom d'un investisseur dans les options/selects — jamais `inv.prenom + inv.nom`.
|
||||
|
||||
---
|
||||
|
||||
## Session 6 — UX et calculs (2026-05-09)
|
||||
|
||||
### Barre de recherche rapide de projets (Layout.jsx)
|
||||
- Composant `ProjectSearch` ajouté dans la topbar globale (`Layout.jsx`)
|
||||
- Ctrl+K / Cmd+K → focus sur la barre
|
||||
- Résultats filtrés (max 8) sur `nom_projet` et `plateforme_nom`, navigation ↑↓ Enter Escape
|
||||
- Clic ou Enter → navigate vers `/investissements/${inv.id}`
|
||||
- Classes CSS : `.project-search-wrap` (border primary, border-radius 20px, min-width 330px), `.project-search-input` (outline:none, box-shadow:none), `.project-search-dropdown`, `.project-search-item`, `.project-search-item-name`, `.project-search-item-meta`, `.project-search-kbd`, `.project-search-clear`
|
||||
- `STATUT_LABELS` dans `ProjectSearch` : `{ en_cours:'en cours', rembourse:'remboursé', en_retard:'en retard', procedure:'procédure', cloture:'clôturé' }` — obligatoire pour avoir les accents corrects
|
||||
|
||||
### Création investissement → redirection automatique vers le détail
|
||||
- Dans `Investissements.jsx`, après `api.post('/investissements', payload)` : `navigate(`/investissements/${created.id}`)` (au lieu d'un reload)
|
||||
|
||||
### KPI dynamiques (année/plateforme) — pattern unifié
|
||||
- **Architecture** : `allRows` → `kpiRows` (filtre plateforme + `platYear`/`rembPlatYear`/`drPlatYear`, sans filtre statut/type) → `chartRows` (+ statut/type) → `rows` (+ mois)
|
||||
- `totals` calculés depuis `kpiRows` pour que les KPI restent dynamiques
|
||||
- `platYear` / `rembPlatYear` / `drPlatYear` : états indépendants de `filter.year` — pour le sélecteur d'année du tableau par plateforme uniquement
|
||||
- **DepotsRetraits** : `kpiSoldePortefeuille` calculé live depuis backend (sans filtre année) OU cumulatif depuis `allRows + allRemb` jusqu'à `${drPlatYear}-12-31` (avec filtre année, inclut les remboursements `methode=portefeuille`)
|
||||
- KPI sur une seule ligne `.dr-kpi-row` en flexbox (`.dr-kpi-row > .kpi { flex: 1 }`) — s'adapte à n'importe quel nombre de KPI
|
||||
|
||||
### Bouton "+" dans InvestissementDetail — header "Remboursements enregistrés"
|
||||
- Bouton `.btn-icon` à droite du titre (flex justify-content: space-between)
|
||||
- Appelle `openNewRemb()` : pré-remplit date du jour + methode_remboursement de la plateforme, ouvre la modale de saisie
|
||||
- CSS `.btn-icon` : 32×32px, border-radius 50%, color var(--text), hover → background var(--surface-2) + color var(--primary). `.btn-icon svg { width:18px; height:18px }` — toujours dimensionner via CSS, pas via attributs SVG width/height
|
||||
|
||||
### Taux PFU — fallback dernière année connue
|
||||
- **Avant** : si année absente de la table PFU → 0% (bug projections futures)
|
||||
- **Après** : utiliser `getLastKnownRates()` = `pfuRates.reduce((best, r) => r.annee > best.annee ? r : best, pfuRates[0])`
|
||||
- Corrigé dans : `Dashboard.jsx` (`getPfuReduction`), `InvestissementDetail.jsx` (`getRatesForYear`), `Remboursements.jsx` (`getRatesForYear` + `getPfuReduction`)
|
||||
|
||||
### Première période partielle (mois incomplet) — adjustFirstPartialPeriod
|
||||
- Ajouté dans `backend/src/utils/schedule.js`
|
||||
- Appelé à la fin de `adjustSimulForActuals` (après le recalcul capital ET après `generateSimul` pour le cas `total_capital <= 0`)
|
||||
- **Logique** : trouve le 1er remboursement réel avec `interets_bruts > 0` → cherche l'entrée simul du même mois YYYY-MM → si `interets_prevus (simul) > interets_bruts (réel)` d'au moins 0,001 € → met à jour la première ligne avec le montant réel + reporte la différence sur la dernière échéance
|
||||
- **Idempotent** : si les valeurs sont déjà alignées, aucune modification
|
||||
- **Ne s'applique pas** aux prêts `differe` (versement unique, pas de période partielle)
|
||||
|
||||
---
|
||||
|
||||
## Pièges / points d'attention
|
||||
|
||||
1. **`net_recu_total`** dans Investissements = `SUM(r.net_recu)` = capital + cashback + intérêts nets — c'est une approximation du rendement net, pas uniquement les intérêts.
|
||||
2. **XIRR** : calculé uniquement sur `statut === 'rembourse'`. Flux bruts = capital + cashback + interets_bruts. Flux nets = net_recu.
|
||||
3. **`InteretsChart`** n'a plus de toggle interne — toujours passer `netMode` en prop depuis le parent.
|
||||
4. **Migrations DB** : guard `PRAGMA table_info()` obligatoire pour éviter les erreurs si colonne déjà présente.
|
||||
5. **Admin** : middleware `requireAdmin` sur `/api/admin`. Lien visible dans UserMenu uniquement si `isAdmin`.
|
||||
6. **Job auto-statut** : `backend/src/jobs/autoStatut.js` — met à jour les statuts investissements automatiquement.
|
||||
7. **Modal + dropdown** : ne jamais utiliser `position: absolute` pour un dropdown dans une modale — utiliser `position: fixed` + `getBoundingClientRect()` (cf. `CategorySelect.jsx`).
|
||||
8. **Suppression dans modale** : ne pas chaîner `.then(() => closeModal())` sur une fonction qui appelle `confirm()` — inliner la logique avec early return si `!confirm(...)` pour éviter la fermeture sur "Annuler".
|
||||
9. **Affichage investisseur** : toujours `memberLabel(inv)` — jamais `inv.prenom + ' ' + inv.nom` car `nom` contient déjà le nom complet.
|
||||
Reference in New Issue
Block a user