diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/MEMORY.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/MEMORY.md new file mode 100644 index 0000000..b54138b --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/MEMORY.md @@ -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 `
` 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) diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_adjustsimul.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_adjustsimul.md new file mode 100644 index 0000000..f41e47c --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_adjustsimul.md @@ -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()()`. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_adjustsimul_capital_solde.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_adjustsimul_capital_solde.md new file mode 100644 index 0000000..5562007 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_adjustsimul_capital_solde.md @@ -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`. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_dbindex_export_duplicate.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_dbindex_export_duplicate.md new file mode 100644 index 0000000..db61c17 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_dbindex_export_duplicate.md @@ -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]] diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_differe_date_calcul.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_differe_date_calcul.md new file mode 100644 index 0000000..7e79346 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_differe_date_calcul.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_express_route_order.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_express_route_order.md new file mode 100644 index 0000000..46abc87 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_express_route_order.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_fieldset_checkbox_css.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_fieldset_checkbox_css.md new file mode 100644 index 0000000..564c588 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_fieldset_checkbox_css.md @@ -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 `
`. + +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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_file_truncation.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_file_truncation.md new file mode 100644 index 0000000..ac59457 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_file_truncation.md @@ -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 "^ "` 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 :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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_investisseur_for_plat.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_investisseur_for_plat.md new file mode 100644 index 0000000..9a03a0f --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_investisseur_for_plat.md @@ -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`. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_menu_actions_format.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_menu_actions_format.md new file mode 100644 index 0000000..7248da0 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_menu_actions_format.md @@ -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: , label: '…', onClick, disabled?, color? }` +- Rendu : `display: flex, gap: 8` avec `{icon}` +- Hover : `onMouseEnter/Leave` pour background `var(--surface-2)` +- Fermeture au scroll via `useEffect` diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_montant_verse_formula.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_montant_verse_formula.md new file mode 100644 index 0000000..40cf2ec --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_montant_verse_formula.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_multi_detenteur.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_multi_detenteur.md new file mode 100644 index 0000000..e100bc0 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_multi_detenteur.md @@ -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 => ( + +))} +``` + +**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`). diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_notifications_audience.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_notifications_audience.md new file mode 100644 index 0000000..2601f09 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_notifications_audience.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_onclick_default_param.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_onclick_default_param.md new file mode 100644 index 0000000..a373ede --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_onclick_default_param.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_otplib_esm.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_otplib_esm.md new file mode 100644 index 0000000..14bc57e --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_otplib_esm.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_pageicon_topbar.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_pageicon_topbar.md new file mode 100644 index 0000000..4b94a06 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_pageicon_topbar.md @@ -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 `` 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 +

Tableau de bord

+``` + +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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_projection_no_double_count.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_projection_no_double_count.md new file mode 100644 index 0000000..b6080c7 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_projection_no_double_count.md @@ -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`. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_put_payload_minimal.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_put_payload_minimal.md new file mode 100644 index 0000000..45d0d52 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_put_payload_minimal.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_reinvest_adjustsimul.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_reinvest_adjustsimul.md new file mode 100644 index 0000000..7030bee --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_reinvest_adjustsimul.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_remb_methode_fallback.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_remb_methode_fallback.md new file mode 100644 index 0000000..1c08e4f --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_remb_methode_fallback.md @@ -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]]. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_reprocess_adjustsimul.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_reprocess_adjustsimul.md new file mode 100644 index 0000000..981983c --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_reprocess_adjustsimul.md @@ -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()()`. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_result_banner.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_result_banner.md new file mode 100644 index 0000000..cfaaa6e --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_result_banner.md @@ -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 ` 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`. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_safari_mobile_viewport.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_safari_mobile_viewport.md new file mode 100644 index 0000000..ff92cac --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_safari_mobile_viewport.md @@ -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`. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_sed_truncation.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_sed_truncation.md new file mode 100644 index 0000000..dda3320 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_sed_truncation.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_sidebar_expand_overlay.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_sidebar_expand_overlay.md new file mode 100644 index 0000000..f1a81e2 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_sidebar_expand_overlay.md @@ -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 ` + ))} +
+ +)} +``` + +**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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_methode_remboursement_investissement.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_methode_remboursement_investissement.md new file mode 100644 index 0000000..99cba35 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_methode_remboursement_investissement.md @@ -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 `` avec `` 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]]. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_notifications.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_notifications.md new file mode 100644 index 0000000..d77b8da --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_notifications.md @@ -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`. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_pagination.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_pagination.md new file mode 100644 index 0000000..1481999 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_pagination.md @@ -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 "X–Y 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_` 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 `
` pour éviter un problème d'alignement centré diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_plateforme_detenteur_logo.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_plateforme_detenteur_logo.md new file mode 100644 index 0000000..ecb450a --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_plateforme_detenteur_logo.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_plateformes_page.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_plateformes_page.md new file mode 100644 index 0000000..81f53e3 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_plateformes_page.md @@ -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` : `` (plus d'inline styles) ; `` STATUT utilise `tip-th-name` +- `STATUT_BG` / `STATUT_FG` supprimés de `InvMensuelTable.jsx` +- Titres "Mouvements de trésorerie" et "Capital investi par investissement" → `

` 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 `` +- "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). diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_profil_import_ia_tags.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_profil_import_ia_tags.md new file mode 100644 index 0000000..d0ae775 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_profil_import_ia_tags.md @@ -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). diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_projections_display.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_projections_display.md new file mode 100644 index 0000000..1152d59 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_projections_display.md @@ -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) 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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_referentiel_plateformes.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_referentiel_plateformes.md new file mode 100644 index 0000000..229e0a2 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_referentiel_plateformes.md @@ -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))` diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_referentiel_search_filter.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_referentiel_search_filter.md new file mode 100644 index 0000000..6b47492 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_referentiel_search_filter.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_reinvestissements.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_reinvestissements.md new file mode 100644 index 0000000..15239b9 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_reinvestissements.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_settings_admin_refacto.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_settings_admin_refacto.md new file mode 100644 index 0000000..1643f16 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_settings_admin_refacto.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_settings_nav.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_settings_nav.md new file mode 100644 index 0000000..f89ff6a --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_settings_nav.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_solde_portefeuille.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_solde_portefeuille.md new file mode 100644 index 0000000..45013ce --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_solde_portefeuille.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_suppression_detenteur.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_suppression_detenteur.md new file mode 100644 index 0000000..e62f725 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_suppression_detenteur.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tableau_interets_plateforme.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tableau_interets_plateforme.md new file mode 100644 index 0000000..9bd9cbf --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tableau_interets_plateforme.md @@ -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) diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tableau_remboursements_detail.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tableau_remboursements_detail.md new file mode 100644 index 0000000..7efeec7 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tableau_remboursements_detail.md @@ -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 `` +- `cursor: pointer` sur le `` +- 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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tags_suggeres_admin.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tags_suggeres_admin.md new file mode 100644 index 0000000..0dc5d17 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tags_suggeres_admin.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_taux_credit_impot.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_taux_credit_impot.md new file mode 100644 index 0000000..e0133b2 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_taux_credit_impot.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_taxreport.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_taxreport.md new file mode 100644 index 0000000..81e6ca7 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_taxreport.md @@ -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`. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tip_css_drillcell_consolidation.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tip_css_drillcell_consolidation.md new file mode 100644 index 0000000..b557454 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tip_css_drillcell_consolidation.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_toggle_consolidation.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_toggle_consolidation.md new file mode 100644 index 0000000..8b0234f --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_toggle_consolidation.md @@ -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"). diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_trendbadge_depots_remb.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_trendbadge_depots_remb.md new file mode 100644 index 0000000..b14bb9c --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_trendbadge_depots_remb.md @@ -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`. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_trendbadge_investissements.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_trendbadge_investissements.md new file mode 100644 index 0000000..4dfca70 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_trendbadge_investissements.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_user_preferences.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_user_preferences.md new file mode 100644 index 0000000..7f5961e --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_user_preferences.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_xlsx_export.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_xlsx_export.md new file mode 100644 index 0000000..bf04013 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_xlsx_export.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_year_selector_kpi.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_year_selector_kpi.md new file mode 100644 index 0000000..6016fcc --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_year_selector_kpi.md @@ -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 + + {/* KPI + sélecteur */} +
+
+ {/* 6 KPIs existants */} +
+ +
+ {/* Graphiques */} +
+ + +
+
+``` + +`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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_zip_backup_referentiel.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_zip_backup_referentiel.md new file mode 100644 index 0000000..e442e10 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_zip_backup_referentiel.md @@ -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. diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/user_profile.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/user_profile.md new file mode 100644 index 0000000..c01ed35 --- /dev/null +++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/user_profile.md @@ -0,0 +1,11 @@ +--- +name: Profil utilisateur +description: Olivier, propriétaire et développeur de l'app crowdlending, pilote les évolutions fonctionnelles +type: user +originSessionId: b87fd892-4ff2-4b8b-b3e6-f0f92e303c8e +--- +Olivier (olivier@croguennec.net) est le propriétaire de l'application crowdlending-app. Il pilote directement les évolutions fonctionnelles et techniques, formule des demandes précises, et valide les résultats visuellement dans l'interface. + +Il est à l'aise avec le code mais délègue l'implémentation. Il préfère des réponses concises et des recommandations directes (pas d'options multiples sauf demande explicite). + +L'application est un tracker de portefeuille de prêts participatifs, multi-investisseurs (famille + entreprises), usage personnel/familial. diff --git a/.cowork/registertask.ps1 b/.cowork/registertask.ps1 new file mode 100644 index 0000000..6c7d549 --- /dev/null +++ b/.cowork/registertask.ps1 @@ -0,0 +1,6 @@ +# Crée la tâche planifiée (toutes les heures) +$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NonInteractive -File C:\dev\crowdlending-app\.cowork\sync-cowork-memory.ps1" +$trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Hours 1) -Once -At (Get-Date) +$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 5) + +Register-ScheduledTask -TaskName "CoworkMemorySync-crowdlending" -Action $action -Trigger $trigger -Settings $settings -RunLevel Highest -Force \ No newline at end of file diff --git a/.cowork/sync-cowork-memory.ps1 b/.cowork/sync-cowork-memory.ps1 new file mode 100644 index 0000000..7a1867b --- /dev/null +++ b/.cowork/sync-cowork-memory.ps1 @@ -0,0 +1,18 @@ +# sync-cowork-memory.ps1 +$memorySource = "C:\Users\olivi\AppData\Roaming\Claude\local-agent-mode-sessions\cad3a9c0-cf8e-4204-ac6e-e22c03811abe\7707aa40-4613-41ca-918f-a32d6df19bce\spaces" +$memoryDest = "C:\dev\crowdlending-app\.cowork\memory" +$repoPath = "C:\dev\crowdlending-app" + +# Sync des fichiers mémoire +robocopy $memorySource $memoryDest *.md /E /XO /NFL /NDL + +# Commit Git si des changements existent +Set-Location $repoPath +$status = git status --porcelain .cowork +if ($status) { + git add .cowork + git commit -m "chore: sync cowork memory $(Get-Date -Format 'yyyy-MM-dd HH:mm')" + Write-Host "Mémoire Cowork synchronisée et commitée." -ForegroundColor Green +} else { + Write-Host "Aucun changement détecté." -ForegroundColor Gray +} \ No newline at end of file