import { useMemo, useState } from 'react'; import { fmtEUR } from '../utils/format.js'; const MOIS_LONG = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre']; /* ── Helpers dates ───────────────────────────────────────────────── */ function endOfMonth(Y, M) { const d = new Date(Y, M, 0); // day 0 of month M+1 = last day of month M return `${Y}-${String(M).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; } function startOfMonth(Y, M) { return `${Y}-${String(M).padStart(2,'0')}-01`; } function ChevronDown({ size = 10 }) { return ( ); } /* ── Composant ───────────────────────────────────────────────────── */ export default function CapitalMensuelTable({ allRows, allRembs, allReinvests, plats, expandButton }) { const currentYear = new Date().getFullYear(); const currentMonth = new Date().getMonth() + 1; const [annee, setAnnee] = useState(currentYear); /* ── Toggle consolidation détenteurs ── */ const [groupByNom, setGroupByNom] = useState(() => { try { return localStorage.getItem('cl_tip_group_by_nom') === 'true'; } catch { return false; } }); const toggleGroupByNom = () => { setGroupByNom(v => { const next = !v; try { localStorage.setItem('cl_tip_group_by_nom', String(next)); } catch {} return next; }); }; /* ── Années disponibles ── */ const availableYears = useMemo(() => { const set = new Set(allRows.map(r => r.date_souscription?.slice(0,4)).filter(Boolean)); return [...set].map(Number).sort((a,b) => a - b); }, [allRows]); /* ── Precompute : reinvests et capital_remb par investissement ── */ const reinvestByInv = useMemo(() => { const map = {}; for (const rv of allReinvests) { const id = rv.investissement_id; if (!id) continue; if (!map[id]) map[id] = []; map[id].push({ date: rv.date_reinvestissement?.slice(0,10), montant: rv.montant || 0 }); } return map; }, [allReinvests]); const capRembByInv = useMemo(() => { const map = {}; for (const rb of allRembs) { const id = rb.investissement_id; if (!id || rb.type !== 'normal') continue; if (!map[id]) map[id] = []; map[id].push({ date: rb.date_remb?.slice(0,10), capital: rb.capital || 0 }); } return map; }, [allRembs]); const lastRembDateMap = useMemo(() => { const map = {}; for (const rb of allRembs) { const id = rb.investissement_id; const d = rb.date_remb?.slice(0,10); if (!id || !d) continue; if (!map[id] || d > map[id]) map[id] = d; } return map; }, [allRembs]); /* ── Calcul capital encours par plateforme par mois ───────────── * Pour chaque mois M : * - L'investissement est actif si souscrit avant fin M * ET (statut actif aujourd'hui OU date_fin >= début M) * - capital = montant_investi + reinvests_≤_finM − capital_remboursé_≤_finM * ─────────────────────────────────────────────────────────────── */ const { grid, multiDetenteur } = useMemo(() => { if (!allRows.length) return { grid: null, multiDetenteur: false }; const ACTIVE = ['en_cours', 'en_retard', 'procedure']; // Index plateformes par id (pour nom + detenteur) const platMap = {}; for (const p of plats) platMap[p.id] = p; // Pour chaque investissement, capital encours au end of month M const getCapitalAtEndOfMonth = (inv, Y, M) => { const endM = endOfMonth(Y, M); if (inv.date_souscription > endM) return 0; const startM = startOfMonth(Y, M); const isActive = ACTIVE.includes(inv.statut) || ((inv.date_cible || lastRembDateMap[inv.id] || null) >= startM); if (!isActive) return 0; const reinvM = (reinvestByInv[inv.id] || []) .filter(rv => rv.date && rv.date <= endM) .reduce((s, rv) => s + rv.montant, 0); const capRembM = (capRembByInv[inv.id] || []) .filter(rb => rb.date && rb.date <= endM) .reduce((s, rb) => s + rb.capital, 0); return Math.max(0, inv.montant_investi + reinvM - capRembM); }; // Agréger par plateforme (id) const byPlat = {}; for (const inv of allRows) { const pid = inv.plateforme_id; if (!byPlat[pid]) { const p = platMap[pid] || {}; byPlat[pid] = { id: pid, nom: inv.plateforme_nom || p.nom || '—', investisseur_id: p.investisseur_id ?? inv.investisseur_id ?? null, detenteur_nom: inv.plateforme_detenteur_nom || null, months: Array(12).fill(0), }; } const row = byPlat[pid]; for (let m = 1; m <= 12; m++) { row.months[m-1] += getCapitalAtEndOfMonth(inv, annee, m); } } const allPlats = Object.values(byPlat).filter(p => p.months.some(v => v > 0)); // Détection multi-détenteur sur données brutes const multi = new Set(allPlats.map(p => p.investisseur_id).filter(v => v != null)).size > 1; // Consolidation par nom si demandée let rows; if (groupByNom && multi) { const byNom = {}; for (const row of allPlats) { if (!byNom[row.nom]) { byNom[row.nom] = { id: row.nom, nom: row.nom, investisseur_id: null, detenteur_nom: null, months: [...row.months] }; } else { for (let i = 0; i < 12; i++) byNom[row.nom].months[i] += row.months[i]; } } rows = Object.values(byNom); } else { rows = allPlats; } rows = rows .filter(p => p.months.some(v => v > 0)) .sort((a,b) => b.months.reduce((s,v) => s+v, 0) - a.months.reduce((s,v) => s+v, 0)); return { grid: rows, multiDetenteur: multi }; }, [allRows, annee, reinvestByInv, capRembByInv, lastRembDateMap, plats, groupByNom]); /* ── Totaux et moyennes ── */ const stats = useMemo(() => { if (!grid) return null; const monthTotals = Array.from({ length: 12 }, (_, i) => grid.reduce((s, row) => s + row.months[i], 0)); const grandTotal = monthTotals.reduce((s, v) => s + v, 0); // Moyenne : average of non-zero months per platform const platMoyennes = grid.map(row => { const nonZero = row.months.filter(v => v > 0); return nonZero.length ? nonZero.reduce((s,v) => s+v, 0) / nonZero.length : 0; }); const totalMoyenne = platMoyennes.reduce((s,v) => s+v, 0); const platPoids = platMoyennes.map(m => totalMoyenne > 0 ? (m / totalMoyenne) * 100 : 0); const monthMoyennes = Array.from({ length: 12 }, (_, i) => grid.reduce((s, row) => s + row.months[i], 0)); const nonZeroMonthTotals = monthTotals.filter(v => v > 0); const globalMoyenne = nonZeroMonthTotals.length ? nonZeroMonthTotals.reduce((s,v) => s+v, 0) / nonZeroMonthTotals.length : 0; return { monthTotals, grandTotal, platMoyennes, platPoids, totalMoyenne, monthMoyennes, globalMoyenne }; }, [grid]); /* ── Sélecteur d'années ── */ const [windowStart, setWindowStart] = useState(() => { const idx = availableYears.indexOf(currentYear); return Math.max(0, Math.min(Math.max(0, availableYears.length - 3), (idx >= 0 ? idx : availableYears.length - 1) - 1)); }); const visibleYears = availableYears.length ? availableYears.slice(windowStart, windowStart + 3) : [annee]; const canPrev = windowStart > 0; const canNext = windowStart + 3 < availableYears.length; /* ── Rendu ─────────────────────────────────────────────────────── */ return (
{/* ── Header ── */}
{`Capital investi · ${annee}`}
{stats ? fmtEUR(stats.globalMoyenne) : '—'} moy. mensuelle
{/* Sélecteur d'années */}
{visibleYears.map(y => ( ))} {expandButton}
{/* ── Table ── */} {!grid || grid.length === 0 ? (
Aucun capital investi pour {annee}.
) : (
{MOIS_LONG.map((m, i) => ( ))} {grid.map((plat, pi) => ( {plat.months.map((v, mi) => ( ))} ))} {stats.monthTotals.map((v, i) => ( ))}
{annee}
Plateforme {multiDetenteur && ( )} {m} Moyenne Poids
{plat.nom} {!groupByNom && multiDetenteur && plat.detenteur_nom && ( {plat.detenteur_nom} )} {v > 0 ? fmtEUR(v) : } {stats.platMoyennes[pi] > 0 ? fmtEUR(stats.platMoyennes[pi]) : } {stats.platPoids[pi] > 0 ? (
{stats.platPoids[pi].toFixed(1)} %
) : }
Toutes les plateformes {v > 0 ? fmtEUR(v) : } {fmtEUR(stats.globalMoyenne)}
)}
); }