import { useEffect, useState, useRef, useMemo } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
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 InteretsMensuelsChart from '../components/InteretsMensuelsChart.jsx';
import InteretsDonutChart from '../components/InteretsDonutChart.jsx';
import { InteretsChartProvider, useInteretsChart } from '../context/InteretsChartContext.jsx';
import TableauInteretsPlateforme from '../components/TableauInteretsPlateforme.jsx';
import DrillCellPanel from '../components/DrillCellPanel.jsx';
import EmptyState from '../components/EmptyState.jsx';
/* ── Sélecteur d'année — doit être enfant de InteretsChartProvider ── */
function YearSelectorKpi() {
const { annee, setAnnee, availableYears, modeGlobal, toggleModeGlobal } = useInteretsChart();
const [open, setOpen] = useState(false);
const ref = useRef(null);
useEffect(() => {
if (!open) return;
const handler = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [open]);
const handleSelect = (value) => {
if (value === 'all') {
if (!modeGlobal) toggleModeGlobal();
} else {
if (modeGlobal) toggleModeGlobal();
setAnnee(value);
}
setOpen(false);
};
// Années récentes en premier (desc), puis "Depuis le début" en tête
const options = [
{ value: 'all', label: 'Depuis le début' },
...availableYears.map(y => ({ value: y, label: String(y) })),
];
return (
setOpen(v => !v)}
style={{
height: '100%', boxSizing: 'border-box',
background: 'linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%)',
borderRadius: 10,
padding: '16px 20px',
boxShadow: open
? '0 6px 28px rgba(109,40,217,0.45)'
: '0 4px 20px rgba(109,40,217,0.30)',
cursor: 'pointer',
display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
userSelect: 'none',
transition: 'box-shadow .15s',
}}
>
{modeGlobal ? 'Depuis le début' : String(annee)}
{open && (
{options.map((opt, i) => {
const isActive = opt.value === 'all' ? modeGlobal : (!modeGlobal && annee === opt.value);
return (
handleSelect(opt.value)}
style={{
padding: '10px 16px',
cursor: 'pointer',
background: isActive ? 'rgba(109,40,217,0.08)' : 'transparent',
color: isActive ? '#7c3aed' : 'var(--text)',
fontWeight: isActive ? 700 : 400,
fontSize: 'var(--fs-sm)',
borderBottom: i < options.length - 1 ? '1px solid var(--border)' : 'none',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
transition: 'background .1s',
}}
onMouseEnter={e => { if (!isActive) e.currentTarget.style.background = 'var(--surface-2)'; }}
onMouseLeave={e => { if (!isActive) e.currentTarget.style.background = 'transparent'; }}
>
{opt.label}
{isActive && (
)}
);
})}
)}
);
}
/* ── Badge de tendance ── */
function TrendBadge({ current, prev, invert = false }) {
if (prev == null || prev === 0) return null;
const diff = current - prev;
const pct = (diff / prev) * 100;
const up = diff > 0;
const neutral = diff === 0;
// invert=true : une hausse est mauvaise (ex. capital en risque)
const good = neutral ? null : (invert ? !up : up);
const color = neutral ? 'var(--text-muted)' : good ? '#16a34a' : '#dc2626';
const bg = neutral ? 'var(--surface-2)' : good ? 'rgba(34,197,94,0.12)' : 'rgba(239,68,68,0.12)';
const arrow = neutral ? '→' : up ? '↗' : '↘';
const label = `${up ? '+' : ''}${Math.abs(pct) < 10 ? pct.toFixed(1) : Math.round(pct)}%`;
return (
{arrow} {label}
);
}
/* ── Carte KPI individuelle ── */
function KpiCard({ title, value, badge, refValue, onClick }) {
return (
{ e.currentTarget.style.boxShadow = '0 0 0 2px var(--primary)'; e.currentTarget.style.opacity = '0.88'; } : undefined}
onMouseLeave={onClick ? (e) => { e.currentTarget.style.boxShadow = ''; e.currentTarget.style.opacity = ''; } : undefined}
>
{title}
{value}
{badge}
{refValue && (
{refValue}
)}
);
}
/* ── KPI filtrés par année ── */
function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData }) {
const {
annee, modeGlobal, rawDataGlobal, rawData, currentYear, currentMonth,
setInclureInterets, setInclureCapital, setInclureCashback,
} = useInteretsChart();
const isCurrentYear = !modeGlobal && Number(annee) === currentYear;
const isFutureYear = !modeGlobal && Number(annee) > currentYear;
// Capital de référence pour l'année sélectionnée
// Pour les années passées : dernier mois de capitalMensuel (= capital déployé fin décembre)
// Pour l'année courante / mode global : capital actuellement déployé (portfolio)
const capitalAnnee = useMemo(() => {
if (modeGlobal || isCurrentYear) return portfolio.encours + portfolio.en_defaut;
// Année passée : prendre la valeur de fin décembre depuis capitalMensuel
const anneeStr = String(annee);
const moisAnnee = (capitalMensuelData ?? []).filter(c => c.mois?.startsWith(anneeStr));
if (moisAnnee.length > 0) {
// Prendre le dernier mois disponible (normalement décembre)
const last = moisAnnee[moisAnnee.length - 1];
return last.capital ?? 0;
}
return portfolio.encours + portfolio.en_defaut;
}, [modeGlobal, isCurrentYear, annee, capitalMensuelData, portfolio]);
// Capital souscrit par année (pour référence performance N-1, données manquantes, etc.)
const capitalParAnneeMap = useMemo(() => {
const list = rawDataGlobal.capitalParAnnee ?? [];
return Object.fromEntries(list.map(r => [r.annee, r.capital_souscrit]));
}, [rawDataGlobal]);
// ── Estimation réduction PFU pour une année donnée ──
const getPfuReduction = (yr) => {
if (!pfuRates.length) return 0;
const rate = pfuRates.find(r => r.annee === yr)
?? pfuRates.reduce((best, r) => r.annee > best.annee ? r : best, pfuRates[0]);
return (rate.prelev_sociaux + rate.impot_revenu) / 100;
};
// ── Données consolidées pour une année (actuel + projeté selon le cas) ──
const getYearData = (yr) => {
const remRow = (rawDataGlobal.rembourses ?? []).find(r => Number(r.annee) === yr);
const projRow = (rawDataGlobal.projections ?? []).find(r => Number(r.annee) === yr);
if (yr > currentYear) {
// Année future : projections uniquement
const bruts = projRow?.interets_prevus || 0;
const red = getPfuReduction(yr);
return { interets_bruts: bruts, interets_nets: bruts * (1 - red), capital: projRow?.capital_prevu || 0, cashback: 0 };
}
if (yr === currentYear) {
// Année en cours : actuel reçu + reste projeté
const bruts_act = remRow?.interets_bruts || 0;
const bruts_proj = projRow?.interets_prevus || 0;
const red = getPfuReduction(yr);
return {
interets_bruts: bruts_act + bruts_proj,
interets_nets: (remRow?.interets_nets || 0) + bruts_proj * (1 - red),
capital: (remRow?.capital || 0) + (projRow?.capital_prevu || 0),
cashback: remRow?.cashback || 0,
};
}
// Année passée : actuel uniquement
return {
interets_bruts: remRow?.interets_bruts || 0,
interets_nets: remRow?.interets_nets || 0,
capital: remRow?.capital || 0,
cashback: remRow?.cashback || 0,
};
};
// ── Données mensuelles (mois en cours + M-1) — uniquement si année courante ──
const { thisMonthRow, prevMonthRow, prevMonthLabel, capitalCurrent, capitalPrev, enDefautCurrent, enDefautPrev } = useMemo(() => {
const MOIS_FR = ['jan.','fév.','mar.','avr.','mai','juin','juil.','août','sep.','oct.','nov.','déc.'];
const rows = rawData.rembourses ?? [];
const thisStr = String(currentYear) + '-' + String(currentMonth).padStart(2, '0');
const curr = rows.find(r => r.mois === thisStr) ?? null;
const prevDate = new Date(currentYear, currentMonth - 2, 1);
const prevStr = String(prevDate.getFullYear()) + '-' + String(prevDate.getMonth() + 1).padStart(2, '0');
const prev = rows.find(r => r.mois === prevStr) ?? null;
const label = MOIS_FR[prevDate.getMonth()] + ' ' + prevDate.getFullYear();
// Capital investi et en risque M vs M-1 depuis capitalMensuelData
const capRows = capitalMensuelData ?? [];
const capCurr = capRows.find(c => c.mois === thisStr);
const capPrev = capRows.find(c => c.mois === prevStr);
return {
thisMonthRow: curr, prevMonthRow: prev, prevMonthLabel: label,
capitalCurrent: capCurr?.capital ?? null,
capitalPrev: capPrev?.capital ?? null,
enDefautCurrent: capCurr?.en_defaut ?? null,
enDefautPrev: capPrev?.en_defaut ?? null,
};
}, [rawData, currentYear, currentMonth, capitalMensuelData]);
// ── Données annuelles (année sélectionnée + N-1) ──
const { annualData, prevAnnualData } = useMemo(() => {
if (modeGlobal) {
const total = (rawDataGlobal.rembourses ?? []).reduce((acc, r) => ({
interets_bruts: acc.interets_bruts + (r.interets_bruts || 0),
interets_nets: acc.interets_nets + (r.interets_nets || 0),
capital: acc.capital + (r.capital || 0),
cashback: acc.cashback + (r.cashback || 0),
}), { interets_bruts: 0, interets_nets: 0, capital: 0, cashback: 0 });
return { annualData: total, prevAnnualData: null };
}
return {
annualData: getYearData(Number(annee)),
prevAnnualData: getYearData(Number(annee) - 1),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rawDataGlobal, annee, modeGlobal, pfuRates]);
// ── Valeur affichée ──
const getValue = (field) => {
if (isCurrentYear) return thisMonthRow?.[field] || 0;
return annualData[field] || 0;
};
// ── Badge et référence ──
const mkBadge = (field) => {
if (modeGlobal) return null;
if (isCurrentYear) {
if (!thisMonthRow || !prevMonthRow) return null;
return ;
}
if (!prevAnnualData) return null;
return ;
};
const mkRef = (field) => {
if (modeGlobal) return null;
if (isCurrentYear) {
if (!prevMonthRow) return null;
return fmtEUR(prevMonthRow[field] || 0) + ' en ' + prevMonthLabel;
}
if (!prevAnnualData) return null;
const prevYr = Number(annee) - 1;
const suffix = prevYr >= currentYear ? ' (proj.)' : '';
return fmtEUR(prevAnnualData[field] || 0) + ' en ' + prevYr + suffix;
};
const interetsField = netMode ? 'interets_nets' : 'interets_bruts';
// ── Performance annualisée ──────────────────────────────────────
const calcPerfRatio = (interetsVal, capital) =>
capital > 0 && interetsVal != null ? interetsVal / capital : null;
// Mensuelle annualisée (mode année courante)
const capitalDeploye = portfolio.encours + portfolio.en_defaut;
const perfCurrent = isCurrentYear && thisMonthRow && capitalDeploye > 0
? calcPerfRatio(thisMonthRow[interetsField] || 0, capitalDeploye) * 12
: null;
const perfPrev = isCurrentYear && prevMonthRow && capitalDeploye > 0
? calcPerfRatio(prevMonthRow[interetsField] || 0, capitalDeploye) * 12
: null;
// Annuelle (mode année passée/future) — même formule que le tableau :
// (interets + cashback) / capital souscrit cette année-là
const perfInteretsAnnee = (annualData[interetsField] || 0) + (annualData.cashback || 0);
const perfAnnual = !modeGlobal && !isCurrentYear && capitalAnnee > 0
? calcPerfRatio(perfInteretsAnnee, capitalAnnee)
: null;
const prevCapitalAnnee = modeGlobal || isCurrentYear
? capitalDeploye
: (capitalParAnneeMap[Number(annee) - 1] ?? capitalDeploye);
const perfInteretsPrevAnnee = ((prevAnnualData?.[interetsField] || 0) + (prevAnnualData?.cashback || 0));
const perfAnnualPrev = !modeGlobal && !isCurrentYear && prevAnnualData && prevCapitalAnnee > 0
? calcPerfRatio(perfInteretsPrevAnnee, prevCapitalAnnee)
: null;
const perfValue = modeGlobal ? null : (isCurrentYear ? perfCurrent : perfAnnual);
const perfPrevVal = modeGlobal ? null : (isCurrentYear ? perfPrev : perfAnnualPrev);
const perfLabel = netMode ? 'Performance nette annualisée' : 'Performance brute annualisée';
const perfRefLabel = isCurrentYear
? (perfPrevVal != null ? fmtPct(perfPrevVal * 100) + ' en ' + prevMonthLabel : null)
: (perfAnnualPrev != null ? fmtPct(perfAnnualPrev * 100) + ' en ' + (Number(annee) - 1) : null);
return (
0
?
: null}
refValue={isCurrentYear && capitalPrev != null
? fmtEUR(capitalPrev) + ' en ' + prevMonthLabel
: null}
/>
0
?
: null}
refValue={isCurrentYear && enDefautPrev != null && enDefautPrev > 0
? fmtEUR(enDefautPrev) + ' en ' + prevMonthLabel
: null}
/>
: null}
refValue={perfRefLabel}
/>
{ setInclureInterets(true); setInclureCapital(false); setInclureCashback(false); }}
/>
{ setInclureInterets(false); setInclureCapital(true); setInclureCashback(false); }}
/>
{ setInclureInterets(false); setInclureCapital(false); setInclureCashback(true); }}
/>
);
}
export default function Dashboard() {
const { activeId, activeView, activeViewMember, investisseurs } = useInvestisseur();
const { displayMode } = useUi();
const netMode = displayMode === 'net';
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [data, setData] = useState(null);
const [pfuRates, setPfuRates] = useState([]);
const [loading, setLoading] = useState(false);
const [capitalMensuelData, setCapitalMensuelData] = useState([]);
const [plateformes, setPlateformes] = useState([]);
const [platLoaded, setPlatLoaded] = useState(false);
/* ── drillCell : cellule sélectionnée dans le TIP — par défaut mois courant toutes plateformes ── */
const _now = new Date();
const _moisLabels = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'];
/* Restaure le drillCell depuis les params URL (retour depuis Remboursements) */
const _initDrillCell = () => {
const ba = searchParams.get('drill-annee');
const bm = searchParams.get('drill-mois');
const bp = searchParams.get('drill-plat');
if (ba && bm) {
const annee = Number(ba), mois = Number(bm);
return {
platId: bp ? Number(bp) : null,
platNom: null,
annee,
mois,
moisLabel: _moisLabels[mois - 1],
};
}
return {
platId: null,
platNom: null,
annee: _now.getFullYear(),
mois: _now.getMonth() + 1,
moisLabel: _moisLabels[_now.getMonth()],
};
};
const [drillCell, setDrillCell] = useState(_initDrillCell);
/* Nettoie les params URL de retour dès le premier rendu */
useEffect(() => {
if (searchParams.get('drill-annee')) {
setSearchParams({}, { replace: true });
}
}, []); /* eslint-disable-next-line */
useEffect(() => {
api.get('/pfu').then(setPfuRates).catch(() => {});
api.get('/plateformes').then(d => { setPlateformes(d); setPlatLoaded(true); }).catch(() => { setPlatLoaded(true); });
}, []);
useEffect(() => {
if (!activeId && activeView !== 'all') return;
setData(null);
setLoading(true);
const params = activeView === 'all' ? { scope: 'all' } : undefined;
api.get('/dashboard', params).then(setData).finally(() => setLoading(false));
}, [activeView, activeId]);
const ready = activeView === 'all' ? investisseurs.length > 0 : !!activeId;
if (!ready) return Sélectionnez un compte investisseur.
;
if (loading || !data) return Chargement…
;
if (platLoaded && plateformes.length === 0) return (
<>
Tableau de bord
>
);
const { cash, cashByPlatform, portfolio, interets, interetsParAnnee } = data;
const viewTitle = activeView === 'all'
? 'Famille et entreprises'
: memberLabel(activeViewMember);
return (
<>
Tableau de bord — {viewTitle}
{/* ── KPI + sélecteur année ── */}
{/* ── Graphiques ── */}
{/* ── Tableau intérêts par plateforme ── */}
setDrillCell({ platId, platIds, platNom, annee, mois, moisLabel })
}
activeCell={drillCell}
/>
{/* ── Panneau détail mois / plateforme (remplace les Échéances prévues) ── */}
{}}
onEditRecu={(r) => {
const q = new URLSearchParams({
'edit-remb': r.id,
from: 'dashboard',
'drill-annee': drillCell.annee,
'drill-mois': drillCell.mois,
...(drillCell.platId ? { 'drill-plat': drillCell.platId } : {}),
});
navigate(`/remboursements?${q}`);
}}
onEditProjet={(p) => {
const q = new URLSearchParams({
'open-simul': p.investissement_id,
'simul-date': p.date_prevue,
'simul-capital': p.capital_prevu ?? 0,
'simul-interets': p.interets_prevus ?? 0,
from: 'dashboard',
'drill-annee': drillCell.annee,
'drill-mois': drillCell.mois,
...(drillCell.platId ? { 'drill-plat': drillCell.platId } : {}),
});
navigate(`/remboursements?${q}`);
}}
/>
>
);
}