Compare commits

..

134 Commits

Author SHA1 Message Date
ocroguennec 41c464e6a3 Améliorations diverses 2026-07-20 22:12:49 +02:00
ocroguennec 441da1975a Possibilité d'avoir une clé API Globale par famille 2026-07-20 18:47:58 +02:00
ocroguennec 9eb19efd92 MAj MCP Serveur 2026-07-20 17:30:08 +02:00
ocroguennec 6e58731a20 Fix 2026-07-15 23:08:36 +02:00
ocroguennec 56dd1f89bd Update MCP server 2026-07-15 22:59:44 +02:00
ocroguennec 5a0a1c03ac Correction 2026-07-15 22:46:49 +02:00
ocroguennec fa318f240c MCP Distant 2026-07-15 22:40:52 +02:00
ocroguennec 4d8fb9bab8 API V1 Public + preparation serveur MCP 2026-07-15 19:33:13 +02:00
ocroguennec c843464ccd Création de la feature API V1 2026-07-14 16:22:34 +02:00
ocroguennec f9c5316303 Sauvegarde de la mémoire 2026-07-14 15:40:57 +02:00
ocroguennec 3a9de15526 Fix multi-détenteur 2026-07-14 15:35:35 +02:00
ocroguennec 069e316362 Correction bug importation de données de cashback 2026-07-14 15:25:48 +02:00
ocroguennec 84f1cb19d5 Correction importation 2026-07-14 15:03:08 +02:00
ocroguennec 3e4c0dffa8 Analyse des dossier d'investissemetns incomplets 2026-07-14 14:45:23 +02:00
ocroguennec 4ef7cbee04 Amélioration de l'importation de donnée 2026-07-14 14:10:27 +02:00
ocroguennec 86a047a0c5 Mise en place des objectifs 2026-07-13 17:53:22 +02:00
ocroguennec 63f38b5ff0 Amélioration XIRR 2026-07-13 07:28:53 +02:00
ocroguennec 7189507998 XIRR Label 2026-07-13 07:16:46 +02:00
ocroguennec 7ace2c8f2e Amélioration du calcul XIRR 2026-07-13 07:11:42 +02:00
ocroguennec 316a86c95a Fix 2026-07-12 19:38:41 +02:00
ocroguennec 47c5ad34fa Fixe bug 2026-07-12 19:29:52 +02:00
ocroguennec 96effbe15a Nouvelles features: revision des conditions des prêts 2026-07-12 19:14:27 +02:00
ocroguennec ef4576d160 Amélioration de la détection 2026-07-12 18:11:51 +02:00
ocroguennec 794f8684a2 FixBug: Décalrage de date des prêts différés 2026-07-12 17:58:28 +02:00
ocroguennec ec3c7f7cab Corrections sur les jobs En retard 2026-07-12 17:39:57 +02:00
ocroguennec 0d8f92bc9a Bug de rafraichissement de données 2026-07-05 10:50:17 +02:00
ocroguennec 186ffc2596 Selection de la plateforma avant import 2026-07-05 10:40:33 +02:00
ocroguennec 1f75878d87 Correctif Contrainte de suppression de compte 2026-07-05 00:54:40 +02:00
ocroguennec 5e775da711 Amélioration de l'import 2026-07-05 00:37:38 +02:00
ocroguennec 8bcd7e603d Modification de l'import avec prompt IA 2026-07-04 23:09:46 +02:00
ocroguennec 8834236840 Evite les doublons 2026-07-04 19:17:56 +02:00
ocroguennec b138bc53ee Améliorationde l'import 2026-07-04 18:55:40 +02:00
ocroguennec 11790c63fe Correction de l'affichage en tant que mono détenteur 2026-07-04 18:32:51 +02:00
ocroguennec 69d006d358 Correctif redescente du référentiel 2026-07-04 17:43:57 +02:00
ocroguennec ff77ee977d Amélioration pour nouveau connecté 2026-07-03 20:44:32 +02:00
ocroguennec 53742568ce UX amélioration 2026-07-03 20:01:43 +02:00
ocroguennec 2281894802 Feature: Suppression de son compte 2026-07-03 19:49:32 +02:00
ocroguennec 89190c4561 Améliroation de la piste d'audit 2026-07-03 19:49:14 +02:00
ocroguennec e11c767c1c UX améliorations 2026-07-03 19:02:44 +02:00
ocroguennec b481c4cc1d MAJ 2026-07-03 18:33:58 +02:00
ocroguennec 563063ba17 Ajout de la possibilité de voir le mot de passe 2026-07-03 18:31:59 +02:00
ocroguennec 3c5aa635c4 Ajout direct pour nouveau user 2026-07-03 18:21:24 +02:00
ocroguennec 31d7b8f791 chore: add cowork memory restore script 2026-07-03 17:56:59 +02:00
ocroguennec af3de972d0 modif 2026-07-03 17:53:50 +02:00
ocroguennec 261bf103a3 chore: sync cowork memory 2026-07-03 17:46 2026-07-03 17:46:08 +02:00
ocroguennec aef55aeef2 Maj Packages 2026-07-03 17:09:00 +02:00
ocroguennec 8da6c10868 fix 2026-06-18 23:22:36 +02:00
ocroguennec 2157413417 Fix 2026-06-18 23:20:03 +02:00
ocroguennec 52d2767297 Nouveu fix 2026-06-18 23:13:25 +02:00
ocroguennec e1a54fa86b Fix 2026-06-18 23:11:14 +02:00
ocroguennec 5f86950dba Fix 2026-06-18 22:42:10 +02:00
ocroguennec 0f3c27050b Fixe 2026-06-18 22:11:30 +02:00
ocroguennec b2e73b4fe7 amélrioration 2026-06-18 21:45:05 +02:00
ocroguennec fea1d787ca fix 2026-06-18 21:04:54 +02:00
ocroguennec e94401dce8 fix 2026-06-18 20:47:44 +02:00
ocroguennec 8d3470d7f7 fix 2026-06-18 20:34:05 +02:00
ocroguennec 5bbbbfa956 Centre de notification 2026-06-18 20:26:23 +02:00
ocroguennec bfc97442d9 Fix 2026-06-18 08:45:23 +02:00
ocroguennec 08c70b7c55 Amélioration 2026-06-18 08:32:27 +02:00
ocroguennec 03b7b87538 Améliorations diverses 2026-06-18 08:25:05 +02:00
ocroguennec eb961cbed4 Correction des redirections 2026-06-17 22:45:08 +02:00
ocroguennec a8781a9f62 Fix: prb de date sur les prêts différés 2026-06-17 22:16:29 +02:00
ocroguennec f09d5e4143 Fix 2026-06-16 21:26:49 +02:00
ocroguennec d571bde229 amélioration 2026-06-16 21:13:33 +02:00
ocroguennec ab7b945705 fix 2026-06-16 21:03:15 +02:00
ocroguennec 1423354514 fix 2026-06-16 20:57:40 +02:00
ocroguennec a9b7777b10 correctif 2026-06-16 20:51:30 +02:00
ocroguennec f4db83591f amélioration de l'affichage des projections 2026-06-16 20:08:22 +02:00
ocroguennec 58ec637139 Fixe affichage versement 2026-06-16 20:01:25 +02:00
ocroguennec f3750f43c8 Fix 2026-06-16 19:21:39 +02:00
ocroguennec 578a601fae Fix 2026-06-16 18:55:37 +02:00
ocroguennec c23430d915 Maj export 2026-06-16 16:47:06 +02:00
ocroguennec 80506ca2dc Mise à jour des pages d'accueil 2026-06-16 15:12:25 +02:00
ocroguennec 720634971d Export des détenteurs 2026-06-16 09:35:22 +02:00
ocroguennec f097fc65f7 Fix import 2026-06-16 09:25:28 +02:00
ocroguennec 6234c76507 MAJ 2026-06-16 09:16:40 +02:00
ocroguennec 9058bc7d22 Message lorsqu'aucun enregistrement présent 2026-06-16 08:38:23 +02:00
ocroguennec 032a370e7d Maj Section Général 2026-06-15 23:03:37 +02:00
ocroguennec 01526a5551 Règle de gestion des mots de passe 2026-06-15 22:44:17 +02:00
ocroguennec ceda5bcf1b Fix 2026-06-15 22:35:03 +02:00
ocroguennec a5b4f3e721 Audit activité utilisateurs 2026-06-15 22:28:15 +02:00
ocroguennec f54d352b7f Fix 2026-06-15 22:06:30 +02:00
ocroguennec ecc2670fbc Améliorations diverses 2026-06-15 21:56:57 +02:00
ocroguennec 854394f4d6 Maj pour invitation 2026-06-15 21:48:25 +02:00
ocroguennec 9eca1ac8c7 Fix 2026-06-15 21:40:25 +02:00
ocroguennec f3c5387a92 Gestion des utilisateurs 2026-06-15 07:32:36 +02:00
ocroguennec 414c8dbb74 MAj script purge old containers 2026-06-15 06:24:56 +02:00
ocroguennec aeafb38789 amelioration 2026-06-14 22:40:05 +02:00
ocroguennec a6fc9f445f Fix 2FA 2026-06-14 22:35:27 +02:00
ocroguennec 6f3dc0c05a Fix 2026-06-14 22:16:22 +02:00
ocroguennec 402d690e96 Amélioration 2FA 2026-06-14 22:13:33 +02:00
ocroguennec 13ec48a8f3 Fix 2FA 2026-06-14 22:07:57 +02:00
ocroguennec 8eac51856a Fix 2fa 2026-06-14 22:03:14 +02:00
ocroguennec c40741c023 Fix 2FA 2026-06-14 22:01:29 +02:00
ocroguennec 9f6363ec20 Modification des pages d'authentification 2026-06-14 21:58:05 +02:00
ocroguennec dc28b12c27 Fix 2026-06-14 20:44:11 +02:00
ocroguennec 40188b288f Fix 2026-06-14 20:38:41 +02:00
ocroguennec 53b1ad2063 Implementation du mail 2026-06-14 20:34:52 +02:00
ocroguennec 96b1741cd1 fix 2026-06-14 18:58:03 +02:00
ocroguennec c021235af5 fix safari 2026-06-14 18:18:58 +02:00
ocroguennec 2f689c42d8 fix 2026-06-14 18:02:08 +02:00
ocroguennec 440f59d86c Top Bar petits écrans 2026-06-14 17:53:02 +02:00
ocroguennec 1bc626e7d5 fix: Menu minimized quand Ipad 2026-06-14 17:29:28 +02:00
ocroguennec 98d10eaf4a Feat: favicon et manifest 2026-06-14 17:22:54 +02:00
ocroguennec b496f29a43 fix : Création du compte courant 2026-06-14 17:15:56 +02:00
ocroguennec 94dde0707e Correction menu user pour la gestion des profiles 2026-06-14 17:12:57 +02:00
ocroguennec 837b016cb9 feat(fiscal): export/import ZIP référentiel fiscal
- Remplace le dropdown CSV/XLS/JSON de PfuSection par Exporter/Importer ZIP
- GET /api/pfu/export-zip : ZIP avec manifest, pfu.json, taux_credit_impot.json
- POST /api/pfu/import-zip : upsert PFU (par annee) et TCI (par nom_pays/code_pays)
- ResultBanner pour le résultat de l'import dans PfuSection
2026-06-13 20:29:49 +00:00
ocroguennec 81a57dc0b2 fix: export unitaire /:id/export — ajouter notation + garanties (v1.1) 2026-06-13 22:14:10 +02:00
ocroguennec e6d2463a91 feat: export/import ZIP — garantie_types + referentiel_notation (fichier complet) 2026-06-13 22:07:40 +02:00
ocroguennec 611dfe82f0 feat: export/import ZIP référentiel inclut garantie_types + referentiel_notation 2026-06-13 22:03:10 +02:00
ocroguennec c987ef3f42 fix: DATA_DIR env var pour chemins logos/icons (dev vs Docker) 2026-06-13 21:44:47 +02:00
ocroguennec 53d51e0075 fix: logosDir mauvais chemin dans referentiel.js (../../ au lieu de ../../../) 2026-06-13 21:36:53 +02:00
ocroguennec 0cab38e808 fix: import-zip VALUES count + fallthrough false pour logos/icons manquants 2026-06-13 21:31:58 +02:00
ocroguennec b7337862b8 fix: import-zip — VALUES avait 1 ? en trop (34 pour 33 colonnes) 2026-06-13 21:25:10 +02:00
ocroguennec a6a2e46534 fix: import-zip référentiel — ajouter methode_remboursement/type_pret_defaut/freq_interets_defaut manquants dans INSERT et UPDATE 2026-06-13 21:20:48 +02:00
ocroguennec 9f759e91f0 fix: entrypoint copie tous les types de fichiers icônes (svg + png) 2026-06-13 21:11:13 +02:00
ocroguennec 60ed448d69 feat: seed bibliothèque icônes mis à jour — 16 icônes depuis dev 2026-06-13 21:03:59 +02:00
ocroguennec a9ed68de47 fix: chemin icônes/logos ../data au lieu de ../../data (ESM __dirname = /app/src) 2026-06-13 20:56:02 +02:00
ocroguennec 24d5d3c09f fix: icônes seeds dans backend/icons_seed/ hors du contexte data/ 2026-06-13 20:45:19 +02:00
ocroguennec 8bb9d44cda fix: inclure data/icons dans le contexte Docker pour le seed des icônes 2026-06-13 20:43:10 +02:00
ocroguennec bf9bd6a076 feat: entrypoint initialise les icônes seeds dans le volume au premier démarrage 2026-06-13 20:40:58 +02:00
ocroguennec a78ee04d06 fix: backup db par docker cp au lieu de sqlite3 2026-06-13 20:30:46 +02:00
ocroguennec 43fac96f34 fix: backup db par docker cp au lieu de sqlite3 2026-06-13 20:28:51 +02:00
ocroguennec 0b2393a2dc fix 2026-06-13 20:23:20 +02:00
ocroguennec 8b00ab1dc1 Déplacement des volumes 2026-06-13 20:19:45 +02:00
ocroguennec 211efcee44 fix: middleware traefik @docker → @file 2026-06-13 19:16:28 +02:00
ocroguennec a7baeb65a4 fix: nginx upstream backend → crowdlending-backend 2026-06-13 19:13:03 +02:00
ocroguennec f117dec774 Fix 2026-06-13 18:56:24 +02:00
ocroguennec d23bc4819f fix: déplacer migration csg/crds/solidarite avant le seed taux_pfu
La migration ALTER TABLE ADD COLUMN pour csg, crds et solidarite s'exécutait
après le seed qui tentait d'insérer ces colonnes, provoquant un crash SQLite.
Déplace le bloc de migration avant le seed pour respecter l'ordre d'exécution.
2026-06-13 16:47:27 +00:00
Olivier CROGUENNEC 56f10bf8f0 Maj 2026-06-13 18:21:08 +02:00
Olivier CROGUENNEC fa6011fadd Merge master into main 2026-06-13 17:52:55 +02:00
Olivier CROGUENNEC 1694234fc4 Correction d l'ordre des tris 2026-06-13 17:47:29 +02:00
Olivier CROGUENNEC 5432b0bb3c Correction bug 2026-06-13 15:23:01 +02:00
Olivier CROGUENNEC 48ed7fe65e Initial commit 2026-06-13 14:57:15 +02:00
406 changed files with 81543 additions and 2 deletions
@@ -0,0 +1,97 @@
# MEMORY.md — Crowdlending App
- [Profil utilisateur](user_profile.md) — Olivier, propriétaire de l'app crowdlending, développeur qui pilote les évolutions
- [Modèle plateforme — détenteur & logo](project_plateforme_detenteur_logo.md) — Champs investisseur_id, date_ouverture, logo_filename ajoutés aux plateformes ; contrainte UNIQUE modifiée
- [Pattern investisseurForPlat](feedback_investisseur_for_plat.md) — Helper à créer dans chaque page formulaire pour synchroniser le détenteur avec la plateforme choisie
- [Fonctionnalité réinvestissements](project_reinvestissements.md) — Table reinvestissements, route /api/reinvestissements, capital_total, generateSimulWithReinvestissements, adjustSimulForActuals modifiée
- [adjustSimulForActuals — règles complètes](feedback_adjustsimul.md) — Capital effectif, capital soldé (DELETE futures), reprocess doit appeler adjustSimulForActuals après transaction
- [Calcul solde porte-monnaie](project_solde_portefeuille.md) — Formule complète : dépôts retraits_manuels + net_recu_portefeuille + bonus capital_investi ; implémentée backend (dashboard.js) et frontend (DepotsRetraits.jsx)
- [Pattern multiDetenteur](feedback_multi_detenteur.md) — Le détenteur ne s'affiche dans les selects plateforme que si plusieurs détenteurs distincts existent (`new Set(plats.map(p => p.investisseur_id)).size > 1`)
- [Fonctionnalité corrections de solde](project_corrections_solde.md) — Table corrections_solde, route /api/corrections, CorrectionModal dans DepotsRetraits, badge dans Remboursements, intégration fiscal2778
- [Troncature et corruption fichiers — toujours passer par Python](feedback_file_truncation.md) — Edit/Write et sed -i tronquent les fichiers volumineux ; jamais sed, toujours python3 + vérifier tail/hex
- [Composant ConfirmModal et suppressions](project_confirm_modal.md) — ConfirmModal.jsx créé ; pattern standard pour toute future suppression ; 6 fichiers déjà migrés
- [Correction intérêts nets — Investissements](project_interets_nets_investissements.md) — interets_nets_total ajouté au backend ; net_recu_total ≠ intérêts nets (inclut capital+cashback)
- [Fiscalité locale sur remboursements](project_fiscalite_locale_remboursements.md) — Champs interets_bruts_avant_local et taxe_locale ajoutés (DB + backend + 2 formulaires + tableau détail)
- [Page Centre d'aide](project_aide.md) — Route /aide, layout Settings, FAQ accordéon ; bouton dans UserMenu
- [Menus ⋮ actions sur les tableaux et blocs](project_menu_actions_tables.md) — Pattern menu contextuel fixe (position: fixed + backdrop + icônes SVG) ; scroll ferme les menus ; AdminPlateformes ajouté
- [Format menu ⋮ — règle obligatoire](feedback_menu_actions_format.md) — Tout nouveau tableau doit utiliser le menu ⋮ avec icônes SVG + label, jamais de boutons inline
- [Logo plateforme topbar InvestissementDetail](project_logo_topbar_detail.md) — logo remplace le nom si disponible ; plateforme_logo retourné par le backend ; classe logo-plateforme pour dark mode
- [onClick direct avec paramètre par défaut](feedback_onclick_default_param.md) — onClick={fn} passe l'event comme arg1, écrase les defaults ; toujours utiliser onClick={() => fn()}
- [Réinvestissement automatique des intérêts](project_auto_reinvest.md) — auto_reinvest sur investissements, source sur reinvestissements, déclencheur POST remboursement, tabs Manuel/Auto dans la modale
- [Panneau détail unifié DepotsRetraits](project_detail_panel_depots.md) — DetailPanel unifié dépôts/retraits/corrections ; libellé bouton adapté au type ; props { row, onEdit, onDeleteCorrection }
- [Menu ⋮ carte Informations du projet](project_card_menu_investissement.md) — cardMenu state séparé ; Modifier/Réinvestir/Exporter/Supprimer + Désactiver auto-reinvest ; utiliser ConfirmModal pas confirmingInvDelete
- [Méthode de remboursement sur investissements](project_methode_remboursement_investissement.md) — methode_remboursement + nom_compte_courant sur investissements ; champ conditionnel si plateforme=choix_investisseur ; retrait auto depots_retraits ; affichage fiche détail
- [Override fiscalité par investissement](project_fiscalite_override.md) — fiscalite_override='exonere' ; badge + menu ⋮ + modale confirmation ; prelev indicatifs (pas forcés à 0) ; voir [[project_istaxindicatif]]
- [Règle isTaxIndicatif + route reprocess](project_istaxindicatif.md) — prelev toujours calculés, net_recu = brut si indicatif ; POST /api/remboursements/reprocess ; bouton dans MonCompte?section=nettoyage
- [Fallback methode remboursement formulaire](feedback_remb_methode_fallback.md) — openRembFromSimul/openNewRemb : utiliser inv.methode_remboursement si plateforme=choix_investisseur, jamais 'portefeuille' en dur
- [Formulaires remboursement refactorisés](project_formulaires_remboursement.md) — Grille 3 col / 900px, sections Remboursement/Imposition/Versement, Total prélèvements, commentaire Montant versé, isIndicatif aligné
- [Tableau Remboursements enregistrés](project_tableau_remboursements_detail.md) — Fusion PS+IR→Imposition, tooltip CSS instantané (.cell-tooltip), remb-table CSS, clic ligne → modale édition
- [Graphiques Dashboard — barres + donut](project_interets_mensuels_chart.md) — InteretsMensuelsChart (barres empilées) + InteretsDonutChart (donut caps arrondis) + InteretsChartContext partagé ; mode TOUT (vue multi-années) ; layout 2/3+1/3 stretch
- [Donut interactif — tooltip, clic, mémoire, légende](project_donut_interactif.md) — Tooltip SVG, clic isole un type ou Reçu/Projeté, retour arrière via centre (↩), légende cliquable, ordre Intérêts→Capital→Cashback
- [YearSelectorKpi — sélecteur année Dashboard](project_year_selector_kpi.md) — Card violet/indigo sur la ligne KPI, dropdown custom années croissant + "Depuis le début", sync bidirectionnelle avec InteretsChartContext
- [Couleurs personnalisables des graphiques](project_chart_colors.md) — UiContext : cl_chart_interets/capital/cashback ; Settings > Apparence : palette MD2, PaletteSelector, ChartColorPicker, preview Reçu/Projeté (lightenColor 72%)
- [Préférences utilisateur en DB](project_user_preferences.md) — Table user_preferences (user_id, key, value) ; route /api/preferences GET+PATCH ; UiContext sync DB→localStorage au montage, PATCH asynchrone
- [Bibliothèque d'icônes](project_icons_library.md) — Tables app_icons + history, route /api/icons, section Admin, nettoyage fond SVG, composant AppIcon dans Settings
- [InvestissementDetail — nouvelles fonctionnalités](project_investissement_detail_features.md) — Menu simulMenu : modifier DPE + traitement en masse remboursements ; correction ConfirmModal rowDeleteConfirm manquant dans le JSX
- [PUT investissements — payload minimal](feedback_put_payload_minimal.md) — Ne jamais spread `inv` ; construire le payload champ par champ selon le schéma Zod backend
- [KPIs Dashboard — TrendBadge, logique M vs M-1](project_dashboard_kpis.md) — 5 KPIs refaits ; année N = mois courant vs M-1 ; année N-x = annuel vs N-1 ; modeGlobal = cumul sans badge
- [Tableau intérêts par plateforme](project_tableau_interets_plateforme.md) — TableauInteretsPlateforme.jsx dans Dashboard ; endpoint /api/dashboard/interets-par-plateforme ; synchronisé InteretsChartContext
- [Capital investi — formule unifiée](project_invchart_capital_encours.md) — montant_investi + reinvests(≤date) capital_remboursé(≤date) ; dashboard.js + Investissements.jsx + InvChart
- [Timezone — ne pas utiliser .toISOString() pour dates locales](feedback_timezone_lastday.md) — `.toISOString().split('T')[0]` décale d'un jour en UTC+1 ; toujours getFullYear/getMonth/getDate
- [Vision mensuelle — capital par plateforme/mois](project_capital_mensuel_table.md) — CapitalMensuelTable.jsx, onglet entre Plateformes et Investissements, calcul frontend, classes tip-table
- [Exonération fiscale sur comptes](project_comptes_exoneration.md) — Colonne exoneration_fiscale (aucune/pfnl_5ans) sur table comptes ; DB+backend+frontend Settings
- [Vision mensuelle — dépôts/retraits par plateforme/mois](project_depots_mensuel_table.md) — DepotsMensuelTable.jsx, onglet entre Plateformes et Mouvements dans DepotsRetraits, filtres icônes dépôt/retrait
- [UX formulaires Investissement & Remboursement](project_formulaires_ux.md) — Pas de présélection plateforme/investissement ; détenteur masqué (auto) ; taux/durée/DPE requis
- [UserMenu — portail React obligatoire](feedback_usermenu_portal.md) — Popup et sous-panel via createPortal(…, document.body) pour échapper au stacking context de la sidebar sticky
- [TableauInteretsPlateforme — padding wrapper](feedback_tiptable_padding.md) — Envelopper dans `<div style={{ padding: '0 24px' }}>` dans chaque page (Dashboard, Remboursements)
- [Toggle Détaillé/Consolidé tableaux mensuels](project_toggle_consolidation.md) — Bouton dans th Plateforme, clé localStorage `cl_tip_group_by_nom`, sur 4 composants ; mergeMaps pour TableauInteretsPlateforme
- [Icônes bibliothèque — sidebar & titres pages](project_icons_sidebar_titles.md) — NavIcon dans Layout.jsx (24px, fallback SVG) ; PageIcon inline dans h2 (40px, cache module-level) ; 5 pages équipées
- [Règle icônes topbar — ne pas modifier flex](feedback_pageicon_topbar.md) — Toujours insérer PageIcon comme img inline dans le h2, jamais de wrapper div ni display:flex sur le h2
- [Pagination listes](project_pagination.md) — Hook usePagination + composant Pagination ; 15/25/50/100 items, localStorage par page, reset sur filtre
- [Export Excel SheetJS](project_xlsx_export.md) — xlsx installé dans frontend/ ; pattern json_to_sheet → book_append_sheet → write type:array ; MIME xlsx
- [TrendBadge KPIs Investissements](project_trendbadge_investissements.md) — TrendBadge sur les 5 KPIs ; platYear initialisé à l'année courante ; effectiveYear pour badges en vue "Toutes les années"
- [TrendBadge KPIs DepotsRetraits + Remboursements](project_trendbadge_depots_remb.md) — Même pattern déployé sur DepotsRetraits (4 KPIs) et Remboursements (3 KPIs)
- [Page Fiscalité — refonte TaxReport](project_taxreport.md) — Fiscal2778→TaxReport, route /taxreport, onglets dr-tabs, pagination détail, YearSelector violet autonome
- [Simulation CERFA 2561](project_cerfa2561.md) — Cerfa2561Preview.jsx, onglet inline TaxReport, cases 2TT/2TR/2BH/2CK/2TY, type_produit_fiscal sur plateformes, impression PDF
- [FK compte_id sur investissements et remboursements](project_compte_id_remboursements.md) — DB+backend+4 formulaires frontend ; resolvedMethode depuis plateforme ; auto-sélection premier compte_courant ; colonne Versement dans Remboursements
- [Déclaration 2778-SD et PFO](project_2778sd.md) — Onglet TaxReport conditionné par pfoAssujetti ; matrice mensuelle par plateforme étrangère ; simulation CERFA cases BA/IA/PQ/PV/PF1/PG1/QR ; report 2042 (2TR/2BH/2CK) ; taux dynamiques depuis taux_pfu (csg/crds/solidarite)
- [Architecture onglets fiscaux — 2042 / 2561 / 2778-SD](project_cerfa_fiscal.md) — Cerfa2042Preview (nouveau) + refonte 2561 et 2778-SD : sidebar 3 vues, suivi mensuel combiné, données 2042 mixtes avec badges auto/à déclarer, fullName helper, route /taxreport/2778 reconstruite
- [Référentiel taux crédit d'impôt 2047](project_taux_credit_impot.md) — Table taux_credit_impot (124 pays), route /api/taux-credit-impot, section Admin "Référentiels" avec TciModal, TciImportBlock, TciPromptBlock
- [Pièges CSS — fieldset min-width et checkbox width](feedback_fieldset_checkbox_css.md) — fieldset : toujours minWidth:0 ; input[type=checkbox] : toujours width:'auto'
- [Référentiel plateformes — architecture complète](project_referentiel_plateformes.md) — Tables referentiel + categories + notation, HERITABLE_FIELDS, routes push/reset/lier/importer, similarité Levenshtein 80%, frontend Settings + Admin
- [Bibliothèque logos référentiel](project_logos_referentiel.md) — Section Admin "Logos des plateformes" (id: logos-ref) ; POST/DELETE /api/referentiel/:id/logo ; auto-push logo_filename ; placeholder croix si logo manquant ; useRef pas React.useRef dans Admin.jsx
- [Recherche/filtres/pagination référentiel](project_referentiel_search_filter.md) — Recherche nom + filtre domiciliation + filtre catégorie + pagination dans ReferentielSection ; icone_filename prioritaire sur logo_filename, tous deux servis via /api/logos/
- [Catégories & secteurs d'investissement](project_categories_secteurs_inv.md) — Tables categories_inv/secteurs_inv, routes ref-categories/ref-secteurs, fusion, chips dans PlatformeProfile et RefModal, filtre référentiel via ?filterCat=
- [Catégories/secteurs utilisateur — deux niveaux](project_categories_secteurs_utilisateur.md) — user_id sur categories_inv/secteurs_inv, tables jonction plateforme+investissement, routes /api/categories-inv et /api/secteurs-inv, push admin étendu, sync auto investissements, chips formulaires, filtres liste
- [Domiciliation → codes ISO pays](project_domiciliation_iso.md) — Migration complète : Zod string libre, DB 'france'→'FR', sentinel partout, CountrySelect intégré, DOMICILIATION_LABELS supprimé, badges hérité + ConfirmModal reset
- [ResultBanner — règle bannières succès/erreur](feedback_result_banner.md) — Toujours utiliser ResultBanner (× à droite, auto-dismiss 4s) ; result toujours { ok, msg } ; redirect via onDismiss si besoin
- [Tags suggérés admin — catégories/secteurs](project_tags_suggeres_admin.md) — Section inv-suggestions dans /admin/plateformes ; 6 routes backend ; bouton compteur dans RefListSection ; promouvoir/supprimer
- [ProfilImportBlock — import IA avec tags inconnus](project_profil_import_ia_tags.md) — Flow 3 étapes (json→creation→diff) ; resolveTags Levenshtein≤2 ; création tags approuvés avant PUT ; categories_inv_ids/secteurs_inv_ids dans payload
- [Sauvegarde ZIP référentiel](project_zip_backup_referentiel.md) — zip.js pur Node (no deps) ; 3 routes export/import ; api.blob() ; boutons ReferentielSection ; images incluses ; tags résolus par nom
- [Express — ordre des routes statiques vs /:id](feedback_express_route_order.md) — Routes statiques (/export, /import-zip) TOUJOURS avant /:id ; vérifier avec grep après toute insertion
- [Héritage catégories/secteurs — merge](project_categories_secteurs_inv_heritage.md) — Tags référentiel fusionnés dans les plateformes (is_inherited) ; InvSelect.inheritedIds ; associations-inv.js re-ajoute les inherited IDs au PUT ; compteur bandeau séparé des scalaires
- [Admin sous-pages — structure](project_admin_sous_pages.md) — AdminPlateformes (garanties + notation référentiel) ; AdminFiscalite (PfuSection + TauxCreditImpotSection) ; pattern account-layout
- [Settings NAV — structure actuelle](project_settings_nav.md) — 4 groupes : Interface / Mon paramétrage / Mes tags / Mes données ; membres+nettoyage déplacés de MonCompte vers Settings ; MonCompte réduit à profil+securite
- [Refactorisation Settings.jsx + Admin.jsx](project_settings_admin_refacto.md) — Découpés en composants autonomes (pages/settings/ et pages/admin/) ; imports manquants fréquents ; bugs d'extraction documentés
- [Page Plateformes](project_plateformes_page.md) — /plateformes : vue par plateforme, 4 onglets, simul/all endpoint, sélecteurs TIP identiques, grisage tip-td-closed
- [Grisage cellules hors-période tip-table](feedback_tip_td_closed.md) — .tip-td-closed pour mois avant souscription ou après dernier remb (rembourse) ; tip-col-current prioritaire
- [Anti-doublon projection mois courant](feedback_projection_no_double_count.md) — `real === 0` requis avant d'ajouter projAmt dans buildCellValue ; corrigé dans Plateformes + TableauInteretsPlateforme
- [DrillCellPanel Dashboard + navigation croisée](project_drillcellpanel_dashboard.md) — Panel permanent mois courant sur Dashboard ; clic ligne → Remboursements (URL params) + auto-ouverture modal + retour Dashboard avec drill restauré
- [Déploiement Docker — pièges et corrections](project_docker_deployment.md) — DATA_DIR env, fallthrough:false logos, nginx upstream name, Traefik @file, volume seed icons, deploy.sh
- [Sidebar overlay — couleur parasite au survol](feedback_sidebar_expand_overlay.md) — `background: transparent !important` + appearance:none sur .sidebar-expand-overlay et ses états hover/focus/active ; effet visuel via filter:brightness sur le logo uniquement
- [Safari mobile — viewport et dvh](feedback_safari_mobile_viewport.md) — Toujours doubler height:100vh + height:100dvh ; utiliser visualViewport.height au lieu de innerHeight en JS ; écouter visualViewport resize+scroll pour les menus fixed
- [Auth email — vérification, reset, pages layout](project_auth_email.md) — smtp_config table, email_verified sur users, tokens vérification/reset, pages ForgotPassword/ResetPassword/VerifyEmail, MonCompte badges
- [2FA — TOTP + email OTP + appareils de confiance](project_2fa.md) — totp_secret/totp_enabled sur users, tables two_fa_*, routes /2fa/setup|confirm-setup|disable|send-email-code|verify, trusted-devices CRUD, Login 3 étapes, TwoFASection + TrustedDevicesSection dans MonCompte
- [otplib ESM — API fonctionnelle uniquement](feedback_otplib_esm.md) — Pas d'export `authenticator` ; utiliser `generateSecret`, `generateURI`, `verifySync` importés nommément depuis 'otplib'
- [db/index.js — export default en double](feedback_dbindex_export_duplicate.md) — Un `export default db` existe en milieu de fichier (~ligne 942) ; toujours vérifier avec grep avant d'ajouter des migrations en fin de fichier
- [Suppression détenteur — réassignation au principal](project_suppression_detenteur.md) — DELETE investisseur réassigne investissements/depots_retraits/plateformes/comptes au principal avant delete ; toute nouvelle table CASCADE doit être ajoutée à la transaction
- [Export / Restore prod→dev](project_export_restore.md) — ZIP DB+assets, stockage serveur max 10, upload externe, restauration pending-restore+process.exit, job 3h00 (autoExport.js), ExportSection.jsx
- [SQLITE_CORRUPT — VACUUM INTO obligatoire](feedback_sqlite_vacuum_into.md) — db.backup() produit un fichier WAL-mode incomplet → toujours VACUUM INTO ; valider avec integrity_check avant d'écrire le pending-restore
- [Formule montant versé + style champs calculés](feedback_montant_verse_formula.md) — montantRembourse conditionnel isIndicatif (bruts si étranger/exonéré, netRecu si flat_tax) ; champs readonly : var(--surface-2)+var(--text-muted), jamais var(--bg-input-readonly)
- [Affichage projections — montants barrés & échéances non tenues](project_projections_display.md) — matchRembsAll somme tous les rembs du mois ; barré+réel si différence >0,01€ ; "✗ Échéance non tenue" rouge si passé sans paiement
- [Bug prêt différé — recalcul date_premiere_echeance](feedback_differe_date_calcul.md) — Passage à 'differe' doit toujours recalculer DPE si duree_mois présent ; validation dans submit ; label dynamique du champ
- [DrillCellPanel — filtrage lignes à 0](project_drillcell_filtrage_zero.md) — recusVisible/projetesVisible filtrent les lignes à 0 selon critères actifs ; compteur X/Y si lignes cachées
- [CSS tip-table + DrillCell mode consolidé](project_tip_css_drillcell_consolidation.md) — tip-th-name violet comme year ; bug consolidé : _ids[] dans merge, platIds dans onCellClick, plateforme_ids CSV au backend
- [Système de notifications](project_notifications.md) — Table notifications, 8 types (system/ticket_reply/info/team/success/warning/security/announcement), cloche topbar polling 30s, page /notifications, event notif:refresh pour rechargement immédiat
- [Audience des notifications — règle obligatoire](feedback_notifications_audience.md) — Toujours demander si la notif doit être vue par tous les admins (notifyAdmins) ou un utilisateur spécifique (notifyUser) avant d'implémenter
- [Page Communication — tickets & notifications](project_communication.md) — Architecture 3 volets, filtres chips multi-select, dropdowns portal, droits, jobs autoCleanNotifs + autoTicketStatus, pièges React (is_admin=0, msg-img-wrap)
@@ -0,0 +1,33 @@
---
name: feedback_adjustsimul
description: "Règles complètes pour adjustSimulForActuals — capital effectif, capital soldé, reprocess"
metadata:
node_type: memory
type: feedback
originSessionId: cce8aa12-fe3a-4aa6-a101-357b5ce867e8
---
`adjustSimulForActuals` dans `backend/src/utils/schedule.js` est le point central déclenché après chaque remboursement. Trois règles à toujours respecter :
**1. Capital effectif = initial + réinvestissements**
**Why:** Après l'ajout des réinvestissements (mai 2026), la projection s'affichait correctement après un réinvestissement, mais dès qu'un remboursement était saisi, `adjustSimulForActuals` recalculait sur `montant_investi` seul, effaçant l'impact.
**How to apply:** À chaque nouvelle fonctionnalité modifiant le capital réel (réinvestissement, abondement…), vérifier et adapter :
1. `adjustSimulForActuals` dans `schedule.js`
2. `syncInvestissementStatut` dans `remboursements.js`
3. Le bouton ↺ (recalcul manuel) dans `simul.js`
**2. Capital soldé (remainingCapital <= 0)**
**Why:** Sans correction, l'échéance courante gardait `capital_prevu = 0` pour les prêts in fine, et des lignes à 0,00 € restaient dans le tableau au lieu d'être supprimées.
**How to apply:** Quand `remainingCapital <= 0` :
- L'échéance courante (date_prevue <= last_date) reçoit `capital_prevu = capitalAtLastDate`
- Les échéances futures (date_prevue > last_date) sont **supprimées** (DELETE), pas mises à zéro
**3. Route /reprocess doit appeler adjustSimulForActuals**
**Why:** Sans cet appel, recalculer les champs fiscaux en masse ne mettait pas à jour les échéanciers.
**How to apply:** La route `POST /api/remboursements/reprocess` doit sélectionner `r.investissement_id`, collecter les IDs dans un `Set` pendant la transaction, puis itérer avec `adjustSimulForActuals(db, invId)` après le `transaction()()`.
@@ -0,0 +1,17 @@
---
name: feedback_adjustsimul_capital_solde
description: "Comportement de adjustSimulForActuals quand le capital est intégralement soldé avant l'échéance finale"
metadata:
node_type: memory
type: feedback
originSessionId: 054dabdd-2751-4898-95ba-26cc36f7e7e6
---
Quand `remainingCapital <= 0` dans `adjustSimulForActuals` (capital entièrement remboursé) :
1. **L'échéance courante** (date_prevue <= last_date, la plus récente) doit recevoir `capital_prevu = capitalAtLastDate` — c'est là que le capital a réellement été soldé.
2. **Les échéances futures** (date_prevue > last_date) doivent être **supprimées** (DELETE), pas mises à zéro.
**Why:** Avant correction, la fonction ne touchait pas l'échéance courante (capital_prevu restait à 0 pour les prêts in fine) et laissait des lignes à 0,00 € dans le tableau de projection au lieu de les supprimer.
**How to apply:** Toute modification de `adjustSimulForActuals` dans `backend/src/utils/schedule.js` doit préserver ce comportement dans le branchement `remainingCapital <= 0`.
@@ -0,0 +1,15 @@
---
name: feedback_dbindex_export_duplicate
description: db/index.js avait export default db en milieu de fichier — les migrations ajoutées après créaient un doublon
metadata:
node_type: memory
type: feedback
originSessionId: e9aff1a4-9c1c-4cab-b7ec-90146b832fb0
---
`backend/src/db/index.js` a un `export default db;` placé **au milieu du fichier** (ligne ~942, après un groupe de migrations). Lorsqu'on ajoute des migrations à la fin du fichier, Node lève `SyntaxError: Identifier '.default' has already been declared`.
**Règle :** Avant d'ajouter quoi que ce soit à la fin de `db/index.js`, vérifier avec `grep -n "export default db"` qu'il n'y a qu'une seule occurrence. Si deux occurrences existent, supprimer la première (celle en milieu de fichier) et garder uniquement celle tout à la fin.
**Why:** La première occurrence est un artefact d'une ancienne organisation du fichier ; les migrations continuent après elle.
**How to apply:** Vérification systématique après toute modification de ce fichier. Utiliser Python (pas sed) pour l'édition. [[feedback_file_truncation]]
@@ -0,0 +1,37 @@
---
name: feedback-differe-date-calcul
description: "Bug corrigé — recalcul date_premiere_echeance lors du passage au type 'differe' dans les formulaires investissement"
metadata:
node_type: memory
type: feedback
originSessionId: 43bfd5e1-0e82-496d-bdd2-437e92339323
---
Quand l'utilisateur change le type vers 'differe' alors que `duree_mois` est déjà renseigné, l'ancienne logique gardait la `date_premiere_echeance` issue du calcul in_fine (souscription + 1 mois) au lieu de recalculer pour un différé (souscription + durée totale). Seul `date_cible` était mis à jour.
**Why:** Pour un prêt différé, `date_premiere_echeance` = date d'échéance unique = souscription + duree_mois. Si l'utilisateur sélectionne 'differe' après avoir saisi la durée pour un in_fine, la date affichée était visuellement cohérente mais erronée.
**How to apply:** Dans les deux formulaires (Investissements.jsx et InvestissementDetail.jsx), le onChange du select `type_remb` doit, lors du passage à 'differe', **toujours recalculer** date_premiere_echeance si date_souscription ET duree_mois sont disponibles — et ne garder l'ancienne valeur qu'en fallback si duree_mois est absent. Ne pas inverser l'ordre des conditions (duree_mois check en premier).
```js
if (t === 'differe') {
if (form.date_souscription && form.duree_mois) {
const dpe = addMonthsFE(form.date_souscription, 1);
const cible = addMonthsFE(dpe, Number(form.duree_mois) - 1);
next.date_premiere_echeance = cible;
next.date_cible = cible;
} else if (form.date_premiere_echeance) {
next.date_cible = form.date_premiere_echeance;
}
}
```
Validation ajoutée dans les deux submit handlers :
```js
if (form.type_remb === 'differe' && !form.date_premiere_echeance) {
setErr("La date d'échéance est requise pour un prêt différé.");
return;
}
```
Label du champ rendu dynamique : "Date d'échéance (versement unique) *" quand type=differe, "Date 1ère échéance *" sinon.
@@ -0,0 +1,14 @@
---
name: feedback-express-route-order
description: Les routes statiques Express doivent être déclarées avant les routes paramétrées /:id
metadata:
node_type: memory
type: feedback
originSessionId: 264d200a-fea4-4903-b7c5-20dcc9ebfbd2
---
Les routes statiques comme `/export` ou `/import-zip` doivent toujours être enregistrées **avant** les routes paramétrées `/:id` dans Express. Sinon, Express interprète `/export` comme `/:id` avec `id = "export"`.
**Why:** Bug rencontré lors de l'ajout des routes export/import dans referentiel.js — les routes ont d'abord été ajoutées en fin de fichier (après `/:id`), ce qui les rendait inaccessibles.
**How to apply:** Quand on ajoute une nouvelle route statique à un router qui a déjà `/:id`, vérifier l'ordre. Utiliser un ancre précis pour l'insertion (ex : le commentaire `// ── GET /api/referentiel/:id ──`). Toujours vérifier avec `grep -n "router.get\|router.post"` après insertion.
@@ -0,0 +1,17 @@
---
name: feedback_fieldset_checkbox_css
description: Pièges CSS globaux — fieldset min-width et input width:100% sur checkboxes
metadata:
node_type: memory
type: feedback
originSessionId: 6ab3db95-bde5-486d-8d53-7e58134a13ca
---
Deux pièges CSS globaux dans ce projet :
1. **`fieldset` déborde de son conteneur** : le navigateur applique `min-width: min-content` par défaut aux fieldsets, ce qui permet au contenu de dépasser la modale ou le conteneur parent. Toujours ajouter `minWidth: 0` en style inline sur tout `<fieldset>`.
2. **Checkbox prend toute la largeur** : le CSS global `input { width: 100% }` s'applique aussi aux `input[type="checkbox"]`, les forçant sur toute la largeur et cassant l'alignement inline. Toujours ajouter `width: 'auto'` en style inline sur tout `input[type="checkbox"]`.
**Why:** Découvert lors de l'implémentation de TciModal (section Admin 2047). Plusieurs tentatives ratées avec flexbox avant d'identifier les causes racines.
**How to apply:** Systématiquement sur tout nouveau formulaire contenant des fieldsets ou des checkboxes dans ce projet.
@@ -0,0 +1,32 @@
---
name: troncature-et-corruption-fichiers-toujours-passer-par-python
description: Edit/Write tronquent les fichiers >~35 Ko ou introduisent des octets nuls ; sed -i tronque aussi les JSX ; toujours utiliser python3 via bash
metadata:
node_type: memory
type: feedback
originSessionId: cce8aa12-fe3a-4aa6-a101-357b5ce867e8
---
## ⚠️ RÈGLE ABSOLUE : **NE JAMAIS utiliser Edit, Write ou sed sur les fichiers backend ou frontend volumineux. TOUJOURS utiliser python3 via mcp__workspace__bash, sans exception.**
Les outils `Edit`, `Write` et `sed -i` peuvent corrompre les fichiers JSX/JS volumineux de deux façons :
1. **Troncature** : le contenu est coupé en cours d'écriture au-delà de ~35 Ko, sans avertissement.
2. **Octets nuls** : des `\x00` sont ajoutés en fin de fichier, causant une erreur Vite `Unexpected character ''`.
**Why:** Plusieurs fichiers clés ont été affectés (dashboard.js, fiscal2778.js, InvestissementDetail.jsx, DepotsRetraits.jsx). Un `sed -i` de remplacement de couleurs hex en session 2026-06-01 a tronqué Cerfa2561Preview.jsx à mi-fichier sans git pour récupérer. La troncature de `db/index.js` a aussi laissé une migration incomplète (`fiscalite_locale`) réparée manuellement.
**How to apply:**
- Toujours utiliser `python3` via `mcp__workspace__bash` pour écrire ou modifier des fichiers volumineux (pages JSX, routes backend complexes) — jamais `sed -i`
- Pattern sûr : `python3 -c "content = open(path).read(); content = content.replace(old, new); open(path,'w').write(content)"`
- Après chaque écriture, vérifier avec `python3 -c "open(path,'rb').read()[-50:].hex()"` — des `00` en fin signalent des octets nuls
- Nettoyer les octets nuls : `raw = open(path,'rb').read(); open(path,'wb').write(raw.rstrip(b'\x00'))`
- Vérifier les fichiers backend avec `node --check` après modification
- Pour le frontend, vérifier avec `tail -5` que les fermetures JSX sont correctes
- **InvestissementDetail.jsx spécifiquement** : la fin du fichier (menus contextuels) se tronque régulièrement. Chercher `grep -n "^ </Modal>"` et réécrire la queue via python3.
- **Troncature lors de passes multiples** : même avec python3, enchaîner plusieurs scripts indépendants lisant/écrivant le même fichier peut tronquer la fin (confirmé en session 2026-06-13 sur referentiel.js ~46 Ko). Le fichier semblait intact après chaque passe mais était corrompu en git. Pattern sûr : **un seul script Python qui lit depuis `git show <sha>:path` via `subprocess`, applique TOUS les patches en mémoire, écrit une seule fois, puis relit et vérifie `assert written == content` et `'export default router' in content`**. Après CHAQUE write, vérifier `tail -5`.
- **`content.replace()` avec caractères spéciaux** : les fichiers JSX contiennent des espaces insécables (U+202F), flèches et autres Unicode. Le `replace()` Python échoue silencieusement si l'old string ne matche pas exactement — préférer la manipulation par numéros de ligne (`lines[i] = new_line`).
- **Cerfa2778Preview.jsx** : particulièrement sensible aux troncatures (U+202F dans les templates). Toujours travailler par index de lignes et vérifier `tail -5` après chaque écriture.
- **DepotsRetraits.jsx et Remboursements.jsx** : troncatures en session 2026-05-31 lors de l'ajout des TrendBadge — reconstruire depuis les patterns des autres pages.
- Si le backend refuse de démarrer avec "Unexpected end of input" : `xxd | tail -4` pour diagnostiquer, puis reconstruire via python3.
- **auth.js et admin.js** : troncatures confirmées en session 2026-06-15 lors d'instrumentation via Edit (python3 replace). Pour ces fichiers, utiliser exclusivement python3 en mode binaire (`open(path,'rb')` / `open(path,'wb')`) pour éviter les erreurs d'encodage UTF-8 sur les accents. Restaurer le `export default router;` final en dernier si manquant.
- **Récupération depuis git** : `git checkout HEAD -- file` est bloqué ("Operation not permitted") dans le sandbox. Pattern de récupération : `git show HEAD:path/to/file > /tmp/restored.js && python3 -c "import shutil; shutil.copy('/tmp/restored.js', 'path/to/file')"`. Confirmer avec `wc -l`. Incident admin.js 2026-06-16 : tronqué à 414 lignes (sur 672), récupéré via ce pattern.
@@ -0,0 +1,24 @@
---
name: Pattern investisseurForPlat
description: Dans tout formulaire avec un select plateforme, synchroniser automatiquement le détenteur quand la plateforme change
type: feedback
originSessionId: b87fd892-4ff2-4b8b-b3e6-f0f92e303c8e
---
Quand un formulaire propose un select "Plateforme" et un select "Détenteur", le choix de la plateforme doit automatiquement mettre à jour le détenteur avec `investisseur_id` de la plateforme sélectionnée.
**Why:** Sans cette synchronisation, l'utilisateur doit changer manuellement le détenteur après avoir choisi la plateforme — comportement déroutant signalé sur DepotsRetraits et Investissements.
**How to apply:** Dans chaque page concernée, ajouter ce helper (après `defaultInvestisseurId`) :
```js
const investisseurForPlat = (platId) => {
const plat = plats.find(p => String(p.id) === String(platId));
return plat?.investisseur_id ? String(plat.investisseur_id) : defaultInvestisseurId();
};
```
L'utiliser dans :
1. Le `onChange` du select plateforme → `setForm(f => ({ ...f, plateforme_id: platId, investisseur_id: investisseurForPlat(platId) }))`
2. Toutes les initialisations du formulaire (`openNew`, les `useEffect` qui ouvrent la modal avec `?new=1`)
Pages déjà corrigées : `DepotsRetraits.jsx`, `Investissements.jsx`.
@@ -0,0 +1,20 @@
---
name: feedback-menu-actions-format
description: "Tout nouveau tableau doit utiliser le menu ⋮ avec icônes SVG + label, pas de boutons inline"
metadata:
node_type: memory
type: feedback
originSessionId: 2fef7a75-e0f3-4f7d-9c80-79c3be16af03
---
Toujours utiliser le menu contextuel ⋮ avec icônes pour les actions sur les lignes de tableau — jamais de boutons "Modifier" / "Supprimer" inline.
**Why:** Olivier l'a explicitement demandé après avoir vu des boutons inline dans le tableau des catégories/secteurs. Le format avec icônes SVG (IconEdit, IconTrash, etc.) a été validé comme référence.
**How to apply:** Dès qu'un nouveau tableau est construit avec des actions par ligne, appliquer le pattern complet de [[project_menu_actions_tables]] :
- Bouton ⋮ dans la dernière colonne (pas de texte, juste le symbole)
- Menu `position: fixed` avec backdrop
- Items : `{ icon: <IconXxx />, label: '…', onClick, disabled?, color? }`
- Rendu : `display: flex, gap: 8` avec `<span style={{ opacity: 0.7, display: 'flex' }}>{icon}</span>`
- Hover : `onMouseEnter/Leave` pour background `var(--surface-2)`
- Fermeture au scroll via `useEffect`
@@ -0,0 +1,31 @@
---
name: feedback_montant_verse_formula
description: "Formule du montant versé dans les formulaires remboursement — conditionnel isIndicatif, style champs calculés"
metadata:
node_type: memory
type: feedback
originSessionId: 1035faf3-2491-41aa-85f7-cfaabbeda8da
---
Le champ "Montant versé" dans les formulaires remboursement doit être conditionnel selon `isIndicatif` :
```js
const montantRembourse = isIndicatif
? round2(capital + cashback + interets_bruts) // plateforme étrangère ou exonéré : brut versé intégralement
: netRecu; // plateforme française flat_tax : net après prélèvements
```
`isIndicatif = isExonere || currentPlat?.fiscalite !== 'flat_tax'`
**Pourquoi :** bug découvert en juin 2026 — `InvestissementDetail.jsx` utilisait toujours `interets_bruts` au lieu de brancher sur `isIndicatif`, contrairement à `Remboursements.jsx` qui avait le bon pattern. Pour une plateforme française, la flat_tax est prélevée à la source, donc le versement réel est diminué des prélèvements.
**Comment appliquer :** pour tout nouveau formulaire remboursement, vérifier que `montantRembourse`/`netRecu` est calculé avec cette logique conditionnelle. `Remboursements.jsx` (lignes ~1422-1424) est la référence correcte.
## Style champs calculés / en lecture seule
Utiliser systématiquement :
```jsx
style={{ background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' }}
```
Ne **jamais** utiliser `var(--bg-input-readonly)` — cette variable CSS n'est pas définie dans le thème.
@@ -0,0 +1,25 @@
---
name: Pattern multiDetenteur pour les listes de plateformes
description: Le nom du détenteur ne s'affiche dans les selects plateforme que si plusieurs détenteurs distincts existent
type: feedback
originSessionId: 680bfd99-577f-454e-b5b8-ea58460277f9
---
Dans tous les composants affichant une liste déroulante de plateformes, le détenteur (`investisseur_nom`) est affiché de façon conditionnelle — uniquement si au moins deux détenteurs distincts sont déclarés sur l'ensemble des plateformes.
**Pattern à appliquer :**
```js
// Calculé une fois dans le composant, depuis le tableau de plateformes
const multiDetenteur = new Set(plats.map(p => p.investisseur_id)).size > 1;
// Dans le JSX
{plats.map(p => (
<option key={p.id} value={p.id}>
{p.nom}{multiDetenteur && p.investisseur_nom ? `${p.investisseur_nom}` : ''}
</option>
))}
```
**Why:** Si toutes les plateformes appartiennent au même détenteur, afficher le nom en permanence est du bruit inutile. La distinction n'a de sens que dans un contexte multi-membres/multi-entités.
**How to apply:** Chaque page/composant qui charge `plats` ou `plateformes` et affiche un select doit déclarer `multiDetenteur` et l'appliquer sur toutes ses listes de plateformes — filtres ET modales. Pages concernées : `Investissements.jsx`, `InvestissementDetail.jsx`, `DepotsRetraits.jsx`, `Remboursements.jsx`, `Imports.jsx`, `Settings.jsx` (composants `ImportsSection` et `NotationPanel`).
@@ -0,0 +1,17 @@
---
name: feedback-notifications-audience
description: "Avant de créer une notification, toujours demander si elle doit être visible par tous les admins ou seulement par un utilisateur spécifique"
metadata:
node_type: memory
type: feedback
originSessionId: 19a3ce4d-7c1a-418b-9ca2-65b6157249db
---
Avant d'implémenter toute nouvelle notification, toujours poser la question : **cette notification doit-elle être vue par l'ensemble des administrateurs, ou seulement par un utilisateur spécifique ?**
**Why:** Le système stocke une notification par `user_id`. `notifyAdmins()` insère une ligne par admin. Si la notification n'est pas destinée à tous les admins, il ne faut pas utiliser `notifyAdmins()` — il faut cibler le `user_id` concerné avec `notifyUser()`.
**How to apply:**
- Réponse "tous les admins" → utiliser `notifyAdmins(title, body, link)`
- Réponse "non / utilisateur spécifique" → utiliser `notifyUser(userId, title, body, link)`
- Ne jamais présumer l'audience : toujours demander avant de coder.
@@ -0,0 +1,11 @@
---
name: onClick direct sur une fonction avec paramètre par défaut
description: Utiliser onClick={fn} passe l'événement React comme premier argument, écrasant les valeurs par défaut ; toujours utiliser onClick={() => fn()}
type: feedback
originSessionId: f8882910-b8d1-4a87-ab0b-2962aa6aad7b
---
Quand une fonction a des paramètres par défaut (ex. `openReinvModal(tab = 'manuel')`), l'écrire directement dans un handler React (`onClick={openReinvModal}`) fait que React passe l'objet événement comme premier argument, écrasant la valeur par défaut.
**Why:** Bug découvert sur `openReinvModal` — le modal s'ouvrait parfois sur l'onglet "Automatique" au lieu de "Manuel" parce que `tab` recevait un SyntheticEvent au lieu de `'manuel'`.
**How to apply:** Toujours envelopper dans une lambda : `onClick={() => fn()}` ou `onClick={() => fn(arg)}`. Ne jamais écrire `onClick={fn}` si `fn` a des paramètres avec valeur par défaut qu'on ne veut pas écraser.
@@ -0,0 +1,22 @@
---
name: feedback_otplib_esm
description: "otplib v12+ n'exporte pas authenticator — utiliser l'API fonctionnelle ESM"
metadata:
node_type: memory
type: feedback
originSessionId: e9aff1a4-9c1c-4cab-b7ec-90146b832fb0
---
`otplib` (version installée dans le projet) n'expose pas de named export `authenticator` en ESM. L'import `{ authenticator } from 'otplib'` lève `SyntaxError: does not provide an export named 'authenticator'`. Même `createRequire` échoue car le package a changé d'API.
**Utiliser l'API fonctionnelle** directement :
```js
import { generateSecret as totpGenerateSecret, generateURI as totpGenerateURI, verifySync as totpVerifySync } from 'otplib';
const secret = totpGenerateSecret();
const uri = totpGenerateURI({ issuer, label: email, secret });
const valid = totpVerifySync({ token: code, secret, strategy: 'totp' });
```
**Why:** Le package a migré vers une API fonctionnelle (v12+), l'ancienne interface orientée objet (`authenticator`) a été supprimée.
**How to apply:** Toujours utiliser ces 3 imports nommés ESM pour toute fonctionnalité TOTP.
@@ -0,0 +1,20 @@
---
name: feedback-pageicon-topbar
description: Ne jamais modifier display ou ajouter de wrapper dans .topbar pour les icônes de titre de page
metadata:
node_type: memory
type: feedback
originSessionId: 743b204f-7d6b-45ed-b91c-387060b5941e
---
Ne pas modifier la structure flex du `.topbar` pour y insérer des icônes.
**Why:** `.topbar` est `display: flex; justify-content: space-between`. Ajouter un wrapper div ou mettre `display: flex` sur le `h2` change le nombre/type de flex items, ce qui casse le layout en cascade (KPIs `dr-kpi-row`, tabs `dr-tabs`, et même `account-layout` sur d'autres pages non modifiées).
**How to apply:** Insérer l'icône comme `<img>` inline directement dans le `h2` existant, sans toucher à sa structure ni à son `display`. Utiliser `style={{ display: 'inline', verticalAlign: 'middle', marginRight: 10 }}` sur l'img. Le `h2` reste un élément block normal, l'img est du contenu inline — zéro interaction avec flex/grid.
```jsx
<h2><PageIcon name="dashboard" />Tableau de bord</h2>
```
Le composant `PageIcon` est dans `frontend/src/components/PageIcon.jsx` avec un cache module-level (un seul appel `/api/icons` pour toutes les pages). Taille par défaut : 40px.
@@ -0,0 +1,22 @@
---
name: feedback_projection_no_double_count
description: Pas de guard real===0 dans buildCellValue — le backend filtre déjà les projections par NOT EXISTS par investissement
metadata:
node_type: memory
type: feedback
originSessionId: b8b23026-381b-4ec2-b150-02080d5f4720
---
Le backend (`dashboard/interets-par-plateforme`) filtre les projections avec `NOT EXISTS` **au niveau investissement** : `projections[mois]` ne contient que les projets qui n'ont pas encore reçu de remboursement réel ce mois. Il n'y a donc pas de risque de double-comptage.
```js
// CORRECT — le filtre NOT EXISTS backend garantit l'absence de doublon
if (showProjected && proj) { /* ajouter proj */ }
// INCORRECT — bloque l'affichage des projections sur les projets non encore remboursés
// dès qu'un autre projet de la même plateforme a été remboursé ce mois
if (showProjected && proj && real === 0) { /* ajouter proj */ }
```
**Why:** Maclear en juin 2026 : 7 projets remboursés (real > 0) + 9 projets encore en attente (dans projections). Le guard `real === 0` masquait à tort les 9 projections.
**How to apply:** Ne PAS ajouter `real === 0` dans `buildValue` / `buildCellValue`. Corrigé dans `Plateformes.jsx` et `TableauInteretsPlateforme.jsx`.
@@ -0,0 +1,14 @@
---
name: feedback-put-payload-minimal
description: "Ne jamais spread l'objet inv pour un PUT investissements — construire un payload minimal champ par champ"
metadata:
node_type: memory
type: feedback
originSessionId: 06bf2c9d-6350-45bd-a65f-4399f1218408
---
Ne jamais faire `{ ...inv, champModifie: valeur }` pour un `PUT /investissements/:id`.
**Why:** L'objet `inv` côté frontend contient des champs extra (`simul`, `remboursements`, `auto_reinvest`, `fiscalite_override`, `plateforme_nom`, etc.) absents du schéma Zod backend. Certains ont des valeurs invalides pour le schéma (ex. arrays, objets imbriqués), ce qui provoque une "Validation error" même si le champ modifié est correct.
**How to apply:** Toujours construire le payload explicitement avec uniquement les champs attendus par le schéma Zod de la route (voir `backend/src/routes/investissements.js``const Schema = z.object({...})`). Utiliser `|| undefined` pour les champs optionnels string et `?? null` pour les champs nullable.
@@ -0,0 +1,14 @@
---
name: adjustSimulForActuals doit tenir compte des réinvestissements
description: Bug constaté — après saisie d'un remboursement, la projection ignorait les réinvestissements et revenait au capital initial
type: feedback
originSessionId: 680bfd99-577f-454e-b5b8-ea58460277f9
---
Ne jamais oublier qu'`adjustSimulForActuals` est le point central déclenché après chaque remboursement. Si une nouvelle notion modifie le capital effectif d'un prêt, cette fonction doit en être informée.
**Why:** En mai 2026, après l'ajout des réinvestissements, la projection s'affichait correctement juste après un réinvestissement (car `generateSimulWithReinvestissements` était appelée). Mais dès qu'un remboursement était saisi, `adjustSimulForActuals` recalculait sur `montant_investi` seul, effaçant l'impact du réinvestissement.
**How to apply:** À chaque fois qu'une nouvelle fonctionnalité modifie le capital réel d'un prêt (réinvestissement, abondement, etc.), vérifier et adapter les trois points suivants :
1. `adjustSimulForActuals` dans `schedule.js`
2. `syncInvestissementStatut` dans `remboursements.js`
3. Le bouton ↺ (recalcul manuel) dans `simul.js` — il passe par `adjustSimulForActuals`, donc hérite automatiquement des corrections.
@@ -0,0 +1,21 @@
---
name: feedback_remb_methode_fallback
description: openRembFromSimul et openNewRemb doivent utiliser inv.methode_remboursement comme fallback quand la plateforme est choix_investisseur
metadata:
node_type: memory
type: feedback
originSessionId: 5682d634-7345-40a8-8184-f617d1dd8444
---
Quand `plat.methode_remboursement === 'choix_investisseur'`, les fonctions `openRembFromSimul` et `openNewRemb` dans **InvestissementDetail.jsx** doivent utiliser `inv.methode_remboursement` comme valeur par défaut — pas `'portefeuille'`.
Pattern correct :
```js
const methode = plat && plat.methode_remboursement !== 'choix_investisseur'
? plat.methode_remboursement
: (inv?.methode_remboursement || 'portefeuille');
```
**Why:** Sans ce correctif, tous les remboursements saisis depuis la fiche détail d'un investissement en `compte_courant` étaient enregistrés en `portefeuille`, ce qui empêchait la création du retrait automatique dans `depots_retraits`.
**How to apply:** Vérifier ce pattern à chaque fois qu'on modifie ou crée une fonction d'ouverture du formulaire de remboursement. Voir [[project_methode_remboursement_investissement]].
@@ -0,0 +1,14 @@
---
name: feedback_reprocess_adjustsimul
description: La route /reprocess doit appeler adjustSimulForActuals après la transaction fiscale
metadata:
node_type: memory
type: feedback
originSessionId: 054dabdd-2751-4898-95ba-26cc36f7e7e6
---
La route `POST /api/remboursements/reprocess` doit appeler `adjustSimulForActuals(db, invId)` pour chaque `investissement_id` distinct touché, après la transaction de mise à jour fiscale.
**Why:** Sans cet appel, recalculer les champs fiscaux en masse ne mettait pas à jour les tableaux de projection — les remboursements de capital n'étaient donc plus reflétés dans les échéanciers après un reprocess.
**How to apply:** La requête SQL du reprocess doit sélectionner `r.investissement_id`. Collecter les IDs dans un `Set` pendant la transaction, puis itérer avec `adjustSimulForActuals` après le `transaction()()`.
@@ -0,0 +1,28 @@
---
name: feedback-result-banner
description: Toute bannière succès/erreur doit utiliser ResultBanner avec × à droite et auto-dismiss 4s
metadata:
node_type: memory
type: feedback
originSessionId: 264d200a-fea4-4903-b7c5-20dcc9ebfbd2
---
Utiliser le composant `ResultBanner` (`frontend/src/components/ResultBanner.jsx`) pour **toute** bannière de résultat succès/erreur dans l'app.
**Règle** : jamais de div inline avec `result.ok ? 'rgba...'`. Toujours `<ResultBanner result={result} onDismiss={() => setResult(null)} />`.
**Comportement garanti par le composant** :
- × positionné à l'extrême droite (`justifyContent: space-between`)
- Auto-dismiss après 4 secondes (paramètre `delay` configurable)
- Callback `onDismiss` appelé à la fermeture manuelle ET automatique
**Format du state `result`** : toujours `{ ok: boolean, msg: string }`. Convertir les structures complexes (ex: résultats d'import `{ inserted, total, skipped, errors }`) en message texte au moment du `setResult`.
**Redirect après dismiss** : si la section doit rediriger après fermeture, passer le navigate dans `onDismiss` :
```jsx
onDismiss={() => { setResult(null); navigate('/admin/plateformes?section=referentiel'); }}
```
**Why:** Demande explicite d'Olivier — cohérence visuelle sur toute l'app (× à droite, disparition automatique).
**How to apply:** Vérifier systématiquement `setResult` + affichage résultat dans tout nouveau composant. Si la structure du result n'est pas `{ ok, msg }`, normaliser au `setResult`.
@@ -0,0 +1,35 @@
---
name: feedback-safari-mobile-viewport
description: "Safari mobile — utiliser 100dvh et visualViewport pour éviter les décalages liés à la barre d'adresse"
metadata:
node_type: memory
type: feedback
originSessionId: 21fbd9de-5545-4f5d-8809-a41863f7cdb6
---
Sur Safari iOS (iPhone/iPad), `100vh` inclut la barre d'adresse du navigateur, ce qui pousse les éléments en bas de page (sidebar, UserMenu) hors de la zone visible.
**Why:** Safari mobile calcule `100vh` par rapport à la hauteur totale incluant le chrome du navigateur, pas la zone réellement visible. `window.innerHeight` a le même problème pour les calculs JS.
**How to apply — CSS :**
Toujours doubler les déclarations de hauteur plein-écran :
```css
height: 100vh;
height: 100dvh; /* remplace 100vh sur Safari 15.4+ */
min-height: 100vh;
min-height: 100dvh;
```
`100dvh` (dynamic viewport height) s'ajuste dynamiquement quand la barre d'adresse apparaît/disparaît.
**How to apply — JS (calculs de position type `position: fixed`) :**
Remplacer `window.innerHeight` par `window.visualViewport.height` :
```js
const vh = window.visualViewport ? window.visualViewport.height : window.innerHeight;
```
Et écouter les événements du visualViewport si le menu doit rester calé pendant le scroll :
```js
window.visualViewport.addEventListener('resize', computePosition);
window.visualViewport.addEventListener('scroll', computePosition);
```
Appliqué sur : `.sidebar` (height), `.app-shell` (min-height), `UserMenu.computePosition`.
@@ -0,0 +1,14 @@
---
name: feedback-sed-truncation
description: Ne jamais utiliser sed pour remplacer des couleurs dans les fichiers JSX — risque de troncature
metadata:
node_type: memory
type: feedback
originSessionId: 0922bde7-84ed-45dc-b233-50bf0be5ad3e
---
Ne jamais utiliser `sed` pour des remplacements de couleurs (ou autres chaînes) dans les fichiers JSX volumineux.
**Why:** En session 2026-06-01, un `sed -i` de remplacement de couleurs hex a tronqué `Cerfa2561Preview.jsx` à 271 lignes (sur ~443), coupant le fichier en plein milieu d'une chaîne JSX. Il n'y avait pas de git pour récupérer, ce qui a nécessité une reconstruction manuelle depuis l'historique de la conversation.
**How to apply:** Pour les remplacements en masse dans les fichiers JSX, toujours utiliser `python3` via bash avec lecture/écriture du fichier complet, ou l'outil `Edit` avec `replace_all: true` pour des remplacements ciblés. Vérifier systématiquement `wc -l` et `tail | xxd` après tout remplacement en masse.
@@ -0,0 +1,21 @@
---
name: feedback-sidebar-expand-overlay
description: Fix couleur parasite au survol du logo en sidebar réduite — utiliser background:transparent !important sur .sidebar-expand-overlay
metadata:
node_type: memory
type: feedback
originSessionId: 21fbd9de-5545-4f5d-8809-a41863f7cdb6
---
Le bouton `.sidebar-expand-overlay` (position: absolute; inset: 0) provoque une couleur parasite au survol même avec `background: none`, car le navigateur applique ses propres styles par défaut sur `<button>:hover`.
**Why:** Le bouton couvre toute la zone `.sidebar-brand` en mode collapsed. Les styles par défaut du navigateur s'appliquent sur le `:hover` du `<button>` malgré `background: none`.
**How to apply:** Toujours ajouter sur `.sidebar-expand-overlay` :
```css
background: transparent !important;
-webkit-appearance: none; appearance: none;
outline: none;
```
Et les mêmes règles sur `:hover`, `:focus`, `:active`.
L'effet visuel passe uniquement par `filter: brightness(1.18)` sur `.sidebar-brand-logo` au hover.
@@ -0,0 +1,39 @@
---
name: feedback_sqlite_vacuum_into
description: Ne jamais utiliser db.backup() pour les exports — toujours VACUUM INTO ; valider avec integrity_check avant pending-restore
metadata:
node_type: memory
type: feedback
originSessionId: 5cf6a868-7a3f-44e5-a0f8-0499f84b1afc
---
Ne jamais utiliser `db.backup()` pour créer une copie de la DB destinée à être lue comme fichier brut.
**Why:** `db.backup()` (better-sqlite3) copie les pages SQLite, y compris le header qui indique "WAL mode". Le fichier résultant est techniquement en WAL mode mais sans fichier WAL — il est incomplet et provoque `SQLITE_CORRUPT` lors de la réouverture. Incident prod constaté le 2026-06-16 : serveur en boucle de crash après restauration.
**How to apply:**
Pour tout export (route export-full, job autoExport, backup de sécurité dans la route restore) :
```js
// ✅ Correct — produit un fichier DELETE-mode propre, sans WAL
db.exec(`VACUUM INTO '${tmpDb.replace(/'/g, "''")}'`);
const dbData = fs.readFileSync(tmpDb);
```
Avant d'écrire le fichier `.pending-restore`, toujours valider :
```js
import Database from 'better-sqlite3';
const tmpValidate = path.join(os.tmpdir(), `cl-validate-${Date.now()}.db`);
let testDb;
try {
fs.writeFileSync(tmpValidate, dbEntry.data);
testDb = new Database(tmpValidate, { readonly: true });
const check = testDb.pragma('integrity_check');
if (!check || check[0]?.integrity_check !== 'ok') throw new HttpError(500, '...');
} finally {
if (testDb) try { testDb.close(); } catch {}
if (fs.existsSync(tmpValidate)) fs.unlinkSync(tmpValidate);
}
```
Cette validation est déjà en place dans `backend/src/routes/admin.js` (route restore).
@@ -0,0 +1,25 @@
---
name: feedback-timezone-lastday
description: "Ne jamais utiliser .toISOString().split('T')[0] pour calculer une date locale — provoque un décalage d'un jour en UTC+1"
metadata:
node_type: memory
type: feedback
originSessionId: 2aaa93e3-eec2-479c-892a-d5ce8371a99f
---
Ne jamais calculer le dernier jour d'un mois avec `.toISOString().split('T')[0]`.
**Why:** `new Date(annee, m, 0).toISOString()` convertit en UTC. En France (UTC+1 en hiver), minuit local le 31 déc. = 23h le 30 déc. UTC → la chaîne retournée est `"2024-12-30"` au lieu de `"2024-12-31"`. Les investissements souscrits le dernier jour du mois sont alors exclus à tort des calculs de capital mensuel dans le dashboard.
**How to apply:** Toujours utiliser les méthodes locales pour construire une chaîne de date :
```js
// ❌ À éviter
const lastDay = new Date(annee, m, 0).toISOString().split('T')[0];
// ✅ Correct
const _d = new Date(annee, m, 0);
const lastDay = `${_d.getFullYear()}-${String(_d.getMonth() + 1).padStart(2, '0')}-${String(_d.getDate()).padStart(2, '0')}`;
```
Appliquer ce pattern partout où une date locale est formatée en chaîne `YYYY-MM-DD` à partir d'un objet `Date`. Déjà corrigé dans `backend/src/routes/dashboard.js` (capitalMensuel de `/api/dashboard/interets-par-plateforme`).
@@ -0,0 +1,41 @@
---
name: feedback_tip_td_closed
description: Règle de grisage des cellules hors-période dans les tableaux mensuels tip-table
metadata:
node_type: memory
type: feedback
originSessionId: b8b23026-381b-4ec2-b150-02080d5f4720
---
Toute nouvelle page ou composant utilisant un tableau `tip-table` avec des données par investissement doit griser les cellules hors-période via la classe `.tip-td-closed`.
**Règle :** Rendre `<td className="tip-td-closed" />` (sans contenu, sans tiret) quand :
- Le mois est **avant** le début de l'investissement (date_souscription ou premier paiement)
- Le mois est **après** le dernier remboursement pour les prêts `statut === 'rembourse'`
**Pattern de détection :**
```js
// Avant (via date_souscription)
const subYear = Number(inv.date_souscription?.slice(0, 4));
const subMo = Number(inv.date_souscription?.slice(5, 7)) - 1;
const isBefore = subYear > displayYear || (subYear === displayYear && mi < subMo);
// Après (via lastRembDateMap)
const isAfter = inv.statut === 'rembourse' && lastDate && (() => {
const lastYear = Number(lastDate.slice(0, 4));
const lastMo = Number(lastDate.slice(5, 7)) - 1;
if (lastYear < displayYear) return true;
if (lastYear === displayYear) return mi > lastMo;
return false;
})();
```
**CSS défini dans styles.css :**
```css
.tip-td-closed { background: var(--surface-2) !important; }
.tip-td-closed.tip-col-current { background: rgba(109,40,217,0.045) !important; }
```
La surbrillance du mois courant reste prioritaire grâce au double sélecteur.
**Why:** Demande utilisateur pour indiquer visuellement qu'il n'y a rien à attendre sur ces mois.
**How to apply:** Appliquer dans tout nouveau tableau mensuel tip-table dès sa création. Déjà présent dans Plateformes.jsx (onglets Remboursements + Dépôts/Retraits) et InvMensuelTable.jsx.
@@ -0,0 +1,20 @@
---
name: feedback-tiptable-padding
description: "Le composant TableauInteretsPlateforme doit être enveloppé dans un div padding 0 24px dans chaque page qui l'utilise"
metadata:
node_type: memory
type: feedback
originSessionId: 752b3578-1999-4431-9dd9-73297a9baaf4
---
`TableauInteretsPlateforme` ne gère pas son propre padding horizontal. Chaque page qui l'intègre doit l'envelopper :
```jsx
<div style={{ padding: '0 24px' }}>
<TableauInteretsPlateforme />
</div>
```
**Why:** Le composant touche les bords sans ce wrapper. Corrigé dans `Dashboard.jsx` et `Remboursements.jsx` (session 2026-05-31).
**How to apply:** Si le tableau est intégré dans une nouvelle page, ajouter systématiquement ce wrapper.
@@ -0,0 +1,14 @@
---
name: feedback-usermenu-portal
description: "UserMenu popup et sous-panel doivent être rendus via createPortal(…, document.body) pour éviter les problèmes de stacking context avec la sidebar sticky"
metadata:
node_type: memory
type: feedback
originSessionId: 752b3578-1999-4431-9dd9-73297a9baaf4
---
Le popup `.user-menu-popup` et le sous-panel `.user-menu-subpanel` de `UserMenu.jsx` sont rendus via `createPortal(…, document.body)`.
**Why:** La sidebar a `position: sticky` qui crée un stacking context. Même avec `position: fixed` et `z-index: 200`, les popups restaient derrière les tableaux `.tip-table` dont les cellules ont `position: sticky; z-index: 1/2`. Le portail sort complètement du DOM de la sidebar, éliminant tout conflit de stacking context.
**How to apply:** Toujours utiliser `createPortal` (importé depuis `react-dom`) pour tout popup/dropdown rendu dans la sidebar ou dans un conteneur avec `overflow` ou `position` non-statique.
@@ -0,0 +1,57 @@
---
name: project_2778sd
description: "Fonctionnalité déclaration mensuelle 2778-SD (PFO sur revenus étrangers) — matrice, simulation CERFA, report 2042, taux dynamiques, paramètre pfoAssujetti"
metadata:
node_type: memory
type: project
originSessionId: 984f7812-9585-45f1-be5b-5037de201a05
---
Onglet **2778-SD** dans la page Fiscalité (TaxReport), conditionné par la préférence `pfoAssujetti`.
**Architecture :**
- Backend : `GET /api/taxreport/2778` — matrice mensuelle par plateforme étrangère (domiciliation ≠ france), base = `interets_bruts_avant_local` si > 0 sinon `interets_bruts`
- Frontend : `Cerfa2778Preview.jsx` — deux sous-vues : Matrice mensuelle + Simulation CERFA
- TaxReport.jsx : onglet 2778-SD visible uniquement si `pfoAssujetti === true`
**Taux dynamiques :**
- Table `taux_pfu` enrichie : colonnes `csg`, `crds`, `solidarite` (migration dans index.js)
- Taux 2021-2025 : CSG 9,2 % | CRDS 0,5 % | Solidarité 7,5 % → PS total 17,2 % → PFU 30 %
- Taux 2026+ : CSG 10,6 % | CRDS 0,5 % | Solidarité 7,5 % → PS total 18,6 % → PFU 31,4 %
- `pfu.js` calcule `prelev_sociaux` et `pfu_total` automatiquement depuis les 3 composantes
- `getRatesForYear(annee, pfuList)` dans Cerfa2778Preview : trouve l'année exacte ou la plus proche
**Calcul cases CERFA (notre cas = placement revenu fixe) :**
- BA = intérêts bruts totaux (arrondi à l'euro)
- IA = BA × 12,8 % (PFO, acompte non libératoire)
- PQ = BA × CSG%, PV = BA × CRDS%, PF1 = PQ+PV, PG1 = BA × solidarité%
- PU = PF1, PK = PG1, QR = IA + PU + PK (montant à payer)
**Report annuel 2042 :**
- Case 2TR = total BA annuel (intérêts bruts)
- Case 2BH = idem 2TR (éviter double imposition PS)
- Case 2CK = total IA annuel (acompte déjà versé, imputable sur IR définitif)
**Paramètre pfoAssujetti :**
- Stocké dans `user_preferences` (clé `pfo_assujetti`) et `localStorage` (`cl_pfo_assujetti`)
- Exposé via `UiContext` : `pfoAssujetti`, `setPfoAssujetti`
- UI dans Settings > Flat Tax (PFU) : section "Fiscalité des plateformes étrangères", bloc accordéon avec toggle (masqué par défaut)
- Seuils : 25 000 € (célibataire) / 50 000 € (couple) de RFR de l'avant-dernière année
**Sélection plateformes persistée :** `localStorage` clé `cl_2778_excluded_plats` (array d'IDs exclus)
**Structure tableau CerfaBlock (Cerfa2778Preview.jsx) :**
- Grille 6 colonnes : `1fr 90px 90px 90px 90px 90px` → Libellé | Base imposable (BA) | Taux | Case | Montant | (sans titre, colonne Case SUP)
- Composant `Row` : props `label, code, value, highlight, note, caseSup, base, taux, noUnit``noUnit` supprime le symbole €, `whiteSpace: pre-line` sur le label pour les sauts de ligne
- Prop `mois` et `annee` passées à CerfaBlock depuis le parent pour afficher "Mai 2026" dans Montant
- Sections : Page de garde (page 1) | Prélèvement forfaitaire obligatoire non libératoire (page 2) | Prélèvements sociaux (page 3) | Totaux à reporter (page 4)
- Ligne BA+IA fusionnée : libellé sur 2 lignes (séparateur `\n`), code=IA, value=IA, taux=pfo%
- Ligne "Mois concerné" : code vide, value=`${MOIS_LABELS[mois]} ${annee}`, noUnit=true
**Settings > Flat Tax (PFU) :**
- Table : 7 colonnes (Année, PFU total, IR, PS total, └> dont CSG, └> dont CRDS, └> dont Solidarité)
- Fond indigo léger `rgba(99,102,241,0.06)` sur les 3 colonnes de décomposition
- Formulaires ajout/édition : saisir IR + CSG + CRDS + Solidarité → PFU total calculé automatiquement
**Why:** Obligation légale déclaration mensuelle PFO pour revenus d'intérêts de plateformes étrangères.
**How to apply:** Toujours vérifier `pfoAssujetti` avant d'afficher quoi que ce soit lié à la 2778-SD. Les taux viennent de `/api/pfu`, jamais hardcodés.
@@ -0,0 +1,32 @@
---
name: project_2fa
description: "Implémentation complète du 2FA (TOTP + email OTP) — DB, backend, frontend Login et MonCompte"
metadata:
node_type: memory
type: project
originSessionId: e9aff1a4-9c1c-4cab-b7ec-90146b832fb0
---
Fonctionnalité 2FA ajoutée en session juin 2026.
**DB (db/index.js)** — colonnes sur `users` : `totp_secret TEXT`, `totp_enabled INTEGER DEFAULT 0`. Tables : `two_fa_sessions` (session temporaire entre /login et /2fa/verify, 5 min), `two_fa_email_codes` (OTP email 6 chiffres, 5 min), `two_fa_trusted_devices` (appareils de confiance 30 jours, avec `user_agent` et `ip_address`).
**Backend (auth.js)** — routes :
- `POST /login` : si `totp_enabled` → vérifie `deviceToken` (header `X-Device-Token`), sinon retourne `{ requires2FA: true, sessionToken }`
- `GET /2fa/setup` : génère secret TOTP + QR code base64 (lib `otplib` API fonctionnelle)
- `POST /2fa/confirm-setup` : vérifie code, active 2FA
- `POST /2fa/disable` : désactive (mot de passe requis), efface tous les appareils de confiance
- `POST /2fa/send-email-code` : envoie OTP 6 chiffres par email (validité 5 min)
- `POST /2fa/verify` : vérifie code (TOTP ou email), émet JWT, crée `two_fa_trusted_devices` si `trustDevice=true`
- `GET /trusted-devices` : liste appareils de confiance actifs
- `DELETE /trusted-devices/:id` : révoque un appareil
- `DELETE /trusted-devices` : révoque tous
**Frontend** :
- `AuthContext.jsx` : `login(email, password, deviceToken)` retourne `{ requires2FA, sessionToken }` si 2FA actif ; nouvelle fonction `completeLogin(token, user)` appelée après vérification du code
- `Login.jsx` : machine à 3 états (`form``method``code`) ; countdown 5 min pour OTP email ; checkbox "Faire confiance 30 jours" ; `cl_device_token` stocké dans localStorage
- `MonCompte.jsx` > Sécurité : composant `TwoFASection` (QR code setup, clé manuelle, confirmation code, désactivation) + composant `TrustedDevicesSection` (liste appareils, badge APPAREIL ACTUEL, déconnexion individuelle ou tout)
- `api.js` : `authHeaders()` envoie automatiquement `X-Device-Token` si `cl_device_token` présent
**Why:** Sécurité renforcée, demande utilisateur.
**How to apply:** Toujours envoyer `X-Device-Token` depuis `authHeaders()` (déjà en place). Ne pas exposer les tokens bruts dans les réponses API.
@@ -0,0 +1,25 @@
---
name: project_admin_sous_pages
description: "Architecture des sous-pages Admin — AdminPlateformes, AdminFiscalite et leurs sections"
metadata:
node_type: memory
type: project
originSessionId: da8e6c45-04c2-4db9-8e7f-189d65e7cf99
---
Trois sous-pages Admin existent sous `/admin/*`, chacune suivant le pattern `account-layout` / `account-sidebar` / `account-content` avec un bouton "← Administration" pour revenir.
**`/admin/plateformes` — AdminPlateformes.jsx**
NAV : Référentiel | Logos des plateformes | Tags suggérés | Types de garanties | Notation
- `GarantiesSection` : CRUD garanties (import CSV/JSON, export dropdown)
- `NotationRefSection` : critères de notation liés aux plateformes du **référentiel** (`/api/referentiel/:id/notation`) — pas aux plateformes utilisateur. Routes CRUD dans `referentiel.js`.
**`/admin/fiscalite` — AdminFiscalite.jsx**
NAV : Flat Tax (PFU) | 2047 — Crédit d'impôts
Section par défaut : `pfu`
- `PfuSection` : composant autonome (state + handlers + modals intégrés), charge `/api/pfu`
- `TauxCreditImpotSection` : taux crédit d'impôt 2047 (124 pays), charge `/api/taux-credit-impot`
**Why:** Les sections PFU, garanties et notation ont été déplacées hors de Settings car elles relèvent de la configuration administrative et non des préférences utilisateur.
**How to apply:** Toute nouvelle section de référentiel admin → ajouter dans AdminPlateformes ou AdminFiscalite selon la nature. Ne pas recreer ces sections dans Settings.
@@ -0,0 +1,22 @@
---
name: Page Centre d'aide
description: Page /aide avec layout Settings, section FAQ accordéon, bouton dans UserMenu
type: project
originSessionId: c7a8bfe3-1a9f-476a-be50-c4f412629322
---
Page d'aide accessible via le menu utilisateur (sous "Paramètres").
**Fichiers créés / modifiés :**
- `frontend/src/pages/Aide.jsx` — page créée (layout `account-layout`, même pattern que Settings)
- `frontend/src/App.jsx` — route `/aide` ajoutée
- `frontend/src/components/UserMenu.jsx` — bouton "Aide" + `IconAide` ajoutés sous "Paramètres"
**Structure de la page :**
- Titre sidebar : "Centre d'aide"
- Navigation par `?section=` (défaut : `faq`)
- Section FAQ avec composant `FaqItem` (accordéon dépliable, chevron animé)
**FAQ existante (section `faq`) :**
1. "Comment est calculé le solde du porte-monnaie d'une plateforme ?" — décrit la formule complète : dépôts retraits manuels + remboursements (selon fiscalité plateforme) + bonus capital investi + corrections.
**How to apply :** Pour ajouter une nouvelle entrée FAQ, ajouter un `<FaqItem question="...">` dans la section `faq` de `Aide.jsx`. Pour ajouter une nouvelle section, ajouter une entrée dans le tableau `NAV` et un bloc conditionnel `{section === 'xxx' && ...}` dans le contenu.
@@ -0,0 +1,40 @@
---
name: project_auth_email
description: "Vérification email, mot de passe oublié, pages auth — architecture complète"
metadata:
node_type: memory
type: project
originSessionId: e9aff1a4-9c1c-4cab-b7ec-90146b832fb0
---
Fonctionnalités auth ajoutées (session juin 2026, avant le 2FA).
**DB** — migrations dans `db/index.js` :
- `users.email_verified INTEGER NOT NULL DEFAULT 1` (DEFAULT 1 pour ne pas bloquer les comptes existants ; 0 pour les nouveaux si SMTP actif)
- Table `email_verification_tokens` (token, expires_at 24h, used)
- Table `password_reset_tokens` (token, expires_at 1h, used)
- Table `smtp_config` (id=1, enabled, host, port, secure, email, username, password, allow_unauth, app_name, app_url)
**Backend (auth.js)** — routes :
- `POST /register` : premier user auto-vérifié ; si SMTP actif → envoie email bienvenue + `{ requiresVerification: true }`
- `POST /login` : bloque si `email_verified=0`, code `EMAIL_NOT_VERIFIED`
- `GET /verify-email?token=` : valide le token, set `email_verified=1`
- `POST /resend-verification` : invalide anciens tokens, envoie nouveau (toujours 200 pour éviter enumeration)
- `PUT /me` : si email change → `email_verified=0` + envoie nouveau lien de vérification
- `POST /forgot-password` / `POST /reset-password` : tokens 1h, toujours 200
**Backend (admin.js)** :
- `GET /admin/users` retourne `email_verified`
- `PATCH /admin/users/:id/verify-email` : vérification manuelle par admin
**Pages frontend** :
- `Login.jsx` : gère `EMAIL_NOT_VERIFIED` → bouton "Renvoyer l'email"
- `Register.jsx` : état `verifyEmail` si `requiresVerification`
- `ForgotPassword.jsx` : layout 2 colonnes, état idle → sent
- `ResetPassword.jsx` : layout 2 colonnes, lit `?token=`, redirect /login après 3s
- `VerifyEmail.jsx` : layout 2 colonnes, appel GET au montage, états loading/ok/error
- `MonCompte.jsx` : badge vérifié/non-vérifié sur email, `EmailChangeForm`, `EmailResendBlock`
- `App.jsx` : routes `/forgot-password`, `/reset-password`, `/verify-email`
**Why:** Sécurité compte, demande utilisateur.
**How to apply:** `getSmtpConfig()` est la source de vérité SMTP. `smtpReady = cfg.enabled && cfg.host && cfg.email`. Toujours wrap les `sendMail` dans try/catch pour ne pas bloquer l'inscription.
@@ -0,0 +1,30 @@
---
name: Fonctionnalité réinvestissement automatique des intérêts
description: auto_reinvest par investissement — déclenché automatiquement après chaque remboursement, brut ou net selon la fiscalité de la plateforme
type: project
originSessionId: f8882910-b8d1-4a87-ab0b-2962aa6aad7b
---
Fonctionnalité ajoutée : réinvestissement automatique des intérêts après chaque remboursement.
**DB (migrations dans `db/index.js`) :**
- `investissements.auto_reinvest` INTEGER DEFAULT 0
- `reinvestissements.source` TEXT DEFAULT 'manuel' ('manuel' | 'auto')
- Migration `fiscalite_locale` (interets_bruts_avant_local, taxe_locale sur remboursements) était tronquée — réparée dans la même session
**Backend :**
- Route `PUT /api/investissements/:id/auto-reinvest { active: bool }` dans `investissements.js`
- `GET /api/investissements/:id` retourne maintenant `plateforme_fiscalite` (JOIN sur plateformes)
- `POST /api/remboursements` : si `auto_reinvest=1`, crée un reinvestissement avec `source='auto'`
- Plateforme `flat_tax` (France) → montant = `interets_nets`
- Autres plateformes → montant = `interets_bruts`
**Frontend (`InvestissementDetail.jsx`) :**
- Modal "Ajouter un réinvestissement" : sélecteur tabs "Manuel" / "Automatique"
- Tab Manuel = formulaire existant
- Tab Automatique = activation/désactivation + description brut/net selon plateforme
- État `reinvTab` reset à 'manuel' à chaque ouverture via `openReinvModal(tab='manuel')`
- Bloc "Réinvestissements complémentaires" visible dès que `auto_reinvest=1` (même sans reinvest existant)
- Badge `auto` sur les lignes créées automatiquement
- Menu ⋮ carte "Informations du projet" : entrée "Désactiver le réinvestissement auto" visible si actif
**Why:** Permettre de réinvestir automatiquement les intérêts perçus pour capitaliser sans saisie manuelle.
@@ -0,0 +1,28 @@
---
name: project_capital_mensuel_table
description: "Composant CapitalMensuelTable.jsx — onglet Vision mensuelle dans Investissements, capital encours par plateforme par mois"
metadata:
node_type: memory
type: project
originSessionId: b72654be-0979-4e85-a9d7-0fd12a61d170
---
Nouvel onglet **"Vision mensuelle"** ajouté dans `Investissements.jsx` entre les onglets "Plateformes" et "Investissements".
**Composant** : `frontend/src/components/CapitalMensuelTable.jsx`
**Calcul 100% frontend** — utilise les données déjà chargées dans la page (`allRows`, `allRembs`, `allReinvests`, `plats`). Pas de nouvel endpoint backend.
**Logique capital encours par mois** : pour chaque mois M de l'année :
- L'investissement est actif si `date_souscription ≤ fin_M` ET (statut actif aujourd'hui OU `date_fin ≥ début_M`)
- `capital = montant_investi + reinvests_≤_finM capital_remboursé_≤_finM`
- Groupé par `plateforme_id`
**Structure du tableau** : classes `tip-table` / `tip-th-*` / `tip-td-*` réutilisées depuis TableauInteretsPlateforme. Colonnes : Plateforme | 12 mois | Moyenne | Poids (barre + %).
**Sélecteur d'années** : boutons + années + bouton TOUT. Le bouton TOUT ramène sur l'année courante (pas de mode global distinct).
**Multi-détenteur** : détenteur_nom affiché si `new Set(grid.map(p => p.investisseur_id)).size > 1` — cohérent avec [[feedback_multi_detenteur]].
**Why:** Demande d'Olivier pour visualiser la répartition mensuelle du capital déployé par plateforme.
**How to apply:** Props à passer depuis Investissements.jsx : `allRows`, `allRembs`, `allReinvests`, `plats`.
@@ -0,0 +1,19 @@
---
name: Menu ⋮ carte Informations du projet — InvestissementDetail
description: Bouton contextuel ⋮ en haut à droite du bloc "Informations du projet" avec Modifier, Réinvestir, Exporter, Supprimer (+ Désactiver auto-reinvest si actif)
type: project
originSessionId: f8882910-b8d1-4a87-ab0b-2962aa6aad7b
---
Ajout d'un menu contextuel sur le bloc "Informations du projet" dans `InvestissementDetail.jsx` :
- État `cardMenu: { x, y } | null` (distinct de `openMenu` qui sert aux tableaux)
- Bouton ⋮ (30×30px, border var(--border), 3 cercles SVG) en haut à droite du header de la card
- Menu (position: fixed + backdrop) avec :
- **Modifier** → `openEdit()`
- **Réinvestir** → `openReinvModal()`
- **Exporter** → `exportDossier()`
- Séparateur
- **Désactiver le réinvestissement auto** (visible uniquement si `autoReinvActive`) → PUT auto-reinvest false
- **Supprimer** (rouge) → `setRowDeleteConfirm(...)` via ConfirmModal (pas `setConfirmingInvDelete` qui est dans le modal d'édition)
**Why:** `setConfirmingInvDelete` est dans le footer du modal d'édition et ne s'affiche pas sans l'ouvrir ; il faut utiliser `rowDeleteConfirm` / `ConfirmModal` pour les suppressions depuis le menu carte.
@@ -0,0 +1,42 @@
---
name: project-categories-secteurs-inv
description: "Tables categories_inv et secteurs_inv, gestion admin dans AdminPlateformes, fusion, filtre référentiel"
metadata:
node_type: memory
type: project
originSessionId: 2fef7a75-e0f3-4f7d-9c80-79c3be16af03
---
Fusion de `type_investissement` (champ enum mono-valeur) et des catégories référentiel en deux tables multi-valeurs gérées globalement par l'admin.
**Tables DB :**
- `categories_inv (id, nom, created_at)` — liste globale des catégories d'investissement
- `referentiel_categories_inv (referentiel_id, categorie_id)` — junction many-to-many
- `secteurs_inv (id, nom, created_at)` — liste globale des secteurs d'investissement
- `referentiel_secteurs_inv (referentiel_id, secteur_id)` — junction many-to-many
**Backend :**
- `/api/ref-categories` (GET/POST/PUT/:id/DELETE/:id/POST/:id/merge) — protégé requireAdmin
- `/api/ref-secteurs` (GET/POST/PUT/:id/DELETE/:id/POST/:id/merge) — protégé requireAdmin
- `referentiel.js` : helpers `attachCatsInv` + `attachSecteursInv`, `categories_inv_ids` et `secteurs_inv_ids` dans saveRelations
- Seeding au démarrage depuis `type_investissement` et `secteur` existants
**Frontend — AdminPlateformes (/admin/plateformes) :**
- Section "Catégories d'investissement" : RefListSection avec apiPath="/ref-categories"
- Section "Secteurs d'investissement" : RefListSection avec apiPath="/ref-secteurs"
- Menu ⋮ par ligne : Renommer / Fusionner avec… / Supprimer
- Fusion : POST /:id/merge → ajoute cible aux référentiels qui n'ont pas déjà la cible, supprime source
- Clic sur un nom de catégorie → navigue vers referentiel avec `?filterCat=<nom>` pré-rempli
**Frontend — PlatformeProfile.jsx :**
- Sélecteurs chips violet (categories_inv) et vert (secteurs_inv) en mode édition
- Vue : badges par catégorie/secteur
- Prompt IA : `{{CATEGORIES_INV_LIST}}` et `{{SECTEURS_INV_LIST}}` interpolés depuis les listes chargées
- `type_investissement` et `secteur` envoyés à null au save (dépréciation)
**Frontend — AdminPlateformes — RefModal (création/édition référentiel) :**
- Charge `/ref-categories` et `/ref-secteurs` au montage
- Chips toggle violet/vert pour sélectionner les catégories et secteurs
- Envoie `categories_inv_ids` et `secteurs_inv_ids` dans le payload
**Why:** Permettre plusieurs catégories et secteurs par plateforme, gérés centralement par l'admin avec propagation automatique via renommage.
@@ -0,0 +1,61 @@
---
name: project-categories-secteurs-inv-heritage
description: "Héritage merge (fusion) des catégories/secteurs d'investissement depuis le référentiel vers les plateformes utilisateur"
metadata:
node_type: memory
type: project
originSessionId: cad3a9c0-cf8e-4204-ac6e-e22c03811abe
---
# Héritage catégories/secteurs d'investissement (merge)
Les catégories et secteurs d'investissement définis dans le référentiel sont **fusionnés** (merge) dans les plateformes liées. Ce mécanisme est distinct de l'héritage scalaire (`overridden_fields`).
**Why:** L'user doit toujours voir les tags du référentiel comme base, sans pouvoir les retirer, mais peut ajouter les siens en plus.
**How to apply:** Toute évolution des tables jonction plateforme↔cats/sects doit préserver cette logique merge. Ne jamais écraser les inherited IDs lors d'un PUT.
---
## Mécanisme
### Backend — `attachCategoriesInv` / `attachSecteursInv` (plateformes.js)
Au moment de lire les plateformes, les tags sont mergés :
1. Charger les propres tags de la plateforme (`plateforme_categories_inv`)
2. Si la plateforme a un `referentiel_id` : charger les tags du référentiel (`referentiel_categories_inv`)
3. Pour chaque tag du référentiel : s'il est déjà dans les propres tags → marquer `is_inherited: true` ; sinon l'ajouter avec `is_inherited: true`
4. Tags propres non dans le référentiel → `is_inherited: false`
### Backend — `PUT /plateformes/:id/categories-inv` (associations-inv.js)
Avant de sauvegarder, les IDs hérités du référentiel sont re-ajoutés à `allIds` (on ne peut pas les retirer).
### Frontend — `InvSelect.jsx`
Prop `inheritedIds={[...]}` :
- Items hérités : toujours cochés + disabled + badge "Réf" (fond accent, 10px)
- `toggle()` ignore les IDs hérités
- CSS : `.cat-select-item.inherited` (cursor: default)
### Frontend — `Settings.jsx`
`openEditPlat()` stocke dans l'état :
```js
inherited_cat_ids: platCatsInv.filter(c => c.is_inherited).map(c => c.id),
inherited_sect_ids: platSectsInv.filter(s => s.is_inherited).map(s => s.id),
```
InvSelect reçoit `inheritedIds={editPlat.inherited_cat_ids || []}`.
Bandeau héritage : affiche "+ catégories & secteurs" si `hasCatInherit || hasSectInherit`.
### Frontend — `PlatformeProfile.jsx`
Chips en lecture : badge "Réf" sur les items `is_inherited: true`.
---
## Note importante
L'héritage catégories/secteurs est **distinct** des champs scalaires (`HERITABLE_FIELDS` + `overridden_fields`). Le compteur `inheritedCount` dans le bandeau Settings ne compte que les scalaires ; cats/sects sont signalés séparément via `hasCatInherit`/`hasSectInherit`.
@@ -0,0 +1,36 @@
---
name: project-categories-secteurs-utilisateur
description: "Catégories/secteurs d'investissement en deux niveaux : globales admin (héritées) + privées utilisateur, avec associations plateformes et investissements"
metadata:
node_type: memory
type: project
originSessionId: 264d200a-fea4-4903-b7c5-20dcc9ebfbd2
---
Extension du système categories_inv / secteurs_inv pour permettre un modèle à deux niveaux.
**DB — nouvelles colonnes et tables :**
- `categories_inv.user_id` et `secteurs_inv.user_id` — NULL = global admin, sinon = privé utilisateur
- `plateforme_categories_inv (plateforme_id, categorie_id)` — associations plateforme↔catégorie utilisateur
- `plateforme_secteurs_inv (plateforme_id, secteur_id)`
- `investissement_categories_inv (investissement_id, categorie_id)`
- `investissement_secteurs_inv (investissement_id, secteur_id)`
**Backend :**
- `/api/categories-inv` et `/api/secteurs-inv` — CRUD user-facing (globales en lecture seule, privées éditables)
- `/api/plateformes/:id/categories-inv` et `secteurs-inv` — GET/PUT associations plateforme
- `/api/investissements/:id/categories-inv` et `secteurs-inv` — GET/PUT associations investissement
- GET `/api/investissements` et `/:id` enrichis : `categories_inv` et `secteurs_inv` arrays inline
- Push référentiel étendu : pousse aussi catégories/secteurs vers plateformes liées (mode doux = INSERT OR IGNORE, fort = DELETE+INSERT) avec sync en cascade sur investissements actifs
**Sync automatique investissements :**
Quand les catégories/secteurs d'une plateforme sont mis à jour (PUT ou push), les investissements `en_cours`/`en_retard`/`procedure` de cette plateforme sont resynchronisés automatiquement.
**Frontend :**
- Settings > "Catégories d'investissement" et "Secteurs d'investissement" : liste globales (badge Héritée, non éditables) + privées (CRUD inline)
- Settings > Plateformes : formulaire d'édition avec chips violet (catégories) et vert (secteurs) ; panneau détail affiche les associations
- Investissements : chips dans le formulaire (pré-remplis depuis la plateforme choisie), badges dans InvestissementDetail, filtres Catégorie et Secteur dans la liste (ET logique)
**Why:** Permettre à chaque utilisateur d'organiser ses investissements selon ses propres critères tout en héritant d'une taxonomie commune définie par l'admin.
**How to apply:** Pour toute nouvelle page affichant les investissements, charger `/api/categories-inv` et `/api/secteurs-inv` pour les filtres. Les données `categories_inv` et `secteurs_inv` sont déjà incluses dans les réponses GET investissements.
@@ -0,0 +1,40 @@
---
name: project-cerfa2561
description: Simulation CERFA 2561 par plateforme dans la page Fiscalité (TaxReport)
metadata:
node_type: memory
type: project
originSessionId: 0922bde7-84ed-45dc-b233-50bf0be5ad3e
---
Composant `frontend/src/components/Cerfa2561Preview.jsx` — simulation HTML du formulaire CERFA 2561, accessible via l'onglet "CERFA 2561" de la page TaxReport.
**Architecture :**
- Backend : `GET /api/taxreport/cerfa2561` — données agrégées par plateforme × investisseur
- Backend : `GET /api/taxreport/cerfa2561/remboursements` — détail des remboursements pour un formulaire
- Backend : `GET /api/taxreport/years` — années disponibles
- Frontend : `Cerfa2561Preview.jsx` — mode inline (onglet) avec prop `inline`, `expanded`, `onToggleExpand`
**Cases CERFA remplies :**
- `2TT/KR` ou `2TR/AR` selon `plateforme.type_produit_fiscal` ('2TT' défaut, '2TR' configurable dans Settings)
- `2BH/DQ` = interets_bruts si flat-tax FR (PS déjà prélevés)
- `2CK/AD` = prelev_forfaitaire si flat-tax FR (crédit d'impôt PFNL)
- `2TY/KS` = pertes en capital
- Montants arrondis à l'entier (sans décimales), récapitulatif fiscal en décimales
**Champ `type_produit_fiscal` sur plateformes :**
- Migration DB : `ALTER TABLE plateformes ADD COLUMN type_produit_fiscal TEXT NOT NULL DEFAULT '2TT'`
- Visible dans Settings > Plateformes uniquement pour les plateformes françaises
- Affiché dans le bloc Détail de la plateforme
**Ergonomie :**
- Sélecteur de plateforme (220px) + bouton agrandir + bouton imprimer dans la toolbar
- Impression via `window.open``print-color-adjust: exact` pour conserver les fonds colorés
- Titre de la fenêtre = `CERFA 2561 — {annee} — {plateforme_nom}` (proposé comme nom de fichier PDF)
- Panneau dépliable "N remboursements pris en compte" — tableau détail avec cache (pas de re-fetch)
- Mode sombre : `colorScheme: 'light'` + `background: '#fff'` forcé sur le formulaire
**Palette couleurs :** violet/indigo aligné sur YearSelector (`#5b21b6`, `#7c3aed`, `#f5f3ff`, `#ede9fe`)
**Why:** Permettre à l'utilisateur de vérifier/simuler ce que chaque plateforme aurait dû déclarer, et de générer un PDF de référence par plateforme.
**How to apply:** Fichier unique `Cerfa2561Preview.jsx`. Pas de sed sur ce fichier (risque de troncature). Utiliser python3 pour les remplacements de masse.
@@ -0,0 +1,65 @@
---
name: project_cerfa_fiscal
description: "Architecture complète des 3 onglets fiscaux TaxReport : CERFA 2042, CERFA 2561 (IFU), CERFA 2778-SD — composants, données, vues"
metadata:
node_type: memory
type: project
originSessionId: cd6fedee-a8a5-422b-912c-bf927e5e7fdd
---
## Onglets TaxReport (TaxReport.jsx)
Ordre : **CERFA 2042** | **CERFA 2561 (IFU)** | **CERFA 2778-SD** (si pfoAssujetti)
`activeTab` par défaut : `'2042'` — les onglets Récapitulatif et Détail par projet ont été supprimés.
**KPI synthèse** : bloc "Cases fiscales 2042 — synthèse {annee}" affiché en haut de page (avant les onglets), calculé en fetchant `/taxreport/cerfa2561` + `/taxreport/2778` dans `TaxReport.jsx` (state `data2042`). Cases affichées : 2TT (si > 0), 2TR (si > 0), 2BH, 2CK (vert), 2TY (rouge, si > 0). Le fetch exclut les plateformes étrangères via `cl_2778_excluded_plats` localStorage.
---
## CERFA 2561 (IFU) — `Cerfa2561Preview.jsx`
Sidebar 3 vues :
- **Suivi mensuel** : tableau plateformes françaises × 12 mois (interets_bruts), avec Total intérêt brut / Taux prélevé / Total intérêt net en bas
- **Données 2561** : sélecteur plateforme+détenteur (FR uniquement, sans doublon prénom) + formulaire CERFA 2561Form
- **Report 2042** : `Report2042Block2561` — cases 2TT/2TR/2BH/2CK/2TY agrégées, breakdown par plateforme, badge BADGE_AUTO
**Données** : `GET /taxreport/cerfa2561` retourne `lignes[]` avec `mois[mm] = { interets_bruts, prelev_sociaux, prelev_forfaitaire }` (breakdown mensuel ajouté cette session)
**Helper nom** : `fullName(l)` — évite doublon si `nom` commence déjà par `prenom`
---
## CERFA 2778-SD — `Cerfa2778Preview.jsx`
Sidebar 3 vues :
- **Suivi mensuel** : tableau plateformes étrangères × 12 mois, avec checkboxes inclusion/exclusion persistées `cl_2778_excluded_plats`
- **Données 2778-SD** : sélecteur mois → `CerfaBlock` avec cases BA/IA/PQ/PV/PF1/PG1/PK/QR
- **Report 2042** : `Report2042Block` — cases 2TR/2BH/2CK pour plateformes étrangères
**Données** : `GET /taxreport/2778` retourne `plateformes[] = { id, nom, mois: { mm: number } }` (route reconstruite cette session après troncature)
**CerfaBlock** : grille 6 col `1fr 90px 90px 90px 90px 90px` — Libellé | Base imposable (BA) | Taux | Case | Montant | (sans titre). Sections : Page de garde (page 1) / PFO (page 2) / PS (page 3) / Totaux (page 4). Props `mois`, `annee` passées pour afficher "Mai 2026" dans Montant sans unité (`noUnit`).
**Wrapper** : le composant retourne un `<div className="card">` depuis cette session.
---
## CERFA 2042 — `Cerfa2042Preview.jsx` (nouveau cette session)
Sidebar 2 vues :
- **Suivi mensuel** : tableau combiné FR (vert, déclaration automatique) + étranger (rouge, à déclarer) ; Total Plateformes françaises dans tbody avant groupe étranger ; Total Plateformes étrangères + Total général + Taux prélevé + Total intérêt net dans tfoot
- **Données 2042** : cases mixtes groupées par code (2TT/2TR/2BH/2CK/2TY) ; chaque ligne de breakdown porte son badge `BADGE_AUTO` ou `BADGE_DECL` ; sélecteur filtre "Tout / Automatique / À déclarer" en haut à droite
**Données** : fetche **toujours** les deux endpoints `/taxreport/cerfa2561` + `/taxreport/2778` (inconditionnellement, indépendant de `pfoAssujetti`) ; lit `cl_2778_excluded_plats` depuis localStorage
**Badges** :
- `BADGE_AUTO` = { text: 'déclaration automatique', bg: rgba(22,163,74,0.1), color: #16a34a }
- `BADGE_DECL` = { text: 'à déclarer', bg: rgba(239,68,68,0.1), color: #dc2626 }
**Calcul net étranger** : `brut × (1 totalTaxRate)` (estimation depuis taux PFU de l'année)
**Noms de mois** : `MOIS_LABELS` utilise les noms complets ('Janvier'…'Décembre') dans les 3 composants.
**Why:** Vue de synthèse pour la déclaration 2042, fusionnant automatique (IFU FR) et manuel (2778-SD étranger).
**How to apply:** `pfoAssujetti` est passé en prop mais n'influe plus sur le fetch (toujours chargé). La prop reste utile pour d'autres contextes. Fichiers sensibles aux troncatures : Cerfa2778Preview.jsx, Cerfa2561Preview.jsx, Cerfa2042Preview.jsx — toujours reconstruire la fin par index de lignes Python.
@@ -0,0 +1,45 @@
---
name: project-chart-colors
description: "Couleurs personnalisables des graphiques — UiContext, Settings Apparence, palette MD2, variante Reçu/Projeté"
metadata:
node_type: memory
type: project
originSessionId: d7532bd8-8eb4-4b40-8c39-8f569dcf432f
---
Fonctionnalité de personnalisation des couleurs des graphiques ajoutée dans Settings > Apparence.
**Fichiers modifiés :** `frontend/src/context/UiContext.jsx`, `frontend/src/pages/Settings.jsx`, `frontend/src/styles.css`
## UiContext
3 nouvelles clés localStorage et états :
- `cl_chart_interets``chartInterets` / `setChartInterets` — défaut `#2196f3` (Bleu 500)
- `cl_chart_capital``chartCapital` / `setChartCapital` — défaut `#4caf50` (Vert 500)
- `cl_chart_cashback``chartCashback` / `setChartCashback` — défaut `#ffc107` (Ambre 500)
Tous exposés dans `useUi()`.
## Settings.jsx — composants ajoutés
- `lightenColor(hex, factor=0.72)` — mélange avec blanc, utilisé pour la variante "Projeté"
- `MD_PALETTE` — palette Material Design 2 complète (19 familles × shades 50→900 + accents A100/A200/A400/A700)
- `isLightColor(hex)` — détecte si une couleur est claire (pour adapter la couleur du texte)
- `findInPalette(hex)` — retrouve `{ family, shade }` d'un hex dans MD_PALETTE
- `SUGGESTED_PALETTES` — 10 palettes prédéfinies (Classique, Indigo & Teal, Nuit, Nature, Coucher de soleil, Violet, Frais, Contraste, Pastel, Moderne)
- `PaletteSelector` — grille 5×2 de cartes avec 3 pastilles colorées ; sélection applique les 3 couleurs
- `ChartColorPicker` — sélecteur en 2 étapes : (1) `<select>` famille + indicateur couleur, (2) grille de teintes ; useEffect synchro famille quand value change depuis l'extérieur
- Icônes SVG inline : `IconInterets`, `IconCapital`, `IconCashback` (taille 22px, fill="currentColor")
## Preview Reçu / Projeté
Chaque picker affiche deux barres :
- **Reçu** — couleur sélectionnée (pleine)
- **Projeté** — version éclaircie à 72% (`lightenColor`)
## CSS (styles.css)
Classes ajoutées : `.chart-color-row`, `.chart-color-item`, `.chart-color-picker`, `.color-family-select-row`, `.color-family-indicator`, `.color-shade-grid`, `.color-shade-btn`, `.color-shade-label`, `.color-preview-pair`, `.color-preview-bar`, `.color-preview-label`, `.color-preview-hex`, `.palette-grid`, `.palette-card`, `.palette-swatches`, `.palette-swatch`, `.palette-name`
**Attention CSS :** `.palette-swatch` et `.color-shade-label` sont des `<span>` — ils nécessitent `display: block` pour que width/height et opacity fonctionnent correctement.
## Prochaine étape
Brancher `chartInterets`, `chartCapital`, `chartCashback` (via `useUi()`) et `lightenColor` dans les composants de graphiques : `InteretsChart.jsx`, `InteretsMensuelsChart.jsx`, `SoldeChart.jsx`, `InteretsDistributionChart.jsx`.
**Why:** L'utilisateur veut que les couleurs choisies dans Settings s'appliquent effectivement aux graphiques.
**How to apply:** Importer `useUi`, déstructurer les 3 couleurs, remplacer les constantes couleur hardcodées par les valeurs dynamiques. Utiliser `lightenColor` importé ou redéfini localement pour les séries projetées.
@@ -0,0 +1,62 @@
---
name: project-communication
description: "Page Communication — tickets support + notifications : architecture, comportements UI, jobs backend"
metadata:
node_type: memory
type: project
originSessionId: 19a3ce4d-7c1a-418b-9ca2-65b6157249db
---
# Page Communication (`frontend/src/pages/Communication.jsx`)
## Architecture générale
3 volets : sidebar (volet 1), liste (volet 2), détail (volet 3).
Onglets : `support` (tickets) | `notifications`.
## Filtres chips (volet 2)
- **Tickets** : multi-select via `useState(new Set(['open', 'pending']))` ; si aucune chip active → liste vide (pas "tout afficher")
- **Notifications** : multi-select `useState(new Set(['unread']))` au démarrage ; même règle liste vide si 0 chip active
- Chips actives = fond `var(--primary)` + checkmark SVG blanc ; cumulatives
## Comportements notifications
- Pas de présélection automatique au chargement ni au changement d'onglet
- La notification sélectionnée reste jusqu'à clic explicite
- Menu ⋮ dans le volet 3 : "Marquer comme lu" / "Tout considérer comme lu" / "Tout supprimer"
## Dropdowns portal (pattern obligatoire)
Tous les dropdowns custom (TagSelect, TypeDropdown, RecipientDropdown) utilisent `createPortal(content, document.body)` + `position: fixed` + `getBoundingClientRect()` pour échapper aux stacking contexts.
## Composant RecipientDropdown
Dropdown searchable pour les broadcasts. Trois niveaux :
- "Tous les utilisateurs" (value `''`)
- "Tous les administrateurs" (value `'admins'`) — routé vers `WHERE role='admin'` côté backend
- Utilisateurs individuels filtrés par `display_name` / `email` avec avatars initiales
## Broadcast payload
```js
payload.user_id = bcUserId === 'admins' ? 'admins' : Number(bcUserId);
```
## Ticket detail header (volet 3)
Structure colonne :
- Ligne 1 : chip Type + titre (flex:1) + StatusBadge
- Ligne 2 : "Ouvert par **Nom** · date [— assigné à **Nom**]" à gauche + chips pages à droite
- `margin-top: 25px` entre les deux lignes
## Droits
- Utilisateur : voit uniquement ses tickets et ses notifications
- Admin : voit tous les tickets ; voit ses notifs + notifs `notifyAdmins`
- Actions admin-only : Assigner à, Mettre en attente, Réouvrir, Clôturer
- "Modifier la catégorisation" : accessible à tous (owner + admin)
- "Fermer" (panneau) : accessible à tous
## Pièges React connus
- `{msg.is_admin && <span>}` → si `is_admin = 0` (SQLite), affiche "0". Toujours `{!!msg.is_admin && ...}`
- Images dans messages : classe `msg-inline-img` + wrapper `msg-img-wrap` ajouté par useEffect. Margin `7px` sur `.msg-img-wrap` ET `.msg-inline-img` pour couvrir les deux cas.
## Jobs backend
- `autoTicketStatus.js` : open→pending (2j sans réponse), pending→resolved (5j sans réponse créateur) — toutes les heures
- `autoCleanNotifs.js` : supprime notifications >7 jours sauf type `security` — toutes les heures
**Why:** Nettoyage automatique pour éviter l'accumulation infinie de notifications lues.
**How to apply:** Si on ajoute un nouveau type de notification critique à ne jamais purger, l'ajouter à la clause `type != 'security'` dans `autoCleanNotifs.js`.
@@ -0,0 +1,38 @@
---
name: project_compte_id_remboursements
description: FK compte_id sur investissements et remboursements — liaison aux comptes bancaires du détenteur
metadata:
node_type: memory
type: project
originSessionId: bf0e3a7b-9629-4ce8-81a7-96943a703cfc
---
Colonne `compte_id` (FK nullable → `comptes.id`, ON DELETE SET NULL) ajoutée sur `investissements` et `remboursements`.
**DB** : migrations dans `backend/src/db/index.js` (guard PRAGMA table_info).
**Backend `investissements.js`** : Schema Zod, GET (join comptes → compte_id/compte_nom/compte_type), POST INSERT, PUT UPDATE. Route `GET /investissements/comptes-par-investisseur/:investisseur_id` : liste les comptes d'un investisseur (pour les selects frontend).
**Backend `remboursements.js`** : Schema Zod, INSERT et UPDATE (nullifié si methode ≠ compte_courant). GET / retourne `compte_nom` via LEFT JOIN comptes. Route `POST /remboursements/backfill-comptes` : lie les remboursements sans compte_id à l'investissement ou au premier compte_courant du détenteur (accessible depuis MonCompte → Nettoyage).
**Cascade compte_id** : PUT investissements met à jour les remboursements avec `methode=compte_courant AND compte_id IS NOT NULL`. Les remboursements redirigés manuellement vers portefeuille sont protégés.
**Frontend — 4 formulaires mis à jour :**
- `Investissements.jsx` openEdit + openNew : `resolvedMethode` depuis la plateforme si méthode fixée
- `InvestissementDetail.jsx` openEdit investissement : idem
- `InvestissementDetail.jsx` formulaire remboursement : `comptesRembInvestisseur` chargé après déclaration useState (éviter TDZ), méthode toujours éditable (override possible vers portefeuille)
- `Remboursements.jsx` : comptes chargés au changement d'investissement_id, hérite `inv.compte_id` en priorité
**Règles UX :**
- Méthode dans formulaire remboursement = toujours éditable (pas de champ readOnly) — permet forçage hors cascade
- Auto-sélection : `inv.compte_id` en priorité, sinon premier compte de type `compte_courant`
- `resolvedMethode` dans openEdit : lire la methode de la plateforme si ≠ `choix_investisseur`
**Page Remboursements — colonne Versement :**
- Remplace colonne "Type" ; affiche nom du compte courant (avec tooltip) ou "Porte-monnaie" (grisé)
- Deux boutons icônes (porte-monnaie + compte-courant) dans le header du bloc pour filtrer ; dépendances useMemo incluses
- Corrections de solde masquées quand filtre porte-monnaie désactivé
**Why:** Permettre de tracer sur quel compte bancaire arrivent les remboursements, pour rapprochement bancaire futur.
**How to apply:** Tout nouveau formulaire affichant methode_remboursement doit suivre ce même pattern. Voir [[project_methode_remboursement_investissement]].
@@ -0,0 +1,22 @@
---
name: project_comptes_exoneration
description: "Champ exoneration_fiscale sur la table comptes — migration DB, backend, frontend Settings"
metadata:
node_type: memory
type: project
originSessionId: bf0e3a7b-9629-4ce8-81a7-96943a703cfc
---
Colonne `exoneration_fiscale` ajoutée à la table `comptes`.
**Valeurs :** `'aucune'` (défaut) | `'pfnl_5ans'` (Exonération IR PFNL si détention 5 ans)
**Migration DB :** `ALTER TABLE comptes ADD COLUMN exoneration_fiscale TEXT NOT NULL DEFAULT 'aucune'` dans `backend/src/db/index.js` (avec guard PRAGMA table_info).
**Backend `comptes.js` :** champ ajouté au Schema Zod, au SELECT, INSERT et UPDATE.
**Frontend `Settings.jsx` :** select dans `CompteFormFields`, `EMPTY_COMPTE` mis à jour, payloads création/édition complétés. Colonne dans le tableau avec "Oui" (vert, title=libellé) / "Non" (grisé). `EXONERATION_LABELS` map définie à côté de `TYPE_COMPTE_LABELS`.
**Why:** Permettre de distinguer les enveloppes fiscalement avantageuses (PEA-PME, etc.) pour les calculs fiscaux futurs.
**How to apply:** Si un nouveau type d'exonération est ajouté, mettre à jour l'enum Zod backend ET `EXONERATION_LABELS` frontend.
@@ -0,0 +1,36 @@
---
name: Composant ConfirmModal et suppressions
description: ConfirmModal.jsx créé ; tous les confirm() natifs remplacés dans les pages
type: project
originSessionId: c8c8964c-17e5-40a2-8e72-52261af1d3e9
---
Un composant `ConfirmModal` générique a été créé dans `frontend/src/components/ConfirmModal.jsx`.
Props : `open`, `title` (défaut "Confirmer la suppression"), `message`, `confirmLabel` (défaut "Supprimer"), `onConfirm`, `onCancel`.
Il s'appuie sur `Modal.jsx` existant, largeur 440px, bouton ghost "Annuler" + bouton danger.
**Why:** Remplacer les 12 `confirm()` natifs (popup navigateur) par des modales React ergonomiques.
**How to apply:** Toute nouvelle suppression doit utiliser `ConfirmModal` avec un état local `const [deleteConfirm, setDeleteConfirm] = useState(null)`.
Pattern standard :
```js
const del = (item) => {
setDeleteConfirm({
message: `Supprimer "${item.nom}" ?`,
onConfirm: async () => {
await api.del(`/route/${item.id}`);
setDeleteConfirm(null);
reload();
},
});
};
```
Et dans le JSX, avant la fermeture du return :
```jsx
<ConfirmModal
open={!!deleteConfirm}
message={deleteConfirm?.message}
onConfirm={deleteConfirm?.onConfirm}
onCancel={() => setDeleteConfirm(null)}
/>
```
Fichiers déjà migrés : DepotsRetraits, Admin (UsersSection), Investissements, Remboursements, Settings (Settings + GarantiesPanel + NotationPanel), FamilleEntreprises.
@@ -0,0 +1,28 @@
---
name: Fonctionnalité corrections de solde
description: Table corrections_solde, route /api/corrections, intégration dashboard/fiscal/remboursements pour réconcilier les micro-écarts de flat-tax
type: project
originSessionId: 94c97348-4f1d-4ff7-ba4b-e94dda6b32ea
---
Nouvelle fonctionnalité implémentée pour corriger les micro-écarts (0,01 €) de calcul de flat-tax sur les plateformes françaises.
**Modèle :** table `corrections_solde` (id, investisseur_id, plateforme_id, date, montant, notes, created_at). Séparée de `remboursements` pour éviter de rendre `investissement_id` nullable (migration SQLite complexe).
**Déclenchement :** après chaque dépôt/retrait sur une plateforme `fiscalite = 'flat_tax'`, une `CorrectionModal` s'ouvre et demande le solde réel constaté. Si |écart| ≥ 0,005 €, une correction est créée automatiquement via POST `/api/corrections`.
**Solde attendu proposé par défaut :**
- Retrait → 0 € (cas typique : vider le porte-monnaie)
- Dépôt → solde calculé avant l'opération + montant du dépôt
**Impact :**
- `dashboard.js` : `correctionsPerPlat` s'ajoute au `walletMap` par plateforme ; route `GET /solde-historique` ajoutée (calcul du solde à une date passée, filtre `date <= :date` sur les 5 composantes)
- `fiscal2778.js` : corrections ajoutées à `net_recu`, ligne dédiée dans le récapitulatif, tableau séparé, incluses dans l'export CSV
- `Remboursements.jsx` : corrections mergées dans `tableRows` avec flag `_is_correction: true`, badge "CORRECTION" ambré, supprimables via menu ⋮
- `DepotsRetraits.jsx` : corrections affichées comme nouveau type de mouvement dans l'onglet Mouvements (`normalizedCorrections` via useMemo, `type: 'correction'`, id préfixé `corr_${id}`). Menu ⋮ sur chaque ligne (Modifier / Correction de solde / Supprimer). Option "Corrections de solde" dans le filtre type.
**Route :** `/api/corrections` (GET filtré par scope, POST avec validation Zod, DELETE avec vérification ownership)
**Route solde historique :** `GET /api/dashboard/solde-historique?plateforme_id=X&date=Y`
**Why:** Les arrondi de prélèvements sociaux/IR créent des écarts cumulatifs entre le solde calculé et le solde réel de la plateforme.
**How to apply:** Ne pas modifier `remboursements` pour ajouter ces corrections — toujours passer par la table dédiée.
@@ -0,0 +1,34 @@
---
name: project-dashboard-kpis
description: "KPIs Dashboard — TrendBadge, logique M vs M-1, capital investi et en risque avec comparaison mensuelle"
metadata:
node_type: memory
type: project
originSessionId: 752b3578-1999-4431-9dd9-73297a9baaf4
---
6 KPIs dans l'ordre : Capital investi · Capital en risque · Performance annualisée · Intérêts bruts/nets · Capital remboursé · Cashback reçu.
**Composants dans Dashboard.jsx :**
- `TrendBadge({ current, prev, invert })` — badge ↗/↘ vert/rouge, `invert=true` pour Capital en risque (hausse = mauvais)
- `KpiCard({ title, value, badge, refValue })` — carte label uppercase, valeur large, badge inline, ref grise dessous
- `DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData })` — enfant de `InteretsChartProvider`
**Logique comparaison selon sélecteur d'année :**
- Année courante (N) : valeur = mois en cours, badge = M vs M-1, ref = `X € en avr. 2026`
- Année passée (N-x) : valeur = total annuel, badge = N vs N-1, ref = `X € en 2024`
- modeGlobal : cumul sans badge
**Capital investi et Capital en risque — comparaison mensuelle (2026-05-31) :**
- Backend `dashboard.js` boucle `capitalMensuel` : chaque entrée contient maintenant `{ mois, capital, en_defaut }``en_defaut` = somme capital des prêts `en_retard`/`procédure`
- Frontend : `capitalCurrent/capitalPrev` et `enDefautCurrent/enDefautPrev` extraits de `capitalMensuelData` dans le `useMemo` de `thisMonthRow`
- Badge ne s'affiche que si `isCurrentYear` et `prev > 0`
- Approximation : statut non historisé → valeur mensuelle `en_defaut` reflète le statut actuel projeté rétrospectivement
**Données source :**
- `rawData.rembourses` (mensuel) — `{ mois, interets_bruts, interets_nets, capital, cashback }`
- `rawDataGlobal.rembourses` (annuel) — `{ annee, interets_bruts, interets_nets, capital, cashback }`
- `capitalMensuelData` — prop passée depuis Dashboard parent via `onCapitalMensuel` de `TableauInteretsPlateforme`
- `currentYear`, `currentMonth`, `rawData`, `rawDataGlobal` exposés depuis `InteretsChartContext`
**How to apply:** Respecter la distinction `isCurrentYear` pour choisir entre granularité mensuelle (N) et annuelle (N-x). Tout nouveau KPI affichant des intérêts doit utiliser `netMode` pour basculer brut/net.
@@ -0,0 +1,35 @@
---
name: project_depots_mensuel_table
description: "Composant DepotsMensuelTable.jsx — onglet Vision mensuelle dans DepotsRetraits, dépôts/retraits par plateforme par mois avec filtres icônes"
metadata:
node_type: memory
type: project
originSessionId: b72654be-0979-4e85-a9d7-0fd12a61d170
---
Nouvel onglet **"Vision mensuelle"** ajouté dans `DepotsRetraits.jsx` entre les onglets "Plateformes" et "Mouvements".
**Composant** : `frontend/src/components/DepotsMensuelTable.jsx`
**Calcul 100% frontend** — props : `allRows` (dépôts/retraits), `plats`.
**Filtres icônes** : deux boutons dans le header (pattern identique à TableauInteretsPlateforme) :
- Icône `"depot"` (default ON, couleur `#22c55e`) : inclure les dépôts
- Icône `"retrait"` (default OFF, couleur `#ef4444`) : inclure les retraits
- Quand les deux sont actifs → valeur = dépôts retraits (net), valeurs négatives en rouge
- Quand un seul actif → somme positive du type sélectionné
**Icônes** : chargées depuis `/api/icons``libIcons = { name: filename }``<img src="/api/icons-files/{filename}">`. Composant `AppIcon` interne avec fallback span. Noms dans la bibliothèque : `"depot"` et `"retrait"`.
**Agrégation** : par `plateforme_id`, colonnes `depots[]` et `retraits[]` séparées (12 mois), calculées depuis `r.type === 'depot'` / `r.type === 'retrait'`.
**Structure du tableau** : classes `tip-table` réutilisées. Colonnes : Plateforme | 12 mois | Total | Moy. mensuelle.
**Sélecteur d'années** : boutons + années + bouton TOUT = `setAnnee(currentYear)`.
**Multi-détenteur** : `detenteur_nom` affiché si `new Set(byPlat.map(p => p.investisseur_id)).size > 1` — cohérent avec [[feedback_multi_detenteur]].
**hexToRgba** : helper local pour teinter les boutons actifs.
**Why:** Demande d'Olivier pour visualiser la répartition mensuelle des flux de trésorerie (dépôts/retraits) par plateforme.
**How to apply:** Props à passer depuis DepotsRetraits.jsx : `allRows`, `plats`. Onglet tab `activeTab === 'vision-mensuelle'`.
@@ -0,0 +1,18 @@
---
name: Panneau détail unifié — DepotsRetraits
description: DetailPanel unifié pour dépôts/retraits et corrections de solde ; boutons Correction de solde et Supprimer retirés du panel
type: project
originSessionId: f8882910-b8d1-4a87-ab0b-2962aa6aad7b
---
Le composant `DetailPanel` dans `DepotsRetraits.jsx` a été unifié :
- Format identique pour tous les types (dépôt, retrait, correction de solde) sous le titre "Détails du mouvement"
- Champ "Type de mouvement" affiche : "Dépôt", "Retrait" ou "Correction de solde"
- Montant sans couleur verte/rouge (neutre pour tous les types)
- **Footer selon le type :**
- `auto_remboursement` → mention "Modifiable via la page Remboursements"
- `correction` → bouton "Supprimer la correction" (style `dr-detail-edit-btn`)
- `depot` → bouton "Modifier le dépôt"
- `retrait` → bouton "Modifier le retrait"
- Boutons "Correction de solde" et "Supprimer" retirés du panel (disponibles via menu ⋮)
- Props du composant : `{ row, onEdit, onDeleteCorrection }` (onCorrection supprimé)
@@ -0,0 +1,30 @@
---
name: project-docker-deployment
description: "Architecture de déploiement Docker (Traefik, nginx, volumes, icônes seed, DATA_DIR) — pièges et corrections appliquées"
metadata:
node_type: memory
type: project
originSessionId: 8f54f673-57ca-4a21-b205-0857456e2ed0
---
Déploiement Docker sur `crowdlending.croguennec.net` — corrections appliquées en session 2026-06-13.
**Stack :** Traefik (reverse proxy) → crowdlending-frontend (nginx) → crowdlending-backend (Node/Express). Volumes montés sur `~/volumes/crowdlending/data` et `~/volumes/crowdlending/uploads`.
**Pièges résolus :**
1. **Nom du container nginx** : dans `frontend/nginx.conf`, l'upstream doit être `crowdlending-backend` (nom du container Docker), pas `backend`.
2. **Traefik middleware provider** : les middlewares de fichier doivent s'écrire `ipwhitelist-all@file`, pas `ipwhitelist-all@docker`.
3. **`express.static` + `fallthrough`** : sans `{ fallthrough: false }`, un logo manquant appelle `next()` qui tombe sur `app.use('/api', requireAuth)` → 401. Toujours ajouter `fallthrough: false` sur les middlewares statiques publics (logos, icons-files).
4. **Chemin `DATA_DIR`** : les chemins relatifs depuis `__dirname` diffèrent entre dev Windows (`backend/src/routes` → 3 niveaux → projet root/data) et Docker (`/app/src/routes` → 3 niveaux → `/data`). Solution : variable d'env `DATA_DIR=/app/data` dans `docker-compose.yml` ; fallback `../../../data` pour dev. Utilisé dans `referentiel.js` et `server.js`.
5. **Volume masque les fichiers seed** : monter un volume sur `/app/data` masque les fichiers copiés dans l'image. Solution : dossier `backend/icons_seed/` + `docker-entrypoint.sh` qui copie les fichiers manquants dans `/app/data/icons/` au démarrage (copie idempotente, ne remplace pas l'existant).
6. **Sauvegarde DB** : `sqlite3` n'est pas installé dans le container. Utiliser `docker cp $CONTAINER:/app/data/crowdlending.db $BACKUP_PATH` dans `deploy.sh`.
7. **deploy.sh** : `git pull --ff-only` échoue si les branches ont divergé. Utiliser `git pull origin main`.
**How to apply :** Avant tout déploiement depuis zéro, vérifier que `DATA_DIR` est dans `docker-compose.yml`, que `icons_seed/` est peuplé avec les 16 icônes, et que `docker-entrypoint.sh` est exécutable dans le Dockerfile.
@@ -0,0 +1,42 @@
---
name: project-domiciliation-iso
description: Migration du champ domiciliation des plateformes vers codes ISO 3166-1 alpha-2 avec sélecteur pays à drapeaux
metadata:
node_type: memory
type: project
originSessionId: 62e4d5e0-01c1-41c5-a24b-23f6ec59b5fb
---
Migration complète du champ `domiciliation` (plateformes + référentiel) de valeurs texte vers codes ISO 3166-1 alpha-2.
**Ce qui a été fait :**
1. **Zod backend**`z.enum(['france','zone_europeenne','hors_zone_europeenne'])` remplacé par `z.string().min(1).max(100)` dans `plateformes.js` et `referentiel.js`
2. **Migration DB** (`db/index.js`) — trois blocs ajoutés en fin de fichier :
- `'france'``'FR'`, `'zone_europeenne'`/`'hors_zone_europeenne'``'EU'` provisoire sur les deux tables
- `pays_siege` (nom français) → `domiciliation` (ISO) sur `plateformes_referentiel` via mapping NAME_TO_ISO complet
3. **Sentinel** — tous les `=== 'france'` / `!= 'france'` remplacés par `=== 'FR'` / `!= 'FR'` dans : `taxreport.js`, `plateformes.js`, `Settings.jsx`, `AdminPlateformes.jsx`, `TaxReport.jsx`, `Cerfa2561Preview.jsx`, `CerfaRecapTable.jsx`, `Cerfa2042Preview.jsx`
4. **UI**`CountrySelect` (composant existant, codes ISO, drapeaux) intégré dans :
- `Settings.jsx` : formulaire création + formulaire édition plateforme
- `AdminPlateformes.jsx` : RefModal
- `PlatformeProfile.jsx` : section Géographie (lecture + édition), remplace `pays_siege`
5. **DOMICILIATION_LABELS** supprimé dans les 3 fichiers, remplacé par `countryLabel(code)` via `COUNTRIES` de `CountrySelect.jsx`
6. **Filtre référentiel** rendu dynamique (valeurs distinctes en base) dans `AdminPlateformes.jsx`
7. **Badges "hérité"** ajoutés aux 4 champs manquants dans la modale édition Settings : `fiscalite`, `type_produit_fiscal`, `taux_fiscalite_locale`, `logo_filename`
8. **Réinitialiser au référentiel** — ajout ConfirmModal avant exécution + message de succès vert dans le formulaire après reset
**État DB :**
- `plateformes.domiciliation` : valeurs ISO (`'FR'`, `'EU'` provisoire pour ex zone_europeenne)
- `plateformes_referentiel.domiciliation` : valeurs ISO, dont 3 migrées depuis `pays_siege`
- `pays_siege` : champ conservé en DB mais retiré de l'UI (PlatformeProfile)
**Why:** Prépare l'affichage avec drapeaux et ouvre la porte à une granularité pays plutôt que zone géographique, utile pour la fiscalité (crédit d'impôt 2047 par pays).
**How to apply:** Le sentinel clé est `=== 'FR'` (pas `=== 'france'`). Les plateformes encore en `'EU'` sont à corriger manuellement via le sélecteur pays.
@@ -0,0 +1,38 @@
---
name: project-donut-interactif
description: "Fonctionnalités interactives du donut Dashboard — tooltip, clic, mémoire, légende cliquable, labels nets/bruts"
metadata:
node_type: memory
type: project
originSessionId: 75df8125-7b0d-44b0-a3e5-9e68fb07b541
---
Évolutions sur `InteretsDonutChart.jsx`, `InteretsMensuelsChart.jsx` et `InteretsChartContext.jsx`.
**Tooltip au survol (donut)** : chaque segment SVG a `onMouseEnter`/`onMouseLeave`. Le tooltip HTML est positionné via `position: absolute` sur un wrapper `position: relative` autour du SVG (maxWidth 260). Coordonnées calculées depuis le midpoint de l'arc en % du viewBox 240×240.
**Clic → filtrage du bar chart** :
- Mode multi-types : clic sur un segment isole ce type (`setInclureCapital/Cashback/Interets`)
- Mode type unique (Reçu/Projeté) : clic appelle `selectActualOnly()` ou `selectProjectedOnly()`
- `handleArcClick` compare `item.label === 'Intérêts nets' || item.label === 'Intérêts bruts'` (pas 'Intérêts' seul — label dynamique)
**Mémoire / retour arrière** : avant chaque clic, l'état complet est sauvegardé dans `prevStateRef` (useRef) + `hasPrev` (useState). Cliquer sur le centre du donut restaure l'état via `setActualProjected(actual, projected)`. Indicateur visuel : ↩ SVG au-dessus du label central, fond bleu au hover.
**Contexte — exports ajoutés** : `selectActualOnly`, `selectProjectedOnly`, `setActualProjected`.
**Légende donut** :
- Ordre fixe : Intérêts → Capital → Cashback (dans `donutData`, donc aussi dans le SVG)
- Chaque ligne est cliquable (même comportement que le quartier)
- Format compact une ligne : label (muted) + montant (bold) + détail inline `dont X reçus · Y projetés` (11px muted)
- La fonction `makeItem(label, color, opacity, actualAmt, projectedAmt)` centralise la construction
- `getLegendDetail` vérifie `label === 'Intérêts nets' || 'Intérêts bruts'` (pas 'Intérêts' seul)
**Labels nets/bruts** :
- Donut : le label Intérêts devient `netMode ? 'Intérêts nets' : 'Intérêts bruts'` dans `makeItem` (mode multi-types). `netMode` est destructuré depuis `useInteretsChart()`.
- Bar chart légende : `activeTypes` utilise `label: netMode?'Intérêts nets':'Intérêts bruts'`
- Bar chart tooltip bas : dernière ligne affiche toujours "Total" (pas "Projeté" pour les futurs)
**En-tête bar chart** : remplacé le texte plat par des badges colorés inline — un par type actif, fond teinté `hexToRgba(color, 0.12)`, point coloré 7px, label 13px dans la couleur du type, suivi de `· année` en muted. La variable `headerLabel` a été supprimée.
**Why:** Améliorer la lisibilité et la cohérence du dashboard.
**How to apply:** Toujours vérifier les deux variantes du label Intérêts (`'Intérêts nets'` et `'Intérêts bruts'`) dans toute logique qui compare `arc.label` ou `d.label`.
@@ -0,0 +1,14 @@
---
name: project-drillcell-filtrage-zero
description: DrillCellPanel filtre les lignes à 0 selon les critères actifs (intérêts/capital/cashback)
metadata:
node_type: memory
type: project
originSessionId: 43bfd5e1-0e82-496d-bdd2-437e92339323
---
Les lignes REÇUS et PROJETÉS dont le total est 0 selon les critères actifs (intérêts seuls, capital, cashback) sont masquées dans DrillCellPanel.jsx.
**Why:** Un prêt différé avec uniquement un cashback enregistré apparaissait avec 0,00 € dans la colonne Intérêts — ce qui était correct mais visuellement trompeur en mode "Intérêts uniquement".
**How to apply:** Les listes filtrées `recusVisible` et `projetesVisible` sont calculées via `.filter(r => recuRowValue(r) !== 0)`. Le compteur d'en-tête affiche "Reçus (X/Y)" si des lignes sont cachées (X visible, Y total), sinon "Reçus (X)". Message alternatif si recusVisible.length === 0 mais recus.length > 0 : "Aucun montant à afficher avec les critères sélectionnés". Le bouton "Valider en masse" reste conditionné sur `projetes.length` (total réel, pas filtré).
@@ -0,0 +1,36 @@
---
name: project_drillcellpanel_dashboard
description: DrillCellPanel permanent sur Dashboard + navigation croisée Dashboard↔Remboursements
metadata:
node_type: memory
type: project
originSessionId: 8667511a-3e20-485e-b8ed-c038934d7ec3
---
## DrillCellPanel permanent sur Dashboard
- Remplace le tableau "Échéances prévues du mois en cours" (supprimé)
- Toujours visible en bas du Dashboard (`alwaysOpen=true`), initialisé sur le mois courant, toutes plateformes
- Cliquer une cellule du TIP (TableauInteretsPlateforme) met à jour le DrillCellPanel via `setDrillCell`
- Props passées : `cell`, `alwaysOpen`, `pfuRates`, `activeView`, `activeId`, `plateformes`, `investissements=[]`, `onBulkDone`, `onEditRecu`, `onEditProjet`
**Why:** Remplace un tableau statique par un panneau drill-down interactif, cohérent avec la page Remboursements.
## Navigation Dashboard → Remboursements → retour
### Dashboard (`onEditRecu` / `onEditProjet`)
- Reçu cliqué : `navigate('/remboursements?edit-remb=ID&from=dashboard&drill-annee=X&drill-mois=Y[&drill-plat=Z]')`
- Projeté cliqué : `navigate('/remboursements?open-simul=INV_ID&simul-date=DATE&simul-capital=C&simul-interets=I&from=dashboard&drill-annee=X&drill-mois=Y[&drill-plat=Z]')`
- Restauration drillCell au retour : `useSearchParams` lit `drill-annee`, `drill-mois`, `drill-plat` dans `_initDrillCell()`
- Les params URL sont nettoyés au premier rendu (`setSearchParams({}, { replace: true })`)
### Remboursements (auto-ouverture)
- `autoOpenRef = useRef(false)` — ouverture une seule fois
- `backToDashboardRef = useRef(null)` — mémorise l'URL de retour `/?drill-annee=X&drill-mois=Y[&drill-plat=Z]`
- `useEffect([allRows.length, investissements.length, plateformes.length])` — attend que les données soient chargées
- `edit-remb` → cherche dans `allRows`, appelle `openEdit(remb)`
- `open-simul` → reconstruit l'objet simul depuis les params, appelle `openFromSimul({...})`
- Nettoie l'URL avec `navigate('/remboursements', { replace: true })`
- `close()` modifié : si `backToDashboardRef.current`, navigate vers cette URL puis réinitialise la ref
**How to apply:** Tout nouveau lien "ouvrir modal depuis une autre page" doit suivre ce pattern URL params + autoOpenRef + backRef.
@@ -0,0 +1,53 @@
---
name: project_export_restore
description: "Fonctionnalité export complet prod→dev — ZIP DB+assets, stockage serveur, restauration pending-restore, job 3h00"
metadata:
node_type: memory
type: project
originSessionId: 5cf6a868-7a3f-44e5-a0f8-0499f84b1afc
---
# Export / Restore prod → dev
Fonctionnalité permettant à un admin d'extraire l'état complet d'un environnement (DB + logos + icônes) et de le restaurer ailleurs.
## Fichiers concernés
- `backend/src/routes/admin.js` — routes export/restore
- `backend/src/jobs/autoExport.js` — job automatique 3h00
- `frontend/src/pages/admin/ExportSection.jsx` — UI complète
- `frontend/src/pages/admin/JobLogsSection.jsx` — KNOWN_JOBS inclut `auto_export`
- `backend/src/db/index.js` — bloc pending-restore au démarrage
## Routes backend (`/api/admin/`)
| Route | Rôle |
|---|---|
| `GET /export-full` | Génère un ZIP (VACUUM INTO + assets), sauvegarde sur disque, retourne au navigateur |
| `GET /exports` | Liste les exports stockés sur le serveur |
| `POST /exports/upload` | Upload d'un ZIP externe, validation manifest, sauvegarde |
| `GET /exports/:filename` | Téléchargement d'un export stocké |
| `DELETE /exports/:filename` | Suppression d'un export stocké |
| `POST /exports/:filename/restore` | Restauration : backup sécurité + assets + pending-restore + process.exit(0) |
## Mécanisme pending-restore
1. La route restore écrit `{DB_PATH}.pending-restore`
2. `process.exit(0)` → Docker redémarre le container
3. Au démarrage, `db/index.js` détecte le fichier `.pending-restore`, le copie vers `DB_PATH`, supprime les fichiers WAL/SHM, puis supprime le `.pending-restore`
## Stockage
- `DATA_DIR/exports/` — max 10 fichiers ZIP, auto-purge des plus anciens
- Les exports générés automatiquement et manuellement partagent le même dossier
- Les backups de sécurité pré-restore portent le préfixe `pre-restore-backup-`
## Job automatique
- `startAutoExportJob()` — planifié à 3h00 locale chaque jour (sans dérive)
- Tracé dans `job_logs` (job_name = `auto_export`)
- Démarré dans `server.js` aux côtés de `startAutoStatutJob()`
## Incident SQLITE_CORRUPT (2026-06-16)
Voir [[feedback_sqlite_vacuum_into]] — root cause : `db.backup()` produisait un fichier WAL-mode incomplet. Fix : `VACUUM INTO`. Le serveur a été récupéré manuellement via container temporaire node:20-alpine avec bind mount sur `/root/volumes/crowdlending/data`.
@@ -0,0 +1,25 @@
---
name: project_fichiers_tronques
description: Fichiers JSX/JS tronqués en production — dashboard.js et Investissements.jsx reconstruits
metadata:
node_type: memory
type: project
originSessionId: b72654be-0979-4e85-a9d7-0fd12a61d170
---
Fichiers tronqués (octets nuls `\x00` ou fin abrupte) — cause probable : écriture Windows interrompue.
**Reconstruits session 2026-06-01 :**
- `backend/src/server.js` — 2 octets nuls strippés
- `backend/src/routes/plateformes.js` — route DELETE `/:id/logo` reconstruite
- `backend/src/routes/dashboard.js` — fin handler `GET /interets-par-plateforme` reconstruite (796 lignes)
- `backend/src/routes/taxreport.js` — route `GET /years` reconstruite + routes `GET /cerfa2561` et `GET /cerfa2561/remboursements` reconstruites + `round2` helper manquant ajouté
- `backend/src/db/index.js` — fin seed `app_icons` reconstruite (960 lignes)
- `backend/src/routes/simul.js``export default router;` manquant ajouté, doublon supprimé
- `frontend/src/pages/Settings.jsx` — section `NotationPanel` tronquée ligne 2951 (`{fo`) reconstruite
**Reconstruits session 2026-05-29 :**
- `frontend/src/pages/Investissements.jsx`
**Why:** Troncatures causées par des opérations Edit/Write ou flush incomplet sur Windows (voir [[feedback_file_truncation]]).
**How to apply:** Si le backend refuse de démarrer avec "Unexpected end of input", vérifier avec `xxd | tail -4` pour détecter octets nuls ou troncature, puis reconstruire avec python3.
@@ -0,0 +1,39 @@
---
name: Fiscalité locale sur remboursements
description: Nouveaux champs interets_bruts_avant_local et taxe_locale pour les plateformes avec retenue à la source locale
type: project
originSessionId: c7a8bfe3-1a9f-476a-be50-c4f412629322
---
Ajout de la prise en charge de la fiscalité locale (retenue à la source) dans les remboursements, pour les plateformes configurées avec `fiscalite = 'avec_fiscalite_locale'`.
**Modèle financier :**
- `interets_bruts_avant_local` = montant saisi par l'utilisateur (brut avant retenue)
- `taxe_locale` = `interets_bruts_avant_local × taux_fiscalite_locale / 100` (auto-calculé, éditable)
- `interets_bruts` = `interets_bruts_avant_local taxe_locale` (auto-calculé, lecture seule)
- PFU français s'applique sur `interets_bruts` (après retenue locale)
**Why:** Les plateformes hors zone française peuvent prélever une taxe locale avant reversement ; celle-ci doit être stockée séparément pour la réconciliation fiscale.
**How to apply:** Toute future page ou export affichant des intérêts sur ces plateformes doit distinguer `interets_bruts_avant_local` (brut plateforme) de `interets_bruts` (base PFU).
**Migrations DB (backend/src/db/index.js) :**
- `ALTER TABLE remboursements ADD COLUMN interets_bruts_avant_local REAL NOT NULL DEFAULT 0`
- `ALTER TABLE remboursements ADD COLUMN taxe_locale REAL NOT NULL DEFAULT 0`
**Backend (backend/src/routes/remboursements.js) :**
- Les deux champs ajoutés au schéma Zod, INSERT et UPDATE (normal type uniquement, pas les bonus)
**Frontend — formulaires modifiés :**
- `Remboursements.jsx` : champs conditionnels dans le formulaire modal (visible si `avec_fiscalite_locale`)
- `InvestissementDetail.jsx` : idem dans la modale remboursement + 2 colonnes conditionnelles dans le tableau des remboursements enregistrés ("Int. bruts Plateforme" et "Taxe Locale")
**Logique setField / setRembField :**
1. Saisie `interets_bruts_avant_local` → calcule `taxe_locale` et `interets_bruts`, puis PFU
2. Correction manuelle `taxe_locale` → recalcule `interets_bruts` (`avant_local taxe_locale`), puis PFU
3. `interets_bruts` est en lecture seule quand `hasLocalTax` est actif
**openRembFromSimul :** si plateforme avec retenue locale, les `interets_prevus` de la projection sont traités comme `interets_bruts_avant_local` et la taxe est pré-calculée.
**Calcul solde porte-monnaie — règle définitive (corrigé dans dashboard.js et DepotsRetraits.jsx) :**
- `flat_tax``net_recu` (PFU prélevé à la source)
- Hors France (`sans_fiscalite_locale` ou `avec_fiscalite_locale`) → `capital + cashback + interets_bruts` (pas de PFU à la source ; l'investisseur déclare le PFU séparément ; `interets_bruts` est déjà net de la retenue locale)
@@ -0,0 +1,38 @@
---
name: project_fiscalite_override
description: Override de fiscalité par investissement — exonération flat tax pour PEA-PME ou comptes similaires
metadata:
node_type: memory
type: project
originSessionId: 5682d634-7345-40a8-8184-f617d1dd8444
---
## Champ ajouté sur `investissements`
| Colonne | Type | Valeurs |
|---|---|---|
| `fiscalite_override` | TEXT nullable | `null` (suit la plateforme) \| `'exonere'` (traité comme sans_fiscalite_locale) |
## Route backend
`PUT /api/investissements/:id/fiscalite-override { override: 'exonere' | null }` — bascule l'état, vérifie ownership.
## Comportement quand `fiscalite_override = 'exonere'`
- `exonere` est un cas de `isTaxIndicatif = true` (voir [[project_istaxindicatif]]).
- Les prelev sont **calculés automatiquement** (indicatif) depuis les taux PFU, mais les champs sont en **lecture seule** et labellisés "— indicatif".
- `net_recu` = capital + cashback + interets_bruts (pas de déduction des prelev indicatifs).
- Seuls les **futurs** remboursements sont affectés — les existants gardent leurs valeurs (sauf recalcul via reprocess).
## UI dans la fiche détail
- **Badge** vert "Exonéré flat tax" affiché à côté du titre "Informations du projet" quand actif.
- **Menu ⋮** de la carte : option "Exonérer de la flat tax" / "Rétablir la flat tax" (libellé adapté à l'état courant) → ouvre une modale de confirmation (`fiscaliteOverrideConfirm` state) avec explication et bouton Confirmer.
- La modale est rendue juste avant le bloc `{cardMenu && ...}`.
## Auto-reinvest et fiscalite_override
Dans `remboursements.js` (POST), le montant auto-réinvesti utilise `inv.fiscalite === 'flat_tax'` pour choisir entre `interets_nets` et `interets_bruts`. Si l'investissement est exonéré mais la plateforme reste `flat_tax`, ce calcul n'est pas encore ajusté — à surveiller si l'auto-reinvest est activé sur un investissement exonéré.
**Why:** Investissements logés en PEA-PME (ex. Baltis) ne subissent pas la retenue à la source ; les intérêts sont versés bruts.
**How to apply:** Vérifier `inv.fiscalite_override === 'exonere'` partout où `plateforme_fiscalite === 'flat_tax'` conditionne un calcul.
@@ -0,0 +1,33 @@
---
name: formulaires-remboursement-refacto
description: "Refactorisation des modales de saisie de remboursement dans InvestissementDetail et Remboursements — grille 3 colonnes, sections, règles isIndicatif"
metadata:
node_type: memory
type: project
originSessionId: 9f8f19b9-5456-41ce-b666-455acf9ebccb
---
Les deux modales de remboursement ont été refactorisées pour adopter le même format.
**Structure commune (grille 3 colonnes, width=900) :**
- Section **Remboursement** → Capital | Intérêts bruts | Cashback (titre AVANT les champs hasLocalTax)
- Section **Imposition** (+ "— indicatif" si applicable) → Prélèvements sociaux | IR | **Total prélèvements** (readonly grisé = PS + IR)
- Ligne texte : "Montant des intérêts après imposition : xxx €" (pas de cadre, texte simple)
- Section **Versement** → Date | Montant versé (readonly calculé)
- Ligne commentaire : explication du calcul selon isIndicatif (voir formulations ci-dessous)
**Champs supprimés des deux formulaires :** Statut, Notes, Mode de remboursement, Intérêts nets (remplacé par Total prélèvements + ligne commentaire).
**Règle isIndicatif (alignée entre les deux pages) :**
- `isExonere = currentInv?.fiscalite_override === 'exonere'`
- `isIndicatif = isExonere || currentPlat?.fiscalite !== 'flat_tax'`
- Si indicatif : prélèvements grisés en lecture seule, titre "Imposition — indicatif"
- Si indicatif : `netRecu = capital + cashback + interets_bruts` (pas nets)
**Formulations commentaire Montant versé :**
- Indicatif : "L'imposition n'est pas déduite du montant versé (fiscalité appliquée à titre indicatif)."
- Flat tax : "L'imposition est déduite du montant versé (capital + cashback + intérêts nets)."
**Why:** Harmonisation UX + application correcte de la règle d'exonération flat tax (voir [[project_istaxindicatif]]).
**How to apply:** Toute nouvelle page avec formulaire de remboursement doit reproduire ce pattern de grille et la logique isIndicatif.
@@ -0,0 +1,32 @@
---
name: project-formulaires-ux
description: Améliorations UX des formulaires Nouvel investissement et Nouveau remboursement (session 2026-05-30)
metadata:
node_type: memory
type: project
originSessionId: c75c80b6-ddc2-47f8-8f5d-1b772f53c3b5
---
## Formulaire "Nouveau remboursement" (Remboursements.jsx)
- **Investissement select** : les bonus (🎁 Bonus Parrainage, 🏆 Bonus Plateforme) apparaissent en tête de liste, avant le séparateur et les investissements
- **Investissement select** : les investissements sont préfixés du nom de plateforme (`Indemo — LV0000109486 (64,69 €)`) et triés alphabétiquement par plateforme puis nom projet
- **Ouverture** : aucun investissement présélectionné (valeur `''` → tiret)
- **Bonus — champ "Investisseur bénéficiaire" supprimé** : `bonus_investisseur_id` est auto-déduit de `plateforme.investisseur_id` lors de la sélection de la plateforme dans le select bonus
- **Bonus — layout** : ligne 1 = Investissement (span 1) + Plateforme (span 2) ; ligne 2 = Date (span 1) + Montant (span 2)
**Why:** UX — éviter la saisie redondante du détenteur déjà porté par la plateforme ; meilleure lisibilité de la liste d'investissements.
**How to apply:** Si un futur formulaire bonus est ajouté, appliquer le même pattern : pas de champ détenteur visible, dériver depuis la plateforme sélectionnée.
---
## Formulaire "Nouvel investissement" (Investissements.jsx)
- **Plateforme** : aucune plateforme présélectionnée à l'ouverture (tiret par défaut) — corrigé dans `openNew()`, et les deux `useEffect` (`?new=1` et `pendingNew`)
- **Champ "Détenteur" supprimé** : remplacé par `<input type="hidden">` ; `investisseur_id` est auto-alimenté via `investisseurForPlat(platId)` dès qu'une plateforme est choisie
- **Champs obligatoires ajoutés** : Taux annuel (`required`), Durée en mois (`required`), Date 1ère échéance (`required`) — validation navigateur uniquement, pas de contrainte DB
**Why:** Simplifier la saisie et éviter des investissements sans données financières essentielles.
**How to apply:** Ne pas recréer le select Détenteur dans ce formulaire ; la logique `investisseurForPlat` dans le onChange de la plateforme suffit.
@@ -0,0 +1,62 @@
---
name: project-icons-library
description: "Bibliothèque d'icônes admin — table app_icons, route /api/icons, section Admin, nettoyage fond SVG, composant AppIcon dans Settings"
metadata:
node_type: memory
type: project
originSessionId: d7532bd8-8eb4-4b40-8c39-8f569dcf432f
---
Système de gestion d'icônes applicatives accessible via `/admin?section=icons`.
## Tables (backend/src/db/index.js)
```sql
CREATE TABLE IF NOT EXISTS app_icons (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
filename TEXT NOT NULL,
description TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
)
CREATE TABLE IF NOT EXISTS app_icons_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
icon_id INTEGER NOT NULL REFERENCES app_icons(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
replaced_at TEXT NOT NULL
)
```
10 icônes seedées au premier démarrage (guard count=0).
## Route (backend/src/routes/icons.js)
- `GET /api/icons` — liste toutes les icônes
- `GET /api/icons/:name` — détail
- `GET /api/icons/:name/history` — historique (requireAdmin)
- `POST /api/icons` — création (upload multer, requireAdmin)
- `PUT /api/icons/:name` — remplacement avec archivage (max 10 versions)
- `PATCH /api/icons/:name` — modifier description uniquement
- Fichiers servis en statique : `GET /api/icons-files/:filename`
- Dossier physique : `data/icons/` à la racine du projet (pas dans backend/)
## Nettoyage fond SVG à l'upload (stripSvgBackground)
Supprime automatiquement les fonds blancs/gris dans les SVGs uploadés :
1. `background-color` dans `style=""` sur `<svg>`
2. Attribut `enable-background` (artefact Illustrator)
3. `<rect>` blanc/quasi-blanc couvrant le canvas entier
4. `<path>` blanc/quasi-blanc couvrant le canvas — **patterns raster-trace** (ex: `fill="#FEFEFE"`) détectés via `isNearWhite()` (seuil ≥ #F0F0F0) + `pathCoversCanvas()` (coordonnées proches de 0 ET de vbW/vbH)
- Rasters PNG/JPG/WebP : fond blanc → transparent via sharp (seuil R,G,B > 240)
- Fichiers `design/Icones/svg/*.svg` nettoyés rétroactivement
## Composant AppIcon (frontend/src/pages/Settings.jsx)
Remplace les SVGs inline dans le bloc "Couleurs des graphiques" :
```jsx
const ICONS_BASE = '/api/icons-files/';
function AppIcon({ filename, size = 22 }) { ... }
```
- `AppearanceSection` charge `GET /api/icons` au montage → dictionnaire `{ name: filename }`
- Utilisé avec `libIcons.interets`, `libIcons.capital`, `libIcons.cashback` à taille 33px
- CSS `.app-lib-icon` + `[data-theme="dark"] .app-lib-icon { filter: invert(1) }`
- Labels associés : `font-size: 1.1rem`, `font-weight: 600`
**Why:** Source unique — remplacer l'icône dans l'admin met à jour automatiquement tous les endroits qui l'affichent.
**How to apply:** Pour afficher une icône de la bibliothèque ailleurs dans l'app, charger `GET /api/icons` et utiliser `<img src={ICONS_BASE + filename} className="app-lib-icon">`.
@@ -0,0 +1,23 @@
---
name: project-icons-sidebar-titles
description: Icônes de la bibliothèque dans la sidebar nav et titres de pages
metadata:
node_type: memory
type: project
originSessionId: 743b204f-7d6b-45ed-b91c-387060b5941e
---
Icônes de la bibliothèque intégrées dans l'interface à deux endroits.
**Sidebar (`Layout.jsx`)** — composant `NavIcon` : affiche l'icône bibliothèque (`<img className="nav-lib-icon">`) si disponible, sinon fallback SVG inline. Icônes chargées via `api.get('/icons')` au montage du Layout, stockées dans `navIcons` state. Taille : 24px. CSS : `.nav-lib-icon` avec `filter: invert(1)` permanent (sidebar toujours sombre), opacity 0.85 au repos / 1 sur lien actif.
Mapping noms bibliothèque → pages :
- `dashboard` → Tableau de bord
- `investissement` → Investissements
- `depots-retraits` → Dépôts / Retraits
- `remboursement` → Remboursements
- `tax` → Fiscalité
**Titres de pages** — composant `PageIcon` (`frontend/src/components/PageIcon.jsx`) : `<img>` inline dans le `h2` du topbar. Cache module-level partagé (un seul appel API). Taille : 40px. Rendu inline (`display: inline; verticalAlign: middle; marginRight: 10`). Importé dans Dashboard, Investissements, DepotsRetraits, Remboursements, Fiscal2778.
**Why:** Ne jamais wrapper dans un div ou modifier `display` du h2 — voir [[feedback-pageicon-topbar]].
@@ -0,0 +1,114 @@
---
name: project-interets-mensuels-chart
description: InteretsMensuelsChart + InteretsDonutChart + InteretsChartContext — barres empilées + donut + mode TOUT (vue annuelle)
metadata:
node_type: memory
type: project
originSessionId: 9d8a4dc8-0caa-4a53-bf00-040dfe2afe4a
---
## Architecture (3 fichiers)
- `frontend/src/context/InteretsChartContext.jsx` — état partagé entre les deux graphiques
- `frontend/src/components/InteretsMensuelsChart.jsx` — graphique barres empilées
- `frontend/src/components/InteretsDonutChart.jsx` — donut de répartition
Utilisés dans `Dashboard.jsx` via `<InteretsChartProvider>` wrapper, layout flex 2/3 + 1/3 avec `alignItems: 'stretch'`.
## InteretsChartContext
Centralise : `annee`, `inclureInterets/Capital/Cashback`, `selectedMonth`, `rawData`, `months` (useMemo), `annualTotal`, `availableYears`, couleurs UiContext (`chartInterets/Capital/Cashback`).
Reçoit en props depuis Dashboard : `netMode`, `pfuRates`, `activeView`, `activeId`.
**Deux fetches :**
- `GET /api/dashboard/interets-mensuels?annee=&scope=``rawData` — retourne `{ rembourses, projections, annees }`. LEFT JOINs pour bonus (`bonus_investisseur_id`).
- `GET /api/dashboard/interets-annuels?scope=``rawDataGlobal` — retourne `{ rembourses, projections, annees }` groupés par année. Projections filtrées `>= date('now', 'start of month')`.
**Mode global (TOUT) :**
- `modeGlobal` (bool, false par défaut) + `toggleModeGlobal`
- `years` useMemo avec fenêtrage : max 8 années passées, année courante ≤ position 9, toujours ≥ 3 années projetées, max 12 barres
- `selectedYear` (number | null) + `setSelectedYear`
- `globalTotal` = somme de tous les `years[].total`
Algorithme fenêtrage `years` :
```js
pastYears = allSorted.filter(y < currentYear)
futureYears = allSorted.filter(y > currentYear)
pastCount = min(pastYears.length, 8)
pastSlice = pastYears.slice(-pastCount)
usedSlots = pastCount + 1
projCount = max(3, 12 - usedSlots)
futureSlice = futureYears.slice(0, projCount)
visibleYears = [...pastSlice, currentYear, ...futureSlice]
```
Chaque objet `year` dans `years[]` a les mêmes champs qu'un `month` (capitalAmt, cashbackAmt, interetsAmt, capitalProjAmt, interetsProjAmt, actual, projected, total, isCurrent, isFuture, label).
## InteretsMensuelsChart — barres empilées
- 3 toggles avec icônes de la bibliothèque (`GET /api/icons`, `<img src={ICONS_BASE+filename}>`, opacity active/inactive)
- `ICONS_BASE = '/api/icons-files/'` ; noms : `interets`, `capital`, `cashback`
- Segments empilés : Capital → Cashback → Intérêts (bottom to top)
- Couche `actual` (opaque) + `projected` (`hexToRgba(color, 0.28)`)
- Sélecteur année : fenêtre glissante ±1 an, `windowStart` state local
- **Bouton TOUT** : après les chevrons années, style `.solde-range-btn / .active` ; en mode TOUT les chevrons et le sélecteur d'années sont masqués
- En mode TOUT : `items = years`, `barCount = years.length`, gap=10, labels = années, clic → `setSelectedYear(toggle)`
- En mode mensuel : `items = months`, `barCount = 12`, gap=8, labels = mois, clic → `setSelectedMonth(toggle)`
- `filteredTotal(item)` unifié pour les deux modes → `rawMax`, `totalH`, label barre, grand total en-tête
- Légende bas : swatches reçu/projeté + sélecteur Reçu/Projeté
## InteretsDonutChart — donut de répartition
- Asservi à `InteretsChartContext` : mêmes toggles + `selectedMonth` (mode mensuel) ou `selectedYear` (mode TOUT)
- Mode mensuel : vue annuelle → totaux 12 mois ; vue mensuelle → données du mois cliqué
- Mode TOUT : vue globale → totaux toutes années ; vue annuelle → données de l'année cliquée
- Clic sur le sous-titre en légende → reset sélection (`setSelectedMonth(null)` ou `setSelectedYear(null)`)
- **Caps arrondis radiaux** : `roundedArcPath()` — path SVG `M outerS A outerArc A capR capR 0 0 1 innerE A innerArc A capR capR 0 0 0 outerS Z`
- Le cap est un semicercle reliant outer→inner à l'angle exact du segment, sans débordement angulaire
- Cercle complet (1 segment, GAP=0) : deux demi-cercles + `fillRule="evenodd"`
- GAP = 4° entre segments ; GAP = 0 si segment unique
- Constantes : `CX=CY=120`, `OUTER_R=108`, `INNER_R=72`, `capR=18`
- Anneau de fond : `<circle r={(OUTER_R+INNER_R)/2} strokeWidth={OUTER_R-INNER_R} opacity={0.18}/>`
- Texte central : label (mois/année/Total) + montant
- Titre "Répartition" : une ligne EN BAS dans la zone légende (`justifyContent: space-between`, séparé par `borderBottom`)
- SVG : `viewBox="0 0 240 240"`, `maxWidth: 260`, `height: auto`
## Layout Dashboard
```jsx
<InteretsChartProvider netMode={netMode} pfuRates={pfuRates} activeView={activeView} activeId={activeId}>
<div style={{ display:'flex', gap:16, alignItems:'stretch', marginTop:8, marginBottom:24 }}>
<div style={{ flex:2, minWidth:0 }}><InteretsMensuelsChart /></div>
<div style={{ flex:1, minWidth:0 }}><InteretsDonutChart /></div>
</div>
</InteretsChartProvider>
```
`alignItems:'stretch'` + `height:'100%'` sur la carte donut → même hauteur. `maxWidth:260` sur le SVG évite que le donut explose en hauteur.
## Sélecteur Reçu / Projeté
`showActual` + `showProjected` dans le contexte (+ `toggleActual` / `toggleProjected`).
Règle "force reçu" : si `showProjected` est déjà false, `toggleActual` ne peut pas désactiver `showActual`.
Sélecteur UI dans la ligne légende du bar chart :
- Fond `#f0f0f0`, borderRadius 8, padding 3
- Pill actif : fond blanc, boxShadow, fontWeight 600, couleur `#1a1a2e`
- Pill inactif : transparent, couleur `#9ca3af`
Impacts :
- `buildSegments` filtre `actual[]` si `!showActual`, `projected[]` si `!showProjected`
- `filteredTotal(item)` = somme conditionnelle → utilisé pour `rawMax`, `totalH`, label valeur, grand total en-tête
- `totalH` doit utiliser `filteredTotal` (pas `item.total`) sinon le label sort du viewBox quand un filtre est actif
- Donut : `sumA(key)` / `sumP(key)` retournent 0 si le filtre correspondant est off
## Fichiers backend associés
`backend/src/routes/dashboard.js` — deux endpoints :
- `/interets-mensuels` : groupé par mois pour une année donnée
- `/interets-annuels` : groupé par année, toutes années, projections >= début mois courant
**Why:** Visualiser répartition mensuelle/annuelle/multi-années par type (intérêts/capital/cashback), reçu vs projeté.
**How to apply:** Pour ajouter un 3e graphique lié, consommer `useInteretsChart()`. Toujours passer par python3/bash pour éditer ces fichiers (jamais Edit/Write direct sur les grands composants SVG).
@@ -0,0 +1,15 @@
---
name: Correction intérêts nets — page Investissements
description: net_recu_total remplacé par interets_nets_total pour afficher les vrais intérêts nets
type: project
originSessionId: c8c8964c-17e5-40a2-8e72-52261af1d3e9
---
Bug corrigé : la colonne "Intérêts (Net)" du tableau par plateforme dans Investissements.jsx affichait `net_recu_total` (= capital + cashback + intérêts nets) au lieu des seuls intérêts nets.
**Fix backend** (`backend/src/routes/investissements.js`) : ajout d'une sous-requête `SUM(r.interets_nets)` retournée sous le nom `interets_nets_total`, en plus du `net_recu_total` conservé.
**Fix frontend** (`frontend/src/pages/Investissements.jsx`) : tous les usages de `net_recu_total` pour afficher des intérêts nets remplacés par `interets_nets_total`.
**Why:** `net_recu = capital + cashback + interets_nets` — utiliser ce champ pour afficher des "intérêts" gonflait artificiellement la valeur du capital remboursé.
**How to apply:** Pour tout calcul d'intérêts nets sur les investissements, utiliser `interets_nets_total` (SUM des interets_nets des remboursements). `net_recu_total` reste disponible si on veut afficher le flux total reçu.
@@ -0,0 +1,109 @@
---
name: project_invchart_capital_encours
description: "Formule unifiée capital investi (Dashboard + Investissements) : montant_investi + reinvests(≤date) capital_remboursé(≤date)"
metadata:
node_type: memory
type: project
originSessionId: 09de8d8d-fa6d-4d2f-93bf-3920d2b93158
---
## Formule canonique — capital restant engagé à une date D
```
capital_restant(D) = montant_investi
+ SUM(reinvestissements où date_reinvestissement ≤ D)
SUM(remboursements.capital où date_remb ≤ D AND type='normal')
```
Cette formule doit être appliquée identiquement sur toutes les pages et tous les composants.
**Why:** Des divergences existaient car certains endroits utilisaient `capital_total` (tous reinvests sans filtre date) ou `montant_investi` brut (sans soustraire le capital remboursé). Corrigé en session 2026-05-29.
---
## Dashboard — backend (dashboard.js)
### portfolio.encours / portfolio.en_defaut
La requête SQL utilise des sous-requêtes corrélées :
```sql
SELECT i.id, ...
COALESCE(SUM(CASE WHEN i.statut='en_cours' THEN
i.montant_investi
+ COALESCE((SELECT SUM(rv.montant) FROM reinvestissements rv WHERE rv.investissement_id = i.id), 0)
- COALESCE((SELECT SUM(rb.capital) FROM remboursements rb WHERE rb.investissement_id = i.id AND rb.type='normal'), 0)
END), 0) AS encours,
-- idem pour en_defaut (en_retard + procedure)
FROM investissements i WHERE ...
```
### capitalMensuel (interets-par-plateforme)
Boucle sur les 12 mois de l'année demandée, pour chaque investissement :
```js
capital_restant = montant_investi
+ reinvestsCumul( lastDay) // depuis reinvestMap
- capitalRembCumul( lastDay) // depuis capitalRembMap (type='normal')
- capitalSimulCumul // depuis simul_remboursements :
// date_prevue > today
// ET date_prevue > last_remb_date
// ET date_prevue ≤ lastDay
```
`capitalSimulCumul` permet aux années futures (2027+) de décrémenter le capital selon les projections de remboursement.
### KPI "Capital investi" Dashboard (Dashboard.jsx — DashboardKpis)
- **Mode global / année courante** → `portfolio.encours + portfolio.en_defaut`
- **Année passée** → dernier mois de `capitalMensuelData` pour cette année (ex. décembre 2025 = 24 345,13 €)
- `capitalMensuelData` est remonté via callback `onCapitalMensuel` depuis `TableauInteretsPlateforme`
- `prevCapitalAnnee` (référence N-1 pour TrendBadge) utilise encore `capitalParAnneeMap` (capital souscrit par année) faute de données N-1 disponibles
---
## Page Investissements (Investissements.jsx)
### Données chargées
- `allRembs` : tous les remboursements (scope courant)
- `allReinvests` : tous les réinvestissements via `GET /api/reinvestissements?scope=all`
### Memos de coupure (cutoff = platYear ? `${platYear}-12-31` : null)
- `rembParInv``{ [inv_id]: { capital, interets_bruts, interets_nets } }` filtrés par cutoff
- `capRembParInv` → alias `{ [inv_id]: capital }` pour rétrocompatibilité
- `reinvestCumulParInv``{ [inv_id]: montant_cumulé }` filtrés par cutoff
- `lastRembDateMap``{ [inv_id]: date_dernière_remb }` calculé depuis `allRembs`
### Filtre année (platYear) — sémantique "actif au 31/12"
`isActiveAtEndOfYear(r)` : investissement inclus si :
1. `date_souscription ≤ ${platYear}-12-31`
2. ET statut en [`en_cours`, `en_retard`, `procedure`] (actif aujourd'hui)
OU `date_cible ?? lastRembDateMap[r.id] >= ${platYear}-12-01` (clôturé en décembre ou après)
Remplace l'ancien filtre `date_souscription.slice(0,4) === platYear` dans `kpiRows`, `platData`, et `chartRows`.
**Why:** Le filtre par année de souscription excluait les prêts souscrits en 2024 encore actifs en 2025, créant une divergence avec le Dashboard (24 345 € vs 16 118 €).
### Calcul capInv dans totals / platData / invDistRows
```js
const capInv = r.montant_investi + (reinvestCumulParInv[r.id] || 0);
const capRemb = capRembParInv[r.id] || 0;
const capRestant = Math.max(0, capInv - capRemb);
```
### InvChart.jsx
Props : `rows`, `remboursements`, `reinvestissements`, `platYear`
Deltas chronologiques :
- `+montant_investi` à `date_souscription` (plus `capital_total` — supprimé)
- `+reinvestissement.montant` à `date_reinvestissement` (nouveau)
- `remboursement.capital` à `date_remb`
---
## GET /api/reinvestissements — mode scope=all
Route `reinvestissements.js` étendue pour supporter `?scope=all` (tous les reinvestissements de l'utilisateur, via JOIN investissements + investisseurs). Utilisé par la page Investissements au chargement.
@@ -0,0 +1,27 @@
---
name: project-investissement-detail-features
description: "Fonctionnalités ajoutées à InvestissementDetail — modifier DPE, traitement en masse remboursements, correction ConfirmModal"
metadata:
node_type: memory
type: project
originSessionId: 06bf2c9d-6350-45bd-a65f-4399f1218408
---
## Fonctionnalités du menu ⋮ bloc Projections (simulMenu)
Trois items dans le menu `simulMenu` de la carte "Projections de remboursements", dans cet ordre :
1. **Modifier la date de la 1ère échéance** — ouvre `editDpeModal` ; champ date pré-rempli avec `inv.date_premiere_echeance` ; sauvegarde via `PUT /investissements/:id` (payload minimal, pas de spread `inv`) puis `POST /simul/recalculate``load()`.
2. **Traitement en masse des remboursements** — ouvre `bulkRembModal` ; calcule les échéances simul antérieures à aujourd'hui sans remboursement correspondant (match exact date ou mois, sans fallback capRestant), affiche tableau récap + totaux, crée les remboursements séquentiellement via `POST /remboursements` avec barre de progression, écran succès final.
3. **Régénérer l'échéancier** — existant.
## États ajoutés à InvestissementDetail
- `editDpeModal`, `editDpeValue`, `editDpeErr`, `editDpeSaving`
- `bulkRembModal`, `bulkRembItems`, `bulkRembProcessing`, `bulkRembProgress`, `bulkRembDone`
## Bug corrigé — ConfirmModal manquant
Le state `rowDeleteConfirm` (suppression remboursement via menu ⋮ du tableau) était alimenté mais le composant `<ConfirmModal>` n'était jamais monté dans le JSX. Ajouté en fin de template.
**Why:** Oubli lors d'une précédente session — le state existait mais le point de montage manquait.
**How to apply:** Toujours vérifier que chaque état de type `*Confirm` ou `*Modal` a bien son composant `<ConfirmModal>` ou `<Modal>` correspondant dans le return.
@@ -0,0 +1,75 @@
---
name: project_istaxindicatif
description: Règle isTaxIndicatif — prelev indicatifs vs réels, net_recu brut vs net ; route reprocess en masse
metadata:
node_type: memory
type: project
originSessionId: 5682d634-7345-40a8-8184-f617d1dd8444
---
## Règle centrale `isTaxIndicatif`
```js
isTaxIndicatif = (fiscalite !== 'flat_tax') OR (fiscalite_override === 'exonere')
```
Cas couverts :
- Plateforme `sans_fiscalite_locale` ou `avec_fiscalite_locale` (étrangères) → indicatif
- Plateforme `flat_tax` mais investissement `exonere` (PEA-PME) → indicatif
- Plateforme `flat_tax` sans override → prelev réels
## Comportement selon la règle
| `isTaxIndicatif` | Prelev calculés ? | Champs prelev | `net_recu` |
|---|---|---|---|
| `false` | Oui, réels | Modifiables | capital + cashback + interets_nets |
| `true` | Oui, indicatifs | Lecture seule, label "indicatif" | capital + cashback + interets_bruts |
**Pourquoi** : les plateformes étrangères et les PEA-PME ne prélèvent pas la flat tax à la source. Le montant versé à l'investisseur est le brut. Les prelev sont stockés à titre informatif pour la déclaration fiscale personnelle.
## Backend — `remboursements.js`
### Helper `getTaxIndicatif(investissementId)`
```js
function getTaxIndicatif(investissementId) {
const row = db.prepare(`
SELECT p.fiscalite, i.fiscalite_override
FROM investissements i JOIN plateformes p ON p.id = i.plateforme_id
WHERE i.id = ?
`).get(investissementId);
return row.fiscalite !== 'flat_tax' || row.fiscalite_override === 'exonere';
}
```
### `computeChamps(body, isTaxIndicatif)`
```js
const interets_nets = round2(body.interets_bruts - body.prelev_sociaux - body.prelev_forfaitaire);
const net_recu = isTaxIndicatif
? round2(capital + cashback + body.interets_bruts)
: round2(capital + cashback + interets_nets);
```
Utilisé dans POST et PUT handlers.
## Frontend — `InvestissementDetail.jsx`
```js
const isExonere = inv?.fiscalite_override === 'exonere';
const isIndicatif = isExonere || currentPlat?.fiscalite !== 'flat_tax';
```
- Prelev + interets_nets : `readOnly={isIndicatif}`, style grisé (`var(--surface-2)`), label "— indicatif"
- Recalcul auto des prelev depuis taux PFU dans `updateRembField` : toujours calculé, même en mode indicatif
- Montant remboursé affiché = capital + cashback + interets_bruts si indicatif
## Route reprocess en masse
`POST /api/remboursements/reprocess` — recalcule tous les remboursements `type='normal'` de l'utilisateur connecté (tous ses investisseurs).
Champs recalculés : `taxe_locale`, `interets_bruts`, `prelev_sociaux`, `prelev_forfaitaire`, `interets_nets`, `net_recu` + montant du retrait auto associé (`depots_retraits` avec `source='auto_remboursement'`).
Logique : join investissements + plateformes + investisseurs, taux PFU de l'année du remboursement, `isTaxIndicatif` appliqué à `net_recu`.
Déclenchement UI : bouton dans `Settings.jsx` section `nettoyage` (groupe "Mes données") → modale de confirmation → appel `api.post('/remboursements/reprocess', {})`. ⚠️ Anciennement dans `MonCompte.jsx` — déplacé en Settings lors de la réorganisation NAV (juin 2026).
**How to apply** : toute nouvelle route ou formulaire manipulant `prelev_sociaux`, `prelev_forfaitaire`, `interets_nets`, `net_recu` doit appliquer cette règle. Checker `getTaxIndicatif()` (backend) ou `isIndicatif` (frontend).
@@ -0,0 +1,17 @@
---
name: Logo plateforme dans la topbar d'InvestissementDetail
description: Si la plateforme a un logo, il remplace le nom textuel dans la topbar ; dark mode via .logo-plateforme CSS
type: project
originSessionId: f8882910-b8d1-4a87-ab0b-2962aa6aad7b
---
Dans `InvestissementDetail.jsx`, la topbar affiche le logo de la plateforme si disponible, sinon le nom textuel.
**Backend :** `GET /api/investissements/:id` retourne `plateforme_logo` (alias de `p.logo_filename`) via le JOIN sur plateformes. Aussi `plateforme_fiscalite` pour la logique brut/net du réinvestissement auto.
**Frontend :**
- URL du logo : `` `${(import.meta.env.VITE_API_URL || '/api').replace(/\/api$/, '')}/api/logos/${inv.plateforme_logo}` ``
- Classe CSS `logo-plateforme` (définie dans `styles.css`) gère le mode sombre : `filter: invert(1) hue-rotate(180deg)` sur `[data-theme="dark"]`
- Taille : `height: 38px, maxWidth: 140px, objectFit: contain`
- Le nom de la plateforme reste en `title` sur l'image pour le survol
**Bouton retour :** "← Investissements" déplacé à droite de la topbar (à la place des 3 anciens boutons Exporter/Réinvestir/Modifier supprimés).
@@ -0,0 +1,33 @@
---
name: project_logos_referentiel
description: "Bibliothèque de logos dans Admin > Référentiels — upload PNG/SVG par plateforme du référentiel, auto-push vers plateformes liées"
metadata:
node_type: memory
type: project
originSessionId: 009410f3-ba29-4d50-9909-840d1411ff1a
---
Section "Logos des plateformes" ajoutée dans Admin > Référentiels (section id `logos-ref`).
**Backend (`backend/src/routes/referentiel.js`) :**
- `POST /api/referentiel/:id/logo` — upload multer (data/logos/), traitement fond blanc (sharp + SVG cleanup), update `logo_filename` dans `plateformes_referentiel`, auto-push vers plateformes liées non-overridées
- `DELETE /api/referentiel/:id/logo` — supprime fichier + NULL logo_filename + propagation
- Routes protégées par requireAdmin (via mount dans server.js)
- Fichiers nommés `ref_{id}_{timestamp}.{ext}`
**Frontend (`Admin.jsx`) :**
- Composant `LogosBibliothequeSection` : grille de cards, une par entrée référentiel
- Carte avec logo ou croix placeholder SVG + texte "Logo manquant" si `logo_filename` null
- Un seul `<input type="file">` caché partagé via `useRef` (pas `React.useRef` — React n'est pas importé en default dans Admin.jsx)
- Upload via `fetch` multipart (pas `api.post` car FormData)
- Bouton "Remplacer" ou "Importer" selon présence du logo ; bouton "×" pour suppression
- Dans `ReferentielSection` : miniature 32×32 ou `×` pointillé en 1ère colonne du tableau
**Héritage :**
- `logo_filename` est dans `HERITABLE_FIELDS` de plateformes.js → héritage via push/reset
- Auto-push immédiat lors de l'upload logo référentiel
- `InvestissementDetail` affiche déjà `plateforme_logo` (champ retourné par investissements.js)
**Why:** Permettre à l'admin de gérer les logos de toutes les plateformes en un seul endroit, avec propagation automatique vers les comptes utilisateurs et jusqu'aux fiches d'investissement.
**How to apply:** Toute nouvelle plateforme ajoutée au référentiel apparaît automatiquement dans la bibliothèque avec le placeholder croix — l'admin sait qu'il doit importer un logo. Après upload, le logo se propage via logo_filename aux plateformes liées.
@@ -0,0 +1,86 @@
---
name: menus-actions-sur-les-tableaux-et-blocs
description: "Pattern menu contextuel fixe (position: fixed + backdrop + icônes SVG) sur tableaux et headers de blocs ; voir aussi [[feedback_menu_actions_format]]"
metadata:
node_type: memory
type: project
originSessionId: 2fef7a75-e0f3-4f7d-9c80-79c3be16af03
---
Menus d'action ⋮ sur les tableaux :
| Page | Tableau | Actions disponibles |
|---|---|---|
| DepotsRetraits | Mouvements | Modifier / Correction de solde / Supprimer |
| Remboursements | Remboursements | Modifier / Supprimer |
| Investissements | Investissements | Voir le détail / Modifier / Supprimer |
| InvestissementDetail | Remboursements enregistrés | Modifier / Supprimer |
| InvestissementDetail | Réinvestissements complémentaires | Supprimer |
| AdminPlateformes — ReferentielSection | Référentiel plateformes | Modifier / Pousser (douce) / Pousser (forcée) / Supprimer |
| AdminPlateformes — RefListSection | Catégories d'investissement | Renommer / Fusionner avec… / Supprimer |
| AdminPlateformes — RefListSection | Secteurs d'investissement | Renommer / Fusionner avec… / Supprimer |
Menus ⋮ sur les **headers de blocs** (InvestissementDetail) — bouton 30×30 px, border var(--border), 3 cercles SVG, état séparé de `openMenu` :
| Bloc | État | Actions |
|---|---|---|
| Informations du projet | `cardMenu` | Modifier / Réinvestir / Exporter / Désactiver auto-reinvest (si actif) / Supprimer |
| Remboursements enregistrés | `rembMenu` | Ajouter un remboursement / Supprimer tous les remboursements |
| Réinvestissements complémentaires | `reinvMenu` | Ajouter un réinvestissement / Supprimer tous les réinvestissements |
| Projections de remboursements | `simulMenu` | Régénérer l'échéancier |
**Fermeture au scroll :** `useEffect` avec `window.addEventListener('scroll', closeAll, true)` — implémenté sur InvestissementDetail, DepotsRetraits, Investissements, Remboursements, AdminPlateformes. À reproduire sur toute nouvelle page avec menu contextuel.
**Pattern technique (version avec icônes — format actuel de référence) :**
```jsx
// Icônes SVG locales (13×13 px)
function IconEdit() { return <svg .../>; }
function IconTrash() { return <svg .../>; }
// État
const [openMenu, setOpenMenu] = useState(null); // { row, x, y }
// Fermeture scroll
useEffect(() => {
if (!openMenu) return;
const close = () => setOpenMenu(null);
window.addEventListener('scroll', close, true);
return () => window.removeEventListener('scroll', close, true);
}, [openMenu]);
// Ouverture
const handleMenuOpen = (e, row) => {
e.stopPropagation();
const rect = e.currentTarget.getBoundingClientRect();
setOpenMenu({ row, x: rect.right, y: rect.bottom + 4 });
};
// Rendu
{openMenu && (
<>
<div style={{ position: 'fixed', inset: 0, zIndex: 299 }} onClick={() => setOpenMenu(null)} />
<div style={{ position: 'fixed', left: openMenu.x, top: openMenu.y,
transform: 'translateX(-100%) translateY(4px)', zIndex: 300,
background: 'var(--surface)', border: '1px solid var(--border)',
borderRadius: 8, boxShadow: '0 4px 20px rgba(0,0,0,0.15)', padding: '4px 0', minWidth: 180 }}>
{[
{ icon: <IconEdit />, label: 'Modifier', onClick: () => { ... } },
{ icon: <IconTrash />, label: 'Supprimer', color: 'var(--danger)', onClick: () => { ... } },
].map(({ icon, label, onClick, disabled, color }) => (
<button key={label} disabled={disabled}
style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '8px 14px',
background: 'none', border: 'none', cursor: disabled ? 'default' : 'pointer',
fontSize: 'var(--fs-sm)', color: color || 'var(--text)', opacity: disabled ? 0.5 : 1 }}
onMouseEnter={e => { if (!disabled) e.currentTarget.style.background = 'var(--surface-2)'; }}
onMouseLeave={e => e.currentTarget.style.background = 'none'}
onClick={disabled ? undefined : onClick}>
<span style={{ opacity: 0.7, flexShrink: 0, display: 'flex' }}>{icon}</span>
{label}
</button>
))}
</div>
</>
)}
```
**Why:** Actions inline remplacées par menus contextuels compacts pour garder les tableaux lisibles. Les icônes ont été ajoutées suite à la demande d'Olivier pour aligner avec le style de l'image de référence.
@@ -0,0 +1,36 @@
---
name: project_methode_remboursement_investissement
description: "Méthode de remboursement et nom du compte courant sur les investissements — champs DB, formulaires, retrait automatique"
metadata:
node_type: memory
type: project
originSessionId: 5682d634-7345-40a8-8184-f617d1dd8444
---
## Champs ajoutés sur `investissements`
| Colonne | Type | Rôle |
|---|---|---|
| `methode_remboursement` | TEXT nullable | `'portefeuille'` \| `'compte_courant'` \| null |
| `nom_compte_courant` | TEXT nullable | Nom du compte bancaire quand methode = compte_courant |
## Règle d'affichage dans les formulaires
Le select "Méthode de remboursement" n'apparaît que si la plateforme sélectionnée a `methode_remboursement = 'choix_investisseur'`. Implémenté dans **Investissements.jsx** et **InvestissementDetail.jsx** (formulaire d'édition).
Le champ "Nom du compte courant" (obligatoire) apparaît quand `methode_remboursement === 'compte_courant'`. Il utilise un `<input list=...>` avec `<datalist>` peuplé depuis `GET /api/investissements/comptes-courants` (retourne les paires `{investisseur_id, nom_compte_courant}` distinctes, filtré côté client par `form.investisseur_id`).
## Retrait automatique dans dépôts/retraits
Quand un remboursement est enregistré avec `methode_remboursement = 'compte_courant'`, la fonction `syncAutoRetrait()` dans `remboursements.js` crée automatiquement un retrait dans `depots_retraits` avec `source = 'auto_remboursement'` et `remboursement_id = rembId`. Ce retrait est supprimé/recréé à chaque PUT et supprimé au DELETE du remboursement.
Ces retraits automatiques sont **exclus** du calcul du solde porte-monnaie (filtre `source !== 'auto_remboursement'`).
## Affichage fiche détail
`inv.methode_remboursement` affiché dans "Informations du projet" :
- `'portefeuille'` → "Porte-monnaie de la plateforme"
- `'compte_courant'` → "Compte courant de l'investisseur (nom_compte_courant)"
**Why:** Baltis et d'autres plateformes françaises laissent le choix à l'investisseur entre porte-monnaie et virement bancaire direct.
**How to apply:** Toujours vérifier que les deux formulaires (liste + détail) et le pré-remplissage du formulaire remboursement sont cohérents. Voir [[feedback_remb_methode_fallback]].
@@ -0,0 +1,55 @@
---
name: project_notifications
description: "Système de notifications in-app — table DB, routes backend, cloche topbar, page /notifications"
metadata:
node_type: memory
type: project
originSessionId: dc9d0717-dc15-48a7-a013-2988b2e0ad4a
---
## Architecture
**Table SQLite** : `notifications` (id, user_id FK, type, title, body, link, read, created_at). Les notifications sont strictement personnelles (filtrées par `user_id` via JWT sur toutes les routes).
**Routes backend** `backend/src/routes/notifications.js` :
- `GET /` — liste paginée (params: limit, offset, unread_only, type)
- `GET /count` — nombre non lus (polling)
- `PATCH /read-all` — tout marquer lu
- `PATCH /:id/read` — marquer un lu
- `DELETE /:id` — supprimer une notif
- `DELETE /` — tout supprimer
- `POST /seed` — générer des notifs de test (requireAdmin)
⚠️ Routes statiques (`/count`, `/read-all`, `/seed`) déclarées **avant** `/:id` pour éviter le conflit Express.
**Composants frontend** :
- `NotifTypeAvatar.jsx` — source unique des icônes SVG (paths Lucide) et couleurs par type ; exporte `TYPE_META` et le composant par défaut
- `NotificationBell.jsx` — cloche dans la topbar, polling 30s via `fetchCount`, écoute `window` event `notif:refresh` pour rechargement immédiat
- `pages/Notifications.jsx` — page `/notifications`, layout fragment + `.topbar` hidden, bloc card 65% centré, bloc Simulation admin en dessous
**Événement cross-composant** : `window.dispatchEvent(new CustomEvent('notif:refresh'))` à dispatcher après toute création de notification pour forcer le rechargement immédiat du compteur cloche.
## Types de notifications
| Type | Label | Icône Lucide | Couleur |
|---|---|---|---|
| `system` | Système | Radio | Gris |
| `ticket_reply` | Ticket | FileText | Bleu |
| `info` | Info | Info | Bleu ciel |
| `team` | Équipe | Users | Violet |
| `success` | Succès | Check | Vert |
| `warning` | Avertissement | AlertTriangle | Ambre |
| `security` | Sécurité | ShieldAlert | Rouge |
| `announcement` | Annonce | Megaphone | Rose |
Les icônes sont des SVG inline (paths Lucide) car `lucide-react` n'est pas installé dans le projet.
## CSS notable
- `.notif-bell-btn` : sans fond, sans bordure, sans effet hover — juste l'icône et le badge
- `.notif-row-unread` / `.notif-item-unread` : `border-left: 3px solid var(--primary)` + fond primaire à 4-5% d'opacité
- `.notif-dropdown-list` : scrollbar fine (4px) via `scrollbar-width: thin`
- Layout page : fragment root + `.topbar` hidden → wrapper `.notif-center-wrap``.card.notif-block` (max-width 65%, margin auto)
**Why:** Notifications personnelles par user_id ; `notif:refresh` évite d'attendre le poll de 30s après une action.
**How to apply:** Toute future source de notifications doit dispatcher `notif:refresh` après insertion. Nouveaux types à ajouter dans `TYPE_META` de `NotifTypeAvatar.jsx`.
@@ -0,0 +1,27 @@
---
name: project_pagination
description: Pagination côté client sur les 3 pages de listes — hook usePagination + composant Pagination
metadata:
node_type: memory
type: project
originSessionId: f2be746c-47f1-426a-ae80-3c71059fa38c
---
Pagination implémentée sur Investissements, DepotsRetraits et Remboursements.
- Hook `src/hooks/usePagination.js` : tailles 15/25/50/100, défaut 15, localStorage par page, reset page 1 sur changement de filtres
- Composant `src/components/Pagination.jsx` : compteur "XY sur Z", sélecteur taille, boutons nav
- Clés localStorage : `cl_pagesize_inv`, `cl_pagesize_dr`, `cl_pagesize_remb`
- Les exports CSV/XLS/JSON visent toujours la liste filtrée complète, pas la page affichée
**Why:** Listes sans limite pouvant devenir longues.
**How to apply:** Toute nouvelle page avec liste longue doit utiliser `usePagination` avec une clé `cl_pagesize_<id>` dédiée.
## Mode focus (liste agrandie)
Chaque page a un état `listFocused` qui masque les graphiques, KPIs et onglets pour se concentrer sur la liste.
- Bouton expand/collapse (icône maximize/minimize) placé avant le bouton export dans l'en-tête de chaque section liste
- En mode focus : page size passe automatiquement à 25 ; au retour : 15
- Sur les onglets Vision mensuelle : bouton passé en prop `expandButton` aux composants `CapitalMensuelTable`, `DepotsMensuelTable`, `TableauInteretsPlateforme` — s'affiche après le bouton TOUT
- Le bouton expand et l'export doivent toujours être dans un wrapper `<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>` pour éviter un problème d'alignement centré
@@ -0,0 +1,21 @@
---
name: Modèle plateforme — détenteur & logo
description: Évolutions récentes du modèle Plateforme : investisseur_id (détenteur), date_ouverture, logo_filename, contrainte UNIQUE modifiée
type: project
originSessionId: b87fd892-4ff2-4b8b-b3e6-f0f92e303c8e
---
Trois champs ajoutés à la table `plateformes` via migrations dans `backend/src/db/index.js` :
- `investisseur_id` INTEGER FK → investisseurs (détenteur de la plateforme) — backfillé sur le compte principal
- `date_ouverture` TEXT (ISO)
- `logo_filename` TEXT — fichier stocké dans `data/logos/`, servi via `GET /api/logos/:filename` (statique, sans auth)
**Contrainte UNIQUE modifiée** : `UNIQUE(user_id, nom)``UNIQUE(user_id, nom, investisseur_id)` pour permettre la même plateforme détenue par deux investisseurs différents. Migration par recréation de table dans `index.js` (SQLite ne supporte pas DROP CONSTRAINT).
**Logo** : nommage `logo_{nom_sanitisé}_{timestamp}.{ext}` (timestamp pour éviter le cache navigateur). Upload via `POST /api/plateformes/:id/logo` (multer), suppression via `DELETE /api/plateformes/:id/logo`. Helmet configuré avec `crossOriginResourcePolicy: cross-origin`.
**Frontend** : `LOGO_BASE = (VITE_API_URL || '/api').replace(/\/api$/, '') + '/api/logos/'`. Helper `platInvestisseur(p)` reconstruit l'objet investisseur depuis les colonnes dénormalisées du JOIN.
**Why:** Permettre à plusieurs membres de la famille/entreprise de détenir chacun un compte sur la même plateforme (ex. deux comptes Tantiem).
**How to apply:** Toute requête SQL sur `plateformes` doit faire un `LEFT JOIN investisseurs inv ON inv.id = p.investisseur_id` et exposer `inv.nom AS investisseur_nom` etc. Les selects de plateforme dans les formulaires affichent `{p.nom} — {p.investisseur_nom}` pour disambiguïser.
@@ -0,0 +1,85 @@
---
name: project_plateformes_page
description: "Page /plateformes — vue par plateforme avec onglets Remboursements, Dépôts/Retraits, Capital investi, Investissements"
metadata:
node_type: memory
type: project
originSessionId: b8b23026-381b-4ec2-b150-02080d5f4720
---
Page `frontend/src/pages/Plateformes.jsx` créée pour une vision centrée plateforme.
## Structure générale
- Sélecteur Plateforme (carte violette), Sélecteur Détenteur (carte teal, visible si multi-détenteur), Sélecteur Année
- 5 KPIs avec TrendBadge (Capital encours, À risque, Investissements total, Capital remboursé, Intérêts brut/net)
- 4 onglets : **Remboursements** | **Dépôts / Retraits** | **Capital investi** | **Investissements**
## Onglet Remboursements
- Tableau mensuel par investissement (tip-table), même sélecteurs que `TableauInteretsPlateforme` :
- 3 boutons icônes (Intérêts/Capital/Cashback) depuis `/api/icons`
- Sélecteur années ` 2024 2025 ` + TOUT
- Toggle Détaillé/Consolidé (localStorage `cl_remb_plat_detaille`)
- Boutons Reçu / Projeté
- Données réelles depuis `allRembs`, projections depuis `GET /api/simul/all` (nouvel endpoint)
- `buildCellValue` : mois futur → projection seule ; mois courant → réel si dispo, sinon projection (jamais les deux) ; mois passé → réel seul
- Grisage `.tip-td-closed` : avant `firstNonNull` de la ligne, après `lastRembDate` pour statut `rembourse`
## Onglet Dépôts / Retraits
- Tableau mensuel Dépôts / Retraits / Corrections / Net par mois
- Filtre plateforme multi-sélect (custom dropdown, `plateforme_ids[]`) dans DepotsRetraits.jsx
- Clic cellule → navigation `/depots-retraits?tab=mouvements&type=...&plat_ids=...`
- Toggle Détaillé/Consolidé (localStorage `cl_dr_plat_detaille`) groupant par détenteur
- Sélecteur années partagé avec les autres onglets
- Grisage `.tip-td-closed` : avant date souscription, après dernier remb (prêts remboursés)
## Onglet Capital investi
- Composant `InvMensuelTable.jsx` : capital encours par investissement par mois
- Grisage : avant `date_souscription` de l'investissement, après dernier remboursement pour statut `rembourse`
## Backend — nouvel endpoint
`GET /api/simul/all` dans `backend/src/routes/simul.js`, inséré AVANT `router.use(requireInvestisseur)` :
- Supporte `scope=all` → filtre par `user_id`
- Retourne `simul_remboursements` avec `plateforme_id` et `investisseur_id` joints
## CSS
`.tip-td-closed` : `background: var(--surface-2)` avec override pour `.tip-col-current` (fond violet prioritaire).
## Comportements UX
- Changer de plateforme conserve l'onglet actif (pas de reset sur `setActiveTab`)
- `rembShowProjected && proj && real === 0` : la projection n'est ajoutée au mois courant que si aucun réel n'existe (anti-doublon). Même correction dans `TableauInteretsPlateforme.jsx`
## Corrections et évolutions (2026-06-18)
### CSS en-têtes tableaux
- `.tip-th-name` (colonne PLATEFORME/Investissement) passe au dégradé violet comme `.tip-th-year` — texte blanc, border `rgba(255,255,255,.2)`
- `.tip-td-avg` reçoit le même fond subtil que `.tip-td-total`
- Colonne STATUT dans l'onglet Remboursements et dans `InvMensuelTable` : `<span className={`badge ${inv.statut}`}>` (plus d'inline styles) ; `<th>` STATUT utilise `tip-th-name`
- `STATUT_BG` / `STATUT_FG` supprimés de `InvMensuelTable.jsx`
- Titres "Mouvements de trésorerie" et "Capital investi par investissement" → `<h3>` avec span muted pour l'année (aligné sur "Investissements")
### UX / navigation
- Onglet par défaut : `'remboursements'` (était `'depots-retraits'`)
- Bouton Agrandir/Réduire ajouté sur les onglets Dépôts/Retraits et Investissements (les deux onglets Remboursements et Capital investi l'avaient déjà)
- Onglet Investissements : pagination via `usePagination` (clé `cl_pagesize_plat_inv`), tri par date_souscription dans `sortedChartRows`
### Persistance des critères (localStorage)
- `cl_plat_selected` → plateforme sélectionnée (préférée à `platOptions[0]` à l'init)
- `cl_plat_tab` → onglet actif
- `cl_plat_year` → année sélectionnée
- `cl_plat_detenteur` → détenteur sélectionné (restauré sur la même plateforme ; remis à `all` si l'utilisateur change de plateforme, via `isFirstPlatChange` ref qui skip la restauration initiale)
### Onglet Investissements — colonnes tableau (2026-06-18)
- En-tête corrigé : "Émetteur" → "Détenteur" dans le `<th>`
- "Date" → "Date de souscription"
- Colonne "Date cible" ajoutée après "Date de souscription" (thead + tbody alignés, 8 colonnes)
- Thead final : Projet | Détenteur | Date de souscription | Date cible | Montant | Capital restant | Intérêts (Brut/Net) | Statut
**Why:** Page demandée pour avoir une vision centrée plateforme en complément des vues globales.
**How to apply:** Plateformes.jsx fait ~1564 lignes — toujours utiliser Python pour les éditions, jamais Edit/Write direct (risque de troncature).
@@ -0,0 +1,29 @@
---
name: project-profil-import-ia-tags
description: ProfilImportBlock — import IA avec résolution noms→IDs et création de tags inconnus
metadata:
node_type: memory
type: project
originSessionId: 264d200a-fea4-4903-b7c5-20dcc9ebfbd2
---
L'import IA dans `PlatformeProfile.jsx` (composant `ProfilImportBlock`) gère maintenant correctement `categories_inv` et `secteurs_inv`.
**Flow en 3 étapes** (state `step` : `'json'``'creation'``'diff'`) :
1. **JSON** : saisie + "Analyser les changements →"
2. **Création** (seulement si tags inconnus) : panneau ambre listant les nouveaux tags avec cases à cocher (toutes cochées par défaut). L'utilisateur valide ou décoche → "Continuer vers le diff →"
3. **Diff** : tableau normal + bouton "Appliquer"
**`resolveTags(aiNames, knownList, currentItems)`** : helper inline dans le composant
- Normalise les noms (lowercase + accents supprimés)
- Similarité : inclusion OU Levenshtein ≤ 2
- Retourne `{ resolvedIds, resolvedNames, unknownNames, currentNames, hasChange }`
**`apply()`** :
- Pour chaque tag inconnu approuvé (`toCreate[nom] === true`) : crée via `POST /ref-categories` ou `POST /ref-secteurs`, récupère l'ID
- Inclut `categories_inv_ids` et `secteurs_inv_ids` dans le payload PUT `/referentiel/:id`
- Corrige le bug ancien où ces champs n'étaient jamais sauvegardés
**Prompt IA** : `categories_inv` et `secteurs_inv` encouragent l'IA à utiliser les valeurs existantes mais autorisent les noms nouveaux si la plateforme y appartient clairement.
**Props** : `ProfilImportBlock` reçoit maintenant `catsInv` et `secteursInv` (chargés depuis `/ref-categories` et `/ref-secteurs` dans le composant parent).
@@ -0,0 +1,23 @@
---
name: project-projections-display
description: "Amélioration affichage projections InvestissementDetail — montants barrés, échéances non tenues"
metadata:
node_type: memory
type: project
originSessionId: 2824251c-280e-4f17-8623-a78ad9304896
---
Améliorations apportées à la table "Projections de remboursements" dans `InvestissementDetail.jsx`.
**Montants réels vs prévus (barré + réel) :**
- Quand un remboursement est payé mais le montant diffère de la projection : affiche le montant prévu barré en gris + le montant réel en gras.
- Appliqué aux colonnes Intérêts ET Total.
- Utilise `matchRembsAll` qui agrège **tous** les remboursements du mois (pas de priorité date exacte) pour gérer les rattrapages multi-versements.
**Why:** Un prêt peut avoir plusieurs versements dans le même mois (rattrapage de mois manqués), ou le capital peut être remboursé en plusieurs tranches à des dates différentes dans le mois. La `Map` standard n'en garde qu'un — il faut `rembsArrayByMonth` (Map<mois, remb[]>) et sommer.
**Échéances passées non payées :**
- Si `date_prevue < today` et aucun remboursement correspondant : affiche barré + 0,00 € dans les colonnes montants, et statut **✗ Échéance non tenue** en rouge.
- Les projections futures restent affichées normalement avec statut "En attente".
**How to apply:** Pour tout futur travail sur InvestissementDetail, ne pas toucher `matchRemb` (utilisé pour le clic/ouverture modale) mais utiliser `matchRembsAll` pour les comparaisons de montants.
@@ -0,0 +1,98 @@
---
name: project-referentiel-plateformes
description: "Système référentiel commun pour les plateformes — architecture, tables, champs héritables, mécanismes de sync admin→user"
metadata:
node_type: memory
type: project
originSessionId: 7f63e6b8-eac0-45e1-bbd4-3fad774e7e8d
---
# Référentiel des plateformes
Fonctionnalité permettant à l'admin de gérer un référentiel commun (`plateformes_referentiel`) dont les utilisateurs héritent lors de la création de leurs plateformes.
**Why:** Centraliser les données communes (domiciliation, fiscalité, catégories) pour éviter la saisie répétée et garantir la cohérence entre tous les comptes utilisateurs.
**How to apply:** Toujours maintenir la cohérence entre les deux tables lors d'évolutions de schéma. Voir aussi la règle dans CLAUDE.md §7.
---
## Tables DB
### `plateformes_referentiel`
id, nom (UNIQUE), url, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal, logo_filename, **methode_remboursement**, **type_pret_defaut**, **freq_interets_defaut**, description, created_at, updated_at
**Note :** `type_investissement` et `secteur` existent encore en DB mais ne sont plus utilisés dans le code (remplacés par categories_inv/secteurs_inv). `duree_defaut` et `taux_defaut` de `plateformes` ont aussi été supprimés du code (colonnes DB conservées).
### `referentiel_categories`
referentiel_id FK + categorie_nom TEXT (string, pas FK vers categories_plateforme — les catégories user sont scoped)
### `referentiel_notation`
id, referentiel_id FK, nom, type, valeurs (JSON), min_val, max_val, description, ordre, created_at
### Colonnes ajoutées à `plateformes`
- `referentiel_id INTEGER REFERENCES plateformes_referentiel(id) ON DELETE SET NULL`
- `overridden_fields TEXT NOT NULL DEFAULT '[]'` — JSON array des champs modifiés par l'user
---
## Champs héritables scalaires (HERITABLE_FIELDS)
```js
['nom', 'url', 'domiciliation', 'fiscalite', 'taux_fiscalite_locale', 'type_produit_fiscal', 'logo_filename', 'icone_filename', 'methode_remboursement', 'type_pret_defaut', 'freq_interets_defaut']
```
Définis dans `backend/src/routes/plateformes.js`. Identiques dans `referentiel.js` (PUSHABLE, sans `icone_filename`).
**Héritage tags (catégories/secteurs d'investissement) :** mécanisme séparé via merge — voir [[project-categories-secteurs-inv-heritage]].
---
## Routes backend
### Admin — `/api/referentiel` (requireAdmin)
- `GET /` — liste avec nb_plateformes_liees
- `POST /` — créer une entrée
- `PUT /:id` — modifier
- `DELETE /:id` — supprimer (délie les plateformes)
- `GET /:id/plateformes` — plateformes liées
- `POST /:id/push` — pousse les champs vers toutes les plateformes liées ; deux modes :
- **Douce** (défaut, `force` absent) : respecte `overridden_fields`, ignore les champs modifiés par l'user
- **Forcée** (`{ force: true }`) : écrase tous les champs et réinitialise `overridden_fields = '[]'` sur chaque plateforme
- `GET /export` — export ZIP (json + images)
- `POST /import-zip` — import ZIP avec résolution des tags par nom
- **⚠️ Routes statiques (/export, /import-zip) TOUJOURS avant `/:id`**
### Admin — `/api/admin`
- `GET /plateformes-orphelines` — plateformes sans referentiel_id, enrichies d'une suggestion de liaison (Levenshtein ≥ 80%)
- `POST /plateformes-orphelines/:id/importer` — crée une entrée référentiel depuis la plateforme
- `POST /plateformes-orphelines/:id/lier` — liaison directe à un référentiel existant sans import
### User — `/api/plateformes`
- `GET /referentiel-list` — lecture seule du référentiel (requireAuth, pas admin)
- `POST /:id/reset` — réinitialise les champs héritables depuis le référentiel, vide overridden_fields
---
## Logique frontend
### Settings.jsx
- Dropdown "Basé sur le référentiel" en tête du formulaire de création → pré-remplit champs scalaires
- `referentiel_id` transmis au POST
- Formulaire d'édition : bandeau indigo si lié + badge "hérité" par champ + bouton "Réinitialiser au référentiel"
- `openEditPlat()` charge `referentiel_id`, `referentiel_nom`, `overridden_fields`, `inherited_cat_ids`, `inherited_sect_ids`
- Bandeau affiche : `{N} champ(s) hérité(s)` + `+ catégories & secteurs` si applicable
- `inheritedCount` basé sur les 10 champs scalaires heritables
- InvSelect catégories/secteurs reçoit `inheritedIds` → items hérités toujours cochés + désactivés + badge "Réf"
### Admin.jsx (AdminPlateformes)
- Section `referentiel` : CRUD + menu ⋮ (Modifier / Pousser douce / Pousser forcée / Supprimer)
- Formulaire référentiel inclut : méthode de remboursement, type de prêt défaut, périodicité intérêts défaut
- Section `orphelines` : badge similarité ambre + bouton "Lier" / "Importer"
---
## Algorithme similarité noms (admin.js)
Levenshtein normalisé, insensible à la casse et aux accents, seuil 80%.
`similarity(a, b) = 1 - levenshtein(normalize(a), normalize(b)) / max(len(a), len(b))`
@@ -0,0 +1,16 @@
---
name: project_referentiel_search_filter
description: "Recherche, filtres et pagination ajoutés à ReferentielSection dans Admin.jsx"
metadata:
node_type: memory
type: project
originSessionId: f7c8b8d5-8019-47f1-a337-6c144cef302c
---
Recherche par nom (pleine largeur), filtre domiciliation et filtre catégorie ajoutés à `ReferentielSection` dans `Admin.jsx`.
**Pourquoi :** Amélioration UX pour naviguer dans un référentiel croissant.
**How to apply :** Les catégories disponibles sont dérivées dynamiquement avec `useMemo` depuis les données chargées. Pagination via `usePagination` avec clé `cl_pagesize_referentiel`. Bouton "Effacer les filtres" conditionnel. Layout : champ recherche pleine largeur, puis ligne domiciliation + catégorie côte à côte.
**Icône vs logo dans la colonne image :** `icone_filename` (icône bibliothèque Admin) est prioritaire sur `logo_filename` (logo uploadé). Les deux sont stockés dans `data/logos/` et servis via `/api/logos/` — ne jamais utiliser `/api/icons-files/` pour `icone_filename` du référentiel.
@@ -0,0 +1,49 @@
---
name: Fonctionnalité réinvestissements
description: Ajout de capital complémentaire sur un prêt en cours — architecture complète DB/backend/frontend
type: project
originSessionId: 680bfd99-577f-454e-b5b8-ea58460277f9
---
## Fonctionnalité : réinvestissements sur un investissement existant
Implémentée en mai 2026. Permet d'ajouter une ou plusieurs sommes complémentaires à un prêt en cours.
### Base de données
- Nouvelle table `reinvestissements` : `id`, `investissement_id` (FK cascade), `montant`, `date_reinvestissement`, `note`, `created_at`
- Index : `idx_reinv_inv ON reinvestissements(investissement_id, date_reinvestissement)`
- Migration dans `backend/src/db/index.js` (pattern habituel `CREATE TABLE IF NOT EXISTS`)
- `montant_investi` dans `investissements` reste l'initial — ne jamais le modifier pour un réinvestissement
### Backend
- Route : `backend/src/routes/reinvestissements.js` — GET (liste par investissement_id), POST, DELETE
- Montée dans `server.js` : `app.use('/api/reinvestissements', requireAuth, reinvestissementsRouter)`
- GET `/api/investissements/:id` retourne maintenant `reinvestissements`, `reinvestissements_total`, `capital_total`
- GET `/api/investissements/` retourne `reinvestissements_total` et `capital_total` (subqueries SQL)
### Logique de simulation (`backend/src/utils/schedule.js`)
- Nouvelle fonction exportée : `generateSimulWithReinvestissements(db, investissementId)`
- Génère le planning complet en appliquant le capital cumulatif période par période
- Appelée à chaque POST/DELETE sur `/api/reinvestissements`
- `adjustSimulForActuals` **modifiée** pour tenir compte des réinvestissements :
- Calcule `capitalAtLastDate = montant_investi + reinvestissements jusqu'à la date du dernier remb`
- Les réinvestissements futurs (après dernière date remb) gonflent progressivement les périodes suivantes
- Si aucun capital remboursé → bifurque vers `generateSimulWithReinvestissements` si des reinvests existent
**Why:** `adjustSimulForActuals` est appelée après chaque saisie de remboursement. Sans cette modification, elle recalculait la projection sur `montant_investi` seul, ignorant les réinvestissements.
### `remboursements.js` — `syncInvestissementStatut`
- Corrigé pour utiliser `capitalTotal = montant_investi + SUM(reinvestissements)` au lieu de `montant_investi` seul
- Évite de passer le statut à "rembourse" trop tôt quand du capital réinvesti reste dû
### Frontend (`InvestissementDetail.jsx`)
- Bouton "Réinvestir" dans la topbar (même style qu'Exporter)
- Modal de saisie : montant + date + note optionnelle
- Tableau récapitulatif conditionnel (affiché uniquement si reinvests > 0) avec colonne "capital cumulé" et suppression inline
- KPI "Montant investi" → "Capital total investi" si reinvests, avec sous-libellé "Initial X + Y réinvesti"
- XIRR : chaque réinvestissement est un flux sortant supplémentaire `{ amount: -montant, date }`
### `Investissements.jsx` (liste)
- Remplace `r.montant_investi` par `(r.capital_total ?? r.montant_investi)` dans les agrégats KPI, par-plateforme, tableau et export Excel
- Rétro-compatible : fallback sur `montant_investi` si pas de reinvests
**How to apply:** Toute future page affichant le capital d'un investissement doit utiliser `capital_total` (depuis l'API) plutôt que `montant_investi` brut.
@@ -0,0 +1,47 @@
---
name: project_settings_admin_refacto
description: Refactorisation Settings.jsx (3070 lignes) et Admin.jsx en composants autonomes — structure finale et imports manquants corrigés
metadata:
node_type: memory
type: project
originSessionId: ab8b89ba-75a8-4b6e-a0b4-5180e6c20e4a
---
Settings.jsx et Admin.jsx ont été découpés en composants autonomes (chacun avec son propre fetch, sans props drilling depuis le parent).
**Structure finale :**
- `pages/Settings.jsx` → shell 97 lignes, routing `?section=` uniquement
- `pages/Admin.jsx` → shell 74 lignes, routing `?section=` uniquement
- `pages/admin/adminHelpers.jsx``fmt`, `Badge`, `StatusBadge`
- `pages/admin/UsersSection.jsx` — fetch `/admin/users`
- `pages/admin/CreateUserSection.jsx` — POST `/admin/users`
- `pages/admin/JobLogsSection.jsx` — fetch `/admin/job-logs`
- `pages/admin/IconsSection.jsx` — fetch `/icons`
- `pages/settings/AppearanceSection.jsx` — thème, police, couleurs graphiques
- `pages/settings/PlateformesSection.jsx` — CRUD plateformes, export CSV/XLS, PlatDetailPanel
- `pages/settings/CategoriesInvSection.jsx` — CRUD catégories d'investissement
- `pages/settings/SecteursInvSection.jsx` — CRUD secteurs d'investissement
- `pages/settings/ComptesSection.jsx` — CRUD comptes courants/PEA-PME
- `pages/settings/MaFiscaliteSection.jsx` — toggle PFO
- `pages/settings/DataCleanupSection.jsx` — nettoyage données
- `pages/settings/ImportsSection.jsx` — import CSV/XLS/ZIP
**Imports à ne pas oublier dans les sections (erreurs rencontrées) :**
- `PlateformesSection` : `useNavigate` (react-router-dom), `memberLabel` + `fmtDate` (format.js), `ResultBanner`
- `ComptesSection` : `Modal`, `memberLabel` (format.js), `TYPE_COMPTE_LABELS` et `EXONERATION_LABELS` (constantes locales)
- `ImportsSection` : `ResultBanner`, `fmtDate` (format.js)
- `DataCleanupSection` : ne pas laisser traîner le bloc NAV de Settings (bug d'extraction)
**Bugs d'extraction corrigés :**
- `platsToXLS` tronquée → compléter à la main avec Python
- `\n` littéral dans `.join('\n')` → remplacer par séquence escape
- `platImportRef` déclarée deux fois → supprimer le doublon
- Bloc NAV de Settings collé à la fin de DataCleanupSection → couper au marqueur `/* ── Nav`
- Extra `);` en fin de CategoriesInvSection et SecteursInvSection → supprimer
- `saveCatInv` sans signature `async` → insérer l'entête manquant
**Why:** Settings.jsx à 3070 lignes causait des troncatures et des bugs à chaque modification. Découpage en composants autonomes pour éviter ces problèmes.
**How to apply:** Pour toute nouvelle section Settings ou Admin, créer un fichier autonome dans `pages/settings/` ou `pages/admin/`, avec ses propres imports et son propre `useEffect`/fetch. Ne jamais remonter l'état au parent.
@@ -0,0 +1,52 @@
---
name: project_settings_nav
description: "Structure NAV de Settings.jsx — groupes, labels et sections disponibles (état juin 2026)"
metadata:
node_type: memory
type: project
originSessionId: da8e6c45-04c2-4db9-8e7f-189d65e7cf99
---
La NAV de Settings est organisée en 4 groupes :
```
Interface
└ Apparence (id: apparence) — thème, police, langue, devise, couleurs graphiques
Mon paramétrage
├ Mes membres & entreprises (id: membres) — FamilleEntreprises.jsx (anciennement MonCompte?section=famille)
├ Mes plateformes (id: plateformes)
├ Mes comptes courants (id: comptes) — ComptesSection inline dans Settings.jsx
└ Ma fiscalité (id: ma-fiscalite) — toggle PFO/2778-SD (pfoAssujetti via UiContext)
Mes tags
├ Mes catégories d'investissement (id: categories-inv) — 2 accordéons : Global (replié) / Privé (déplié)
└ Mes secteurs d'investissement (id: secteurs-inv) — idem
Mes données
├ Nettoyage de données (id: nettoyage) — DataCleanupSection (anciennement MonCompte?section=nettoyage)
└ Importation de données (id: imports)
```
**Sections supprimées/déplacées :**
- `pfu``/admin/fiscalite?section=pfu`
- `garanties``/admin/plateformes?section=garanties`
- `notation``/admin/plateformes?section=notation`
- Groupe "Qualité" supprimé
- Groupe "Importation" renommé "Mes données"
- `famille` retiré de MonCompte → Settings `membres`
- `nettoyage` retiré de MonCompte → Settings `nettoyage`
**MonCompte** ne contient plus que 2 sections : `profil` et `securite`.
**Section `ma-fiscalite` :** toggle PFO/2778-SD (`pfoAssujetti` via UiContext), texte explicatif dépliable (`showPfoDetail` local).
**Sections `categories-inv` et `secteurs-inv` :** affichées en 2 accordéons —
- "Globalement définis" replié par défaut (`catGlobalOpen=false`, `sectGlobalOpen=false`)
- "Mes propres" déplié par défaut (`catPrivateOpen=true`, `sectPrivateOpen=true`)
États dans le composant Settings principal.
**Composants inline dans Settings.jsx** (définis en bas du fichier, après Settings) :
- `EMPTY_COMPTE`, `CompteFormFields`, `ComptesSection`
**How to apply:** Toute nouvelle section utilisateur va dans "Mon paramétrage". Toute section admin (référentiels, taux) va dans AdminPlateformes ou AdminFiscalite. DataCleanup et famille sont dans Settings, pas dans MonCompte.
@@ -0,0 +1,40 @@
---
name: Calcul solde porte-monnaie
description: Formule correcte du solde porte-monnaie plateforme — inclut la soustraction du capital investi non remboursé
type: project
originSessionId: 680bfd99-577f-454e-b5b8-ea58460277f9
---
## Formule solde porte-monnaie par plateforme
```
solde = dépôts retraits_manuels + net_recu_portefeuille + bonus_wallet capital_total_investi
```
- **retraits_manuels** : retraits hors `source='auto_remboursement'` (les auto-retraits sont générés par les remboursements compte_courant et ne représentent pas un mouvement du porte-monnaie)
- **remb_wallet** : montant crédité au porte-monnaie selon la fiscalité de la plateforme :
- `flat_tax``net_recu` (PFU 30% prélevé à la source par la plateforme française)
- `sans_fiscalite_locale` ou `avec_fiscalite_locale``capital + cashback + interets_bruts` (pas de PFU prélevé à la source ; l'investisseur déclare le PFU séparément ; `interets_bruts` est déjà net de la retenue locale si applicable)
- Uniquement pour `methode_remboursement = 'portefeuille'`
- **bonus_wallet** : cashback des remboursements `type IN ('bonus_parrainage', 'bonus_plateforme')`
- **capital_total_investi** : `montant_investi + SUM(reinvestissements)` par investissement souscrit, groupé par plateforme
Le capital revient progressivement via `net_recu` au fil des remboursements — il n'est pas nécessaire de le déduire partiellement.
## Implémentations
### Backend — `dashboard.js` (vue "Toutes les années")
Quatre requêtes SQL fusionnées dans `walletMap` :
1. `drManuelPerPlat` — dépôts retraits manuels
2. `rembWalletPerPlat` — net_recu portefeuille (fiscalité ajustée selon plateforme)
3. `bonusWalletPerPlat` — cashback bonus
4. `capitalInvestiPerPlat` — capital_total soustrait
### Frontend — `DepotsRetraits.jsx` (vue filtrée par année)
Calcul cumulatif jusqu'à `yearEnd = YYYY-12-31` dans `drPlatData` (useMemo) et `kpiSoldePortefeuille` :
1. Boucle sur `allRows` : exclure `auto_remboursement`, cumuler dépôts/retraits
2. Boucle sur `allRemb` : ajouter `net_recu` si `methode_remboursement === 'portefeuille'`
3. Boucle sur `allInv` (chargé via `/investissements`) : soustraire `capital_total ?? montant_investi`
**Why:** Sans la soustraction du capital investi, le porte-monnaie affichait les dépôts comme disponibles même quand le capital était engagé dans des prêts actifs.
**How to apply:** Toute nouvelle vue ou export affichant un "solde porte-monnaie" doit appliquer cette formule complète, y compris la soustraction du capital.
@@ -0,0 +1,14 @@
---
name: project_suppression_detenteur
description: "Suppression d'un détenteur — réassignation au principal avant delete, pas de cascade"
metadata:
node_type: memory
type: project
originSessionId: 94928e14-ffe7-4c4d-b139-748259d26134
---
La suppression d'un investisseur (détenteur) réassigne toutes les données liées au compte principal avant de supprimer l'enregistrement. Les tables `investissements`, `depots_retraits`, `plateformes` et `comptes` sont mises à jour via une transaction.
**Why:** Le schéma SQL a `ON DELETE CASCADE` sur `investisseur_id` dans `investissements` et `depots_retraits`, ce qui supprimait les données métier à tort. Seule la suppression d'une plateforme dans Paramètres doit supprimer les données associées.
**How to apply:** La logique est dans `DELETE /:id` de `backend/src/routes/investisseurs.js`. Toute nouvelle table avec `investisseur_id NOT NULL` et `ON DELETE CASCADE` devra être ajoutée à cette transaction de réassignation.
@@ -0,0 +1,43 @@
---
name: project_tableau_interets_plateforme
description: "Tableau mensuel intérêts par plateforme dans le Dashboard, synchronisé avec InteretsChartContext"
metadata:
node_type: memory
type: project
originSessionId: e5c19875-c3d7-42c1-ab67-bf3ba23dbda3
---
Composant `TableauInteretsPlateforme.jsx` ajouté dans le Dashboard, juste après les graphiques, à l'intérieur du `InteretsChartProvider`.
**Fichiers modifiés :**
- `frontend/src/components/TableauInteretsPlateforme.jsx` — composant créé
- `frontend/src/pages/Dashboard.jsx` — import + intégration
- `backend/src/routes/dashboard.js` — endpoint `GET /api/dashboard/interets-par-plateforme`
- `frontend/src/styles.css` — classes `tip-*`
**Endpoint backend :** `GET /api/dashboard/interets-par-plateforme?annee=&scope=all`
- Retourne : `{ plateformes: [{ id, nom, rembourses: { 'YYYY-MM': { interets_bruts, interets_nets, cashback, capital } }, projections: { 'YYYY-MM': { interets_prevus, capital_prevu } } }], capitalMensuel: [{ mois, capital }] }`
- Capital mensuel calculé via itération JS sur les investissements (date_souscription / date_cible / statut)
**Logique frontend :**
- Synchronisé avec `InteretsChartContext` : année, netMode, inclureInterets/Capital/Cashback, showActual/showProjected, modeGlobal
- `buildValue(plat, mIdx, { withCapital })` : fonction centrale — withCapital=true pour l'affichage, false pour la performance
- Mois passés = données réelles (si showActual) ; mois courant = réel + projeté ; mois futurs = projections (si showProjected)
- Valeurs projetées affichées en italique/muted
**Lignes du tableau :**
- Une ligne par plateforme (avec Total + Moy. mensuelle)
- "Toutes les plateformes" : somme mensuelle incluant capital si toggle actif
- "Capital investi" : snapshot mensuel, colonne Total = dernier mois non nul
- "Performance nette/brute mensuelle" : perfMonthTotals[i] / capitalValues[i] ; Total = perfAnnTotale/12
- "Performance nette/brute annualisée" : perf_mensuelle × 12 ; Total = perfGrandTotal / lastCapital
**Why:** Les 3 dernières lignes (capital investi + 2 perfs) utilisent `perfMonthTotals` (sans capital) même quand le toggle capital est actif — le capital gonfle les montants mais ne doit pas biaiser la performance.
**Visuel :**
- Couleur thème : violet/indigo (`#7c3aed → #4f46e5`) alignée sur le YearSelectorKpi
- Colonne mois courant : barre indigo en haut du header + fond légèrement teinté
- Header "PLATEFORME" et "TOTAL" : même gradient indigo
- Cellules vides fin de ligne (avg pour les 3 dernières lignes) : `tip-td-void` avec `border: 1px hidden transparent` pour effacer les lignes en border-collapse
- Sélecteur Reçu/Projeté en bas du tableau (même style pill que bar chart)
- En mode global ("Depuis le début") : tableau masqué (retourne null)
@@ -0,0 +1,49 @@
---
name: tableau-remboursements-detail
description: "Évolutions du tableau 'Remboursements enregistrés' dans InvestissementDetail — colonnes fusionnées, tooltip, CSS, clic ligne, retour dashboard"
metadata:
node_type: memory
type: project
originSessionId: cad3a9c0-cf8e-4204-ac6e-e22c03811abe
---
Tableau "Remboursements enregistrés" dans InvestissementDetail.jsx — évolutions UX.
**Colonnes actuelles (sans fiscalité locale) :**
Date | Capital | Intérêts (Brut/Net) | Cashback | Imposition | Montant versé | ⋮
**Colonne "Imposition" (fusion PS + IR) :**
- Remplace les deux colonnes "Prélèv. sociaux" et "Impôt revenu"
- Affiche la somme `prelev_sociaux + prelev_forfaitaire`
- Titre suffixé "— indicatif" si `inv.fiscalite_override === 'exonere' || tblPlat.fiscalite !== 'flat_tax'`
- Tooltip CSS instantané (classe `.cell-tooltip`) : "Prélèvements sociaux : x,xx €\nImpôt sur le revenu : x,xx €"
**Tooltip CSS (styles.css) :**
- Classe `.cell-tooltip` avec `::after { content: attr(data-tooltip); white-space: pre-line; opacity: 0 → 1 au hover }`
- Background : `var(--surface)` (pas de couleur hardcodée)
- Pas de transition = apparition instantanée
**CSS tableau — classe `remb-table` :**
- `table-layout: fixed; width: 100%` → largeurs égales entre colonnes numériques
- `text-align: center` sur toutes les cellules
- Première colonne (Date) : `text-align: left; width: 92px`
- Dernière colonne (⋮) : `width: 36px`
**Commentaire sous le titre de section :**
- Même formulation que dans la modale : isIndicatif ou flat tax
- Calculé via IIFE avec `_plat` et `_isIndicatif` locaux (pas de state dédié)
**Clic sur ligne → modale modification :**
- `onClick={() => openEditRemb(r)}` sur le `<tr>`
- `cursor: pointer` sur le `<tr>`
- Bouton ⋮ stoppe la propagation : `e.stopPropagation()` avant `openRowMenu`
**Ouverture depuis le tableau de bord (remb-date + from=dashboard dans l'URL) :**
- Le useEffect qui traite `remb-date` attend que `pfuRates.length > 0` (guard + dépendance) pour calculer les prélèvements correctement
- `fromRef.current` est capturé EN PREMIER (avant l'ouverture de la modale) et les params URL nettoyés immédiatement
- Après save (`submitRemb`) ou suppression (`deleteRemb`) : `closeRembModal` lit `fromRef.current` et navigue vers `/` si `'dashboard'`
- Bug corrigé : sans le guard pfuRates, les prélèvements étaient 0 car les taux n'étaient pas encore chargés
**Why:** Meilleure lisibilité du tableau, cohérence avec les modales, accès rapide à la modification, UX fluide depuis le dashboard.
**How to apply:** Si un autre tableau similaire est créé, reproduire `.remb-table`, `.cell-tooltip`, et le pattern clic ligne + stopPropagation sur le bouton ⋮. Pour tout flux "ouvrir modale depuis URL param", capturer `fromRef` avant d'ouvrir la modale et attendre que les données de référence (pfuRates) soient chargées.
@@ -0,0 +1,27 @@
---
name: project-tags-suggeres-admin
description: "Système de suggestions admin pour catégories/secteurs d'investissement créés par les utilisateurs"
metadata:
node_type: memory
type: project
originSessionId: 264d200a-fea4-4903-b7c5-20dcc9ebfbd2
---
Système de validation admin des tags (categories_inv / secteurs_inv) créés par les utilisateurs (user_id IS NOT NULL).
**Emplacement** : section `inv-suggestions` dans `/admin/plateformes` (AdminPlateformes.jsx), pas dans Admin.jsx.
**Backend (admin.js)** — 6 routes ajoutées :
- `GET /api/admin/inv-suggestions-count``{ cats, sects, total }`
- `GET /api/admin/inv-suggestions``{ categories: [...], secteurs: [...] }` avec user info + nb_plateformes + nb_investissements
- `POST /api/admin/inv-suggestions/categories/:id/promouvoir` → SET user_id = NULL (rend global)
- `DELETE /api/admin/inv-suggestions/categories/:id`
- `POST /api/admin/inv-suggestions/secteurs/:id/promouvoir`
- `DELETE /api/admin/inv-suggestions/secteurs/:id`
**Frontend (AdminPlateformes.jsx)** :
- `InvSuggestionsSection` : onglets Catégories/Secteurs, groupé par utilisateur, actions Promouvoir/Supprimer via ConfirmModal + ResultBanner
- Bouton compteur dans `RefListSection` (catégories ET secteurs) : gris si 0, rouge si > 0, navigue vers `?section=inv-suggestions`
- Bouton "← Retour aux catégories" dans la section
**How to apply:** Quand un utilisateur crée une catégorie/secteur privé, il apparaît ici. L'admin peut le promouvoir (global = visible par tous) ou le supprimer.
@@ -0,0 +1,26 @@
---
name: project_taux_credit_impot
description: "Référentiel taux crédit d'impôt 2047 — table DB, route backend, section Admin avec import IA"
metadata:
node_type: memory
type: project
originSessionId: 6ab3db95-bde5-486d-8d53-7e58134a13ca
---
Table `taux_credit_impot` seedée avec 124 pays (notice DGFiP 2047). Route `/api/taux-credit-impot` (GET auth, POST/PUT/DELETE admin). Section Admin sous groupe "Référentiels" → id `taux-ci`.
**Schéma :** `nom_pays, code_pays (ISO alpha-2), div_taux, div_taux_alt, div_taux_alt_label, div_exclusif_residence, int_taux, int_taux_alt, int_taux_alt_label, int_exclusif_residence, notice, statut_convention (active/suspendue/caduque), date_suspension, ref_boi`
**Frontend Admin.jsx :**
- `TciModal` — modale create/edit avec `CountrySelect showCode` pour le code pays (auto-remplit `nom_pays` si vide)
- `TauxCreditImpotSection` — tableau avec filtre tabs + recherche, menu ⋮ (Modifier/Supprimer), icône à 10px du badge convention
- `TciImportBlock` — import JSON IA : coller réponse → diff avant/après par champ → appliquer sélectivement
- `TciPromptBlock` — prompt IA éditable (localStorage `cl_tci_prompt`), bouton copier remplace `{{CURRENT_DATA}}` en live
- Légende "excl. résidence" dans le pied du tableau
**Piège fieldset CSS :** `fieldset` a `min-width: min-content` par défaut → ajouter `minWidth: 0` pour éviter le débordement de contenu.
**Piège checkbox CSS :** `input { width: 100% }` global force les checkboxes sur toute la largeur → toujours ajouter `width: auto` sur les `input[type="checkbox"]`.
**Why:** Notice DGFiP 2047 mise à jour annuellement ; le bloc IA permet de comparer et patcher les taux sans retouche manuelle du code.
**How to apply:** Futur champ `code_pays` sur la table `plateformes` permettra une jointure avec ce référentiel pour afficher le taux applicable par plateforme.
@@ -0,0 +1,32 @@
---
name: project-taxreport
description: "Refonte de la page Fiscalité — renommage, onglets, pagination, sélecteur d'année"
metadata:
node_type: memory
type: project
originSessionId: 0922bde7-84ed-45dc-b233-50bf0be5ad3e
---
Page Fiscalité entièrement refactorée en session 2026-05-31.
**Renommage complet :**
- Fichier : `Fiscal2778.jsx``TaxReport.jsx` ; `fiscal2778.js``taxreport.js`
- Route frontend : `/2778-sd``/taxreport` (redirect conservée pour l'ancienne URL)
- Route backend : `/api/fiscal-2778``/api/taxreport`
- Composant : `Fiscal2778()``TaxReport()`
- 5 fichiers mis à jour : App.jsx, Layout.jsx, server.js, TaxReport.jsx, taxreport.js
**Structure de la page :**
- Bloc "Cases fiscales calculées" toujours visible en haut
- Onglets `dr-tabs` (pattern Investissements) : **Récapitulatif** | **Détail par projet**
- Récapitulatif : tableau récap + corrections de solde + pertes en capital (conditionnels)
- Détail par projet : tableau avec pagination (`cl_pagesize_fiscal_detail`), bouton agrandir/réduire, ExportDropdown (CSV + JSON)
**Sélecteur d'année :**
- Composant `YearSelector` autonome, clone stylisé du `YearSelectorKpi` du Dashboard (card violet/indigo)
- Pas de mode "Depuis le début" (fiscal = toujours par année)
- Années disponibles chargées via `GET /api/taxreport/years` (années ayant des remboursements réels, desc)
- Endpoint backend ajouté dans `taxreport.js` avec même logique de scope investisseur
**Why:** Cohérence visuelle avec le Dashboard ; meilleure ergonomie que l'input number.
**How to apply:** Pour toute future modif de la page fiscalité, le fichier est `frontend/src/pages/TaxReport.jsx` et la route backend `backend/src/routes/taxreport.js`.
@@ -0,0 +1,27 @@
---
name: project_tip_css_drillcell_consolidation
description: Correctifs CSS en-têtes tip-table et bug DrillCellPanel en mode consolidé
metadata:
node_type: memory
type: project
originSessionId: 18505460-8577-4ae3-848c-a569e3dd2b75
---
## Correctifs CSS tip-table (2026-06-18)
`.tip-th-name` (colonne PLATEFORME) reçoit maintenant le même dégradé violet `linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%)` que `.tip-th-year`, `.tip-th-total` et `.tip-th-avg`. Texte blanc, border-right `rgba(255,255,255,.2)`.
`.tip-td-avg` reçoit le même fond subtil que `.tip-td-total` (`rgba(109,40,217,.04)` + `border-left rgba(109,40,217,.1)`), avec override dark mode amber cohérent.
## Bug DrillCellPanel en mode consolidé (2026-06-18)
**Cause** : en mode consolidé (`groupByNom`), `plat.id` était remplacé par `plat.nom` (chaîne). `Number("BienPrêter") = NaN` → filtre `plateforme_id` ignoré → toutes les plateformes retournées.
**Fix en 4 fichiers** :
- `TableauInteretsPlateforme.jsx` : lors de la fusion, stocker `_ids: [id1, id2, ...]` (vrais IDs numériques). `onCellClick` passe `platId: plat._ids[0]` (numérique) + `platIds: plat._ids` (tableau complet).
- `Dashboard.jsx` : destructure et propage `platIds` dans `drillCell`.
- `DrillCellPanel.jsx` : si `cell.platIds.length > 1` et filtre non modifié manuellement, envoie `plateforme_ids=1,2,3` au lieu de `plateforme_id`.
- `dashboard.js` (backend) : route `/detail-cellule` accepte `plateforme_ids` (CSV) → `IN (?, ?, ?)` dynamique ; rétrocompatible avec `plateforme_id` seul.
**Why:** `Remboursements.jsx` passe `cellInfo` entier dans `setDrillCell`, donc `platIds` est automatiquement propagé sans modification.
@@ -0,0 +1,24 @@
---
name: project-toggle-consolidation
description: Toggle Détaillé/Consolidé dans tous les tableaux mensuels — regroupe les plateformes par nom quelque soit le détenteur
metadata:
node_type: memory
type: project
originSessionId: 752b3578-1999-4431-9dd9-73297a9baaf4
---
Fonctionnalité ajoutée sur les 4 tableaux "Vision mensuelle" :
- `TableauInteretsPlateforme.jsx` (Dashboard + Remboursements)
- `CapitalMensuelTable.jsx` (Investissements)
- `DepotsMensuelTable.jsx` (Dépôts/Retraits)
**Ce qui a été implémenté :**
- Bouton `[Détaillé ▾]` / `[Consolidé ▲]` dans le `th` "Plateforme", visible uniquement si `multiDetenteur`
- En mode Consolidé : lignes fusionnées par `nom` de plateforme, données sommées, poids recalculé sur total consolidé
- Clé localStorage partagée : `cl_tip_group_by_nom` — changer le mode dans un tableau le change partout
- `useMemo` de consolidation placé AVANT le `return null` conditionnel (règle des hooks React)
**Helper fusion pour TableauInteretsPlateforme :** fonction `mergeMaps(mapA, mapB)` qui somme les champs `interets_bruts`, `interets_nets`, `cashback`, `capital`, `interets_prevus`, `capital_prevu` par clé de mois.
**Why:** Olivier voulait pouvoir voir les données consolidées par plateforme indépendamment du détenteur (ex. "Enky" au lieu de "Enky Olivier" + "Enky Capucine").
@@ -0,0 +1,31 @@
---
name: project-trendbadge-depots-remb
description: "TrendBadge déployé sur DepotsRetraits (4 KPIs) et Remboursements (3 KPIs) — même pattern qu'Investissements"
metadata:
node_type: memory
type: project
originSessionId: d05ae334-ab61-467e-9700-1d87d90f940a
---
TrendBadge ajouté aux KPIs de DepotsRetraits.jsx et Remboursements.jsx en session 2026-05-31.
**DepotsRetraits — 4 KPIs :**
- Dépôts, Retraits (invert=true), Diff. Dépôts vs Retraits, Porte-monnaie
- `drPlatYear` initialisé à `String(new Date().getFullYear())`
- `prevTotals` : filter allRows sur prevYear + même filtre plateforme
- `prevKpiSoldePortefeuille` : même calcul que kpiSoldePortefeuille avec cutoff 31/12 N-1
- `prevYear = String(Number(drPlatYear || new Date().getFullYear()) - 1)`
**Remboursements — 3 KPIs :**
- Capital remboursé, Cashback, Intérêts (Brut/Net)
- `rembPlatYear` initialisé à `String(new Date().getFullYear())`
- `prevTotals` : allRows filtrés sur rembPrevYear + filterPlatId + corrections N-1
- `rembPrevYear = String(Number(rembPlatYear || new Date().getFullYear()) - 1)`
**Pattern commun ([[project-trendbadge-investissements]]) :**
- En vue "Toutes les années" : effectiveYear = année courante → badges vs N-1 toujours visibles
- Valeur de référence N-1 en sous-titre de chaque KPI
**Why:** Cohérence avec le Dashboard et la page Investissements.
**How to apply:** Pour toute nouvelle page avec filtre année + KPIs, initialiser le state année à `String(new Date().getFullYear())` et calculer prevTotals avec `effectiveYear = year || currentYear`.
@@ -0,0 +1,25 @@
---
name: project-trendbadge-investissements
description: TrendBadge ajouté aux 5 KPIs de la page Investissements — comparaison N vs N-1
metadata:
node_type: memory
type: project
originSessionId: d05ae334-ab61-467e-9700-1d87d90f940a
---
TrendBadge (copié depuis Dashboard) ajouté aux 5 KPIs de Investissements.jsx : Capital investi, À risque, Depuis le début, Remboursés, Intérêts perçus.
**Implémentation :**
- `TrendBadge` défini en haut du fichier (avant le composant principal)
- `isActiveAtEndOfYear(r, yr = platYear)` : paramètre `yr` optionnel pour réutilisation avec N-1
- `prevTotals` memo : recalcule les 5 agrégats pour l'année N-1 (rembs + reinvests filtrés au 31/12 N-1)
- `platYear` initialisé à `String(new Date().getFullYear())` pour afficher les badges dès le chargement
**Comportement "Toutes les années" (`platYear = ''`) :**
- `effectiveYear = platYear || String(new Date().getFullYear())`
- Compare le total cumulé actuel vs le total cumulé au 31/12 de l'année précédente
- Badges toujours visibles, même sans filtre année
**Why:** Sans année sélectionnée, `prevTotals` retournait null → badges absents au chargement et en vue globale.
**How to apply:** Si d'autres pages ont un filtre année + KPIs, appliquer le même pattern `effectiveYear` pour que les badges ne disparaissent pas en vue globale.
@@ -0,0 +1,41 @@
---
name: project-user-preferences
description: "Table user_preferences — stockage DB des préférences UI par utilisateur, route /api/preferences, sync UiContext"
metadata:
node_type: memory
type: project
originSessionId: d7532bd8-8eb4-4b40-8c39-8f569dcf432f
---
Système de persistance des préférences UI en base de données, par utilisateur.
## Table (backend/src/db/index.js)
```sql
CREATE TABLE IF NOT EXISTS user_preferences (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
key TEXT NOT NULL,
value TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (user_id, key)
)
```
Table générique clé/valeur — extensible à toutes les futures prefs sans migration.
## Route (backend/src/routes/preferences.js)
- `GET /api/preferences` — retourne `{ key: value, ... }` pour l'utilisateur connecté
- `PATCH /api/preferences` — upsert d'un ou plusieurs couples ; liste blanche des clés autorisées
- Clés actuellement autorisées : `chart_interets`, `chart_capital`, `chart_cashback`
- Pour ajouter une nouvelle pref : l'ajouter dans le Set `ALLOWED_KEYS` de la route
## Stratégie de sync dans UiContext
- **Au montage** : `api.get('/preferences')` → valeurs DB écrasent localStorage (DB fait foi). Silencieux si token absent.
- **À chaque changement** : localStorage mis à jour immédiatement (feedback instantané) + `api.patch('/preferences', { key: value })` asynchrone silencieux en cas d'erreur réseau.
## Extension future
Pour persister d'autres prefs (thème, font_scale, langue, devise, display_mode) :
1. Ajouter la clé dans `ALLOWED_KEYS` (preferences.js)
2. Lire la valeur dans le `useEffect` de montage (UiContext)
3. Appeler `persistPref(key, value)` dans le setter correspondant
**Why:** L'utilisateur veut que ses préférences de couleurs soient liées à son compte et non au navigateur.
**How to apply:** Toute nouvelle préférence UI candidate à la persistance multi-device doit passer par ce système.
@@ -0,0 +1,18 @@
---
name: project_xlsx_export
description: Export Excel migré vers SheetJS (xlsx) — vrais fichiers .xlsx sans avertissement
metadata:
node_type: memory
type: project
originSessionId: f2be746c-47f1-426a-ae80-3c71059fa38c
---
Les exports Excel des 3 pages utilisent SheetJS (`import * as XLSX from 'xlsx'`).
- `xlsx` installé dans `frontend/` via npm
- Pattern : `XLSX.utils.json_to_sheet(data)``XLSX.utils.book_append_sheet``XLSX.write(wb, { type: 'array', bookType: 'xlsx' })`
- `dlBlob` reçoit un `Uint8Array`, MIME type `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
- Extension `.xlsx` (remplace l'ancien `.xml` / `.xls` SpreadsheetML qui déclenchait l'avertissement Excel)
**Why:** L'ancien format SpreadsheetML XML avec extension `.xls` déclenchait un avertissement de sécurité dans Excel.
**How to apply:** Toute nouvelle fonction d'export Excel doit suivre ce pattern SheetJS, pas le XML SpreadsheetML manuel.
@@ -0,0 +1,69 @@
---
name: project-year-selector-kpi
description: "YearSelectorKpi — card sélecteur d'année violet/indigo sur la ligne KPI du Dashboard, synchronisé avec InteretsChartContext"
metadata:
node_type: memory
type: project
originSessionId: 9d8a4dc8-0caa-4a53-bf00-040dfe2afe4a
---
## Emplacement
Composant `YearSelectorKpi` défini dans `frontend/src/pages/Dashboard.jsx`, avant la fonction `Dashboard`. Doit être enfant de `InteretsChartProvider` pour accéder au contexte.
## Layout Dashboard modifié
Le `InteretsChartProvider` remonte pour englober à la fois la grille KPI et les graphiques :
```jsx
<InteretsChartProvider ...>
{/* KPI + sélecteur */}
<div style={{ display:'flex', gap:12, alignItems:'stretch', marginBottom:16 }}>
<div className="kpi-grid" style={{ flex:1, marginBottom:0 }}>
{/* 6 KPIs existants */}
</div>
<YearSelectorKpi />
</div>
{/* Graphiques */}
<div style={{ display:'flex', gap:16, alignItems:'stretch', ... }}>
<InteretsMensuelsChart /> + <InteretsDonutChart />
</div>
</InteretsChartProvider>
```
`alignItems:'stretch'` + `marginBottom:0` sur le kpi-grid (override CSS) → la card année a la même hauteur que la grille.
## YearSelectorKpi
- **Couleur** : `linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%)` — violet/indigo, ombre `rgba(109,40,217,0.30)` qui s'intensifie à l'ouverture
- **Texte blanc**, label "PÉRIODE" en xs uppercase, chevron SVG animé (rotate 180° à l'ouverture)
- **Année en 2rem / bold** ; si modeGlobal actif → affiche "Depuis le début" en 1.1rem
- `width: 200px`, `flexShrink: 0`
## Dropdown custom
- Options : `{ value:'all', label:'Depuis le début' }` en tête, puis `availableYears` en ordre **croissant** (pas de `.reverse()`)
- Item actif : fond `rgba(109,40,217,0.08)`, couleur `#7c3aed`, bold, coche SVG ✓ à droite
- Hover : `var(--surface-2)`
- Séparateurs `borderBottom` entre items sauf le dernier
- `position: absolute; top: calc(100% + 6px); right: 0; z-index: 200`
- Fermeture sur clic extérieur via `useEffect` sur `mousedown`
## Synchronisation bidirectionnelle
```js
const handleSelect = (value) => {
if (value === 'all') {
if (!modeGlobal) toggleModeGlobal(); // active TOUT
} else {
if (modeGlobal) toggleModeGlobal(); // désactive TOUT
setAnnee(value);
}
};
```
- Changer l'année dans le bar chart → `annee` dans contexte → YearSelectorKpi se met à jour
- Activer TOUT dans le bar chart → `modeGlobal` dans contexte → affiche "Depuis le début"
**Why:** L'utilisateur doit voir l'année active de façon très visible et pouvoir la changer directement depuis le dashboard.
**How to apply:** Toujours garder YearSelectorKpi à l'intérieur de InteretsChartProvider. Si on déplace les graphiques, déplacer le Provider en même temps.
@@ -0,0 +1,41 @@
---
name: project-zip-backup-referentiel
description: Système de sauvegarde ZIP pour le référentiel plateformes — export/import avec manifest + data + logos
metadata:
node_type: memory
type: project
originSessionId: 264d200a-fea4-4903-b7c5-20dcc9ebfbd2
---
Système complet d'export/import ZIP pour `plateformes_referentiel`.
**Fichiers créés/modifiés :**
- `backend/src/utils/zip.js` — helper ZIP pur Node.js (pas de dépendance externe). `createZip(entries)` + `readZip(buffer)`. Utilise `zlib.deflateRawSync` / `inflateRawSync` + CRC-32 maison.
- `backend/src/routes/referentiel.js` — 3 nouvelles routes (positionnées AVANT `/:id`) :
- `GET /api/referentiel/export` → télécharge tout le référentiel en ZIP
- `GET /api/referentiel/:id/export` → télécharge une seule entrée en ZIP
- `POST /api/referentiel/import-zip` → importe un ZIP (multipart `file`), écrase en cas de conflit de `nom`
- `frontend/src/api.js` — méthode `api.blob(path)` ajoutée pour les téléchargements binaires authentifiés
- `frontend/src/pages/AdminPlateformes.jsx``ReferentielSection` :
- Boutons "Exporter tout" + "Importer" dans la barre d'outils
- Entrée "Exporter" dans le menu ⋮ par ligne
- `importResult` banner via `ResultBanner`
**Structure ZIP (version 1.1) :**
```
manifest.json { version: '1.1', app, exported_at, count, type }
data.json [ { nom, url, domiciliation, ..., categories_inv: [noms], secteurs_inv: [noms], logo_filename, icone_filename, notation: [...] } ]
garanties.json [ { libelle, description, ordre } ] — types de garanties du user exporteur
logos/ fichiers image logo/icône (si présents sur le serveur)
```
**Import — comportement :**
- Conflit `nom` → UPDATE (écrase tout)
- `categories_inv` / `secteurs_inv` par noms → crée les tags globaux manquants (`user_id = NULL`)
- Images dans le ZIP → écrites dans `logosDir` (= `DATA_DIR/logos`)
- `notation` par plateforme → DELETE + re-INSERT dans `referentiel_notation`
- `garanties.json` → upsert dans `garantie_types` pour le user qui importe
**Why:** Demande explicite — sauvegarde et migration du référentiel entre instances.
**How to apply:** Toute évolution du schéma `plateformes_referentiel` doit répercuter les nouveaux champs dans les routes export (liste `fields`) et dans la logique d'UPDATE/INSERT de l'import.

Some files were not shown because too many files have changed in this diff Show More