Compare commits

...

55 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
204 changed files with 17556 additions and 695 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