diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx
index 4e5082b..2f8a9ab 100644
--- a/frontend/src/pages/Dashboard.jsx
+++ b/frontend/src/pages/Dashboard.jsx
@@ -4,7 +4,8 @@ import PageIcon from '../components/PageIcon.jsx';
import { api } from '../api.js';
import { useInvestisseur } from '../context/InvestisseurContext.jsx';
import { useUi } from '../context/UiContext.jsx';
-import { fmtEUR, fmtPct, fmtDate, fmtStatut, memberLabel } from '../utils/format.js';
+import { fmtEUR, fmtPct, fmtDate, fmtStatut, memberLabel, today } from '../utils/format.js';
+import { xirr } from '../utils/xirr.js';
import InteretsMensuelsChart from '../components/InteretsMensuelsChart.jsx';
import InteretsDonutChart from '../components/InteretsDonutChart.jsx';
import { InteretsChartProvider, useInteretsChart } from '../context/InteretsChartContext.jsx';
@@ -182,7 +183,7 @@ function KpiCard({ title, value, badge, refValue, note, onClick, hint }) {
}
/* ── KPI filtrés par année ── */
-function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activeId, activeView, soldePortefeuilleLive, correctionsTotal, investissements }) {
+function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activeId, activeView, soldePortefeuilleLive, correctionsTotal, investissements, allRembs, allReinvests, allSimulRembs }) {
// Montant total déjà déclaré en perte définitive (tous statuts confondus,
// seuls les dossiers passés en perte_definitive ont un capital_perdu > 0) —
// affiché en complément du KPI "Capital en risque", qui les exclut déjà
@@ -372,6 +373,139 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ
return gainNetTotal - deltaCeMois;
}, [gainNetTotal, thisMonthRow]);
+ // ── XIRR Global (réel + attendu) — depuis le début, indépendant du sélecteur
+ // d'année (même logique que "Gain net depuis le début" ci-dessus). Reprend
+ // exactement la méthode plateformeXirr / plateformeXirrAttendu de
+ // Plateformes.jsx, appliquée ici à la totalité du portefeuille dans le
+ // scope courant (activeId / activeView, déjà appliqué côté API sur
+ // investissements / allRembs / allSimulRembs ; allReinvests est chargé en
+ // scope 'all' comme dans Plateformes.jsx, filtré ci-dessous via `ids`).
+ // Voir mémoire project_xirr_attendu_theorique.
+ const getRatesForYearKpi = (dateStr) => {
+ if (!dateStr || !pfuRates.length) return null;
+ const year = parseInt(dateStr.slice(0, 4), 10);
+ return pfuRates.find(r => r.annee === year)
+ ?? pfuRates.reduce((best, r) => r.annee > best.annee ? r : best, pfuRates[0]);
+ };
+
+ const capRembParInvGlobal = useMemo(() => {
+ const map = {};
+ for (const rb of (allRembs || [])) {
+ if (!rb.investissement_id) continue;
+ map[rb.investissement_id] = (map[rb.investissement_id] || 0) + (rb.capital || 0);
+ }
+ return map;
+ }, [allRembs]);
+
+ const reinvestCumulParInvGlobal = useMemo(() => {
+ const map = {};
+ for (const rv of (allReinvests || [])) {
+ map[rv.investissement_id] = (map[rv.investissement_id] || 0) + (rv.montant || 0);
+ }
+ return map;
+ }, [allReinvests]);
+
+ const xirrGlobal = useMemo(() => {
+ const cutoff = today();
+ const invs = investissements || [];
+ const ids = new Set(invs.map(r => r.id));
+ const flowsBrut = [];
+ const flowsNet = [];
+ for (const r of invs) {
+ if (!r.date_souscription) continue;
+ flowsBrut.push({ amount: -r.montant_investi, date: r.date_souscription });
+ flowsNet.push({ amount: -r.montant_investi, date: r.date_souscription });
+ }
+ for (const rv of (allReinvests || [])) {
+ if (!ids.has(rv.investissement_id)) continue;
+ const d = rv.date_reinvestissement?.slice(0, 10);
+ if (!d || d > cutoff) continue;
+ flowsBrut.push({ amount: -(rv.montant || 0), date: d });
+ flowsNet.push({ amount: -(rv.montant || 0), date: d });
+ }
+ for (const rb of (allRembs || [])) {
+ if (!ids.has(rb.investissement_id)) continue;
+ const d = rb.date_remb?.slice(0, 10);
+ if (!d || d > cutoff) continue;
+ flowsBrut.push({ amount: (rb.capital || 0) + (rb.cashback || 0) + (rb.interets_bruts || 0), date: d });
+ flowsNet.push({ amount: rb.net_recu || 0, date: d });
+ }
+ let estime = false;
+ for (const r of invs) {
+ const capInv = r.montant_investi + (reinvestCumulParInvGlobal[r.id] || 0);
+ const capRemb = capRembParInvGlobal[r.id] || 0;
+ const capRestant = Math.max(0, capInv - capRemb - (r.capital_perdu || 0));
+ if (capRestant > 0.01) {
+ estime = true;
+ flowsBrut.push({ amount: capRestant, date: cutoff });
+ flowsNet.push({ amount: capRestant, date: cutoff });
+ }
+ }
+ const byDate = (a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0);
+ flowsBrut.sort(byDate);
+ flowsNet.sort(byDate);
+ return { brut: xirr(flowsBrut), net: xirr(flowsNet), estime, cutoff };
+ }, [investissements, allRembs, allReinvests, reinvestCumulParInvGlobal, capRembParInvGlobal]);
+
+ const xirrGlobalAttendu = useMemo(() => {
+ const cutoff = today();
+ const invs = investissements || [];
+ const ids = new Set(invs.map(r => r.id));
+ const flowsBrut = [];
+ const flowsNet = [];
+ for (const r of invs) {
+ if (!r.date_souscription) continue;
+ flowsBrut.push({ amount: -r.montant_investi, date: r.date_souscription });
+ flowsNet.push({ amount: -r.montant_investi, date: r.date_souscription });
+ }
+ for (const rv of (allReinvests || [])) {
+ if (!ids.has(rv.investissement_id)) continue;
+ const d = rv.date_reinvestissement?.slice(0, 10);
+ if (!d || d > cutoff) continue;
+ flowsBrut.push({ amount: -(rv.montant || 0), date: d });
+ flowsNet.push({ amount: -(rv.montant || 0), date: d });
+ }
+ for (const rb of (allRembs || [])) {
+ if (!ids.has(rb.investissement_id)) continue;
+ const d = rb.date_remb?.slice(0, 10);
+ if (!d || d > cutoff) continue;
+ flowsBrut.push({ amount: (rb.capital || 0) + (rb.cashback || 0) + (rb.interets_bruts || 0), date: d });
+ flowsNet.push({ amount: rb.net_recu || 0, date: d });
+ }
+ const futureSimulParInv = {};
+ for (const sr of (allSimulRembs || [])) {
+ if (!ids.has(sr.investissement_id)) continue;
+ const d = sr.date_prevue?.slice(0, 10);
+ if (!d || d <= cutoff) continue;
+ if (!futureSimulParInv[sr.investissement_id]) futureSimulParInv[sr.investissement_id] = [];
+ futureSimulParInv[sr.investissement_id].push({ date: d, capital: sr.capital_prevu || 0, interets: sr.interets_prevus || 0 });
+ }
+ for (const r of invs) {
+ const capInv = r.montant_investi + (reinvestCumulParInvGlobal[r.id] || 0);
+ const capRemb = capRembParInvGlobal[r.id] || 0;
+ const capRestant = Math.max(0, capInv - capRemb - (r.capital_perdu || 0));
+ if (capRestant <= 0.01) continue;
+ const echeances = futureSimulParInv[r.id];
+ if (echeances && echeances.length) {
+ for (const e of echeances) {
+ if (e.capital <= 0 && e.interets <= 0) continue;
+ const rate = getRatesForYearKpi(e.date);
+ const pfu = rate ? (rate.prelev_sociaux + rate.impot_revenu) / 100 : 0.3;
+ flowsBrut.push({ amount: e.capital + e.interets, date: e.date });
+ flowsNet.push({ amount: e.capital + e.interets * (1 - pfu), date: e.date });
+ }
+ } else {
+ flowsBrut.push({ amount: capRestant, date: cutoff });
+ flowsNet.push({ amount: capRestant, date: cutoff });
+ }
+ }
+ const byDate = (a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0);
+ flowsBrut.sort(byDate);
+ flowsNet.sort(byDate);
+ return { brut: xirr(flowsBrut), net: xirr(flowsNet) };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [investissements, allRembs, allReinvests, allSimulRembs, reinvestCumulParInvGlobal, capRembParInvGlobal, pfuRates]);
+
// ── Valeur affichée ──
const getValue = (field) => {
if (isCurrentYear) return thisMonthRow?.[field] || 0;
@@ -511,6 +645,76 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ
badge={