Initial commit
This commit is contained in:
@@ -0,0 +1,499 @@
|
||||
import { useEffect, useState, useRef, useMemo } from 'react';
|
||||
import { api } from '../api.js';
|
||||
import { useInteretsChart } from '../context/InteretsChartContext.jsx';
|
||||
import { fmtEUR, fmtPct } from '../utils/format.js';
|
||||
|
||||
const ICONS_BASE = '/api/icons-files/';
|
||||
const MOIS_LONG = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'];
|
||||
|
||||
function hexToRgba(hex, a) {
|
||||
if (!hex || hex.length < 7) return `rgba(79,168,232,${a})`;
|
||||
const r = parseInt(hex.slice(1,3),16), g = parseInt(hex.slice(3,5),16), b = parseInt(hex.slice(5,7),16);
|
||||
return `rgba(${r},${g},${b},${a})`;
|
||||
}
|
||||
|
||||
function ChevronDown({ size = 10 }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Fusionne deux maps de remboursements ou projections ── */
|
||||
function mergeMaps(mapA, mapB) {
|
||||
const result = {};
|
||||
const keys = new Set([...Object.keys(mapA || {}), ...Object.keys(mapB || {})]);
|
||||
for (const k of keys) {
|
||||
const a = mapA?.[k] || {};
|
||||
const b = mapB?.[k] || {};
|
||||
result[k] = {
|
||||
interets_bruts: (a.interets_bruts || 0) + (b.interets_bruts || 0),
|
||||
interets_nets: (a.interets_nets || 0) + (b.interets_nets || 0),
|
||||
cashback: (a.cashback || 0) + (b.cashback || 0),
|
||||
capital: (a.capital || 0) + (b.capital || 0),
|
||||
interets_prevus: (a.interets_prevus || 0) + (b.interets_prevus || 0),
|
||||
capital_prevu: (a.capital_prevu || 0) + (b.capital_prevu || 0),
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export default function TableauInteretsPlateforme({ activeView, activeId, pfuRates, onCapitalMensuel, expandButton, onCellClick, activeCell }) {
|
||||
const {
|
||||
annee, setAnnee, availableYears,
|
||||
inclureInterets, setInclureInterets,
|
||||
inclureCapital, setInclureCapital,
|
||||
inclureCashback, setInclureCashback,
|
||||
netMode,
|
||||
showActual, toggleActual,
|
||||
showProjected, toggleProjected,
|
||||
modeGlobal, toggleModeGlobal,
|
||||
currentYear, currentMonth,
|
||||
chartInterets, chartCapital, chartCashback,
|
||||
} = useInteretsChart();
|
||||
|
||||
const [data, setData] = useState(null);
|
||||
const [libIcons, setLibIcons] = useState({});
|
||||
const [windowStart, setWindowStart] = useState(0);
|
||||
const initializedRef = useRef(false);
|
||||
|
||||
/* ── Toggle consolidation détenteurs (clé partagée avec CapitalMensuelTable) ── */
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
/* ── Icônes bibliothèque ─────────────────────────────────────── */
|
||||
useEffect(() => {
|
||||
api.get('/icons').then(rows => {
|
||||
const m = {};
|
||||
rows.forEach(r => { m[r.name] = r.filename; });
|
||||
setLibIcons(m);
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
/* ── Fenêtre années ──────────────────────────────────────────── */
|
||||
const canPrev = windowStart > 0;
|
||||
const canNext = windowStart + 3 < availableYears.length;
|
||||
const visibleYears = availableYears.length ? availableYears.slice(windowStart, windowStart + 3) : [annee];
|
||||
|
||||
useEffect(() => {
|
||||
if (!availableYears.length || initializedRef.current) return;
|
||||
initializedRef.current = true;
|
||||
const idx = availableYears.indexOf(annee);
|
||||
const safe = idx >= 0 ? idx : availableYears.length - 1;
|
||||
setWindowStart(Math.max(0, Math.min(availableYears.length - 3, safe - 1)));
|
||||
}, [availableYears]);
|
||||
|
||||
/* ── Réduction PFU ───────────────────────────────────────────── */
|
||||
const pfuReduction = useMemo(() => {
|
||||
if (!pfuRates?.length) return 0;
|
||||
const r = pfuRates.find(r => r.annee === annee)
|
||||
?? pfuRates.reduce((best, r) => r.annee > best.annee ? r : best, pfuRates[0]);
|
||||
return (r.prelev_sociaux + r.impot_revenu) / 100;
|
||||
}, [pfuRates, annee]);
|
||||
|
||||
/* ── Fetch données ───────────────────────────────────────────── */
|
||||
useEffect(() => {
|
||||
if (modeGlobal) { setData(null); onCapitalMensuel?.([]); return; }
|
||||
const params = { annee, ...(activeView === 'all' ? { scope: 'all' } : {}) };
|
||||
api.get('/dashboard/interets-par-plateforme', params)
|
||||
.then(d => { setData(d); onCapitalMensuel?.(d.capitalMensuel ?? []); })
|
||||
.catch(() => {});
|
||||
}, [annee, activeView, activeId, modeGlobal]);
|
||||
|
||||
/* ── Helpers affichage ───────────────────────────────────────── */
|
||||
const AppIcon = ({ name, size = 28, active = false }) => {
|
||||
const filename = libIcons[name];
|
||||
if (filename) return (
|
||||
<img src={ICONS_BASE + filename} className="app-lib-icon" width={size} height={size}
|
||||
aria-hidden="true"
|
||||
style={{ opacity: active ? 1 : 0.35, display: 'block', transition: 'opacity .15s' }} />
|
||||
);
|
||||
return <span style={{ width: size, height: size, display: 'block', borderRadius: 4,
|
||||
background: 'var(--text-muted)', opacity: active ? 0.55 : 0.2, transition: 'opacity .15s' }} />;
|
||||
};
|
||||
|
||||
const plateformes = data?.plateformes ?? [];
|
||||
const capitalMensuel = data?.capitalMensuel ?? [];
|
||||
|
||||
// N'afficher le détenteur que s'il y en a plusieurs distincts (pattern multiDetenteur)
|
||||
const multiDetenteur = new Set(plateformes.map(p => p.detenteur_nom).filter(Boolean)).size > 1;
|
||||
|
||||
/* ── Consolidation par nom si demandée ──────────────────────── */
|
||||
const displayPlateformes = useMemo(() => {
|
||||
if (!groupByNom || !multiDetenteur) return plateformes;
|
||||
const byNom = {};
|
||||
for (const plat of plateformes) {
|
||||
if (!byNom[plat.nom]) {
|
||||
byNom[plat.nom] = {
|
||||
...plat,
|
||||
id: plat.nom,
|
||||
detenteur_nom: null,
|
||||
rembourses: { ...plat.rembourses },
|
||||
projections: { ...plat.projections },
|
||||
};
|
||||
} else {
|
||||
byNom[plat.nom].rembourses = mergeMaps(byNom[plat.nom].rembourses, plat.rembourses);
|
||||
byNom[plat.nom].projections = mergeMaps(byNom[plat.nom].projections, plat.projections);
|
||||
}
|
||||
}
|
||||
return Object.values(byNom);
|
||||
}, [plateformes, groupByNom, multiDetenteur]);
|
||||
|
||||
if (modeGlobal || !data || plateformes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ── Valeurs par plateforme/mois ────────────────────────────────
|
||||
* getCellValue : pour l'affichage (interets + cashback + capital selon toggles)
|
||||
* getPerfValue : pour la performance (interets + cashback uniquement, jamais capital)
|
||||
* ─────────────────────────────────────────────────────────────── */
|
||||
const buildValue = (plat, mIdx, { withCapital }) => {
|
||||
const m = mIdx + 1;
|
||||
const moisStr = `${annee}-${String(m).padStart(2, '0')}`;
|
||||
const isFuture = annee > currentYear || (annee === currentYear && m > currentMonth);
|
||||
const isCurrent = annee === currentYear && m === currentMonth;
|
||||
|
||||
if (isFuture) {
|
||||
if (!showProjected) return null;
|
||||
const proj = plat.projections[moisStr];
|
||||
if (!proj) return null;
|
||||
let v = 0;
|
||||
if (inclureInterets) v += netMode ? proj.interets_prevus * (1 - pfuReduction) : proj.interets_prevus;
|
||||
if (withCapital && inclureCapital) v += proj.capital_prevu ?? 0;
|
||||
return v > 0 ? { value: v, projected: true } : null;
|
||||
}
|
||||
|
||||
if (isCurrent) {
|
||||
const remb = plat.rembourses[moisStr];
|
||||
const proj = plat.projections[moisStr];
|
||||
let real = 0;
|
||||
if (showActual && remb) {
|
||||
if (inclureInterets) real += netMode ? remb.interets_nets : remb.interets_bruts;
|
||||
if (inclureCashback) real += remb.cashback ?? 0;
|
||||
if (withCapital && inclureCapital) real += remb.capital ?? 0;
|
||||
}
|
||||
let projAmt = 0;
|
||||
// Les projections backend sont déjà filtrées NOT EXISTS par investissement → pas de double-comptage
|
||||
if (showProjected && proj) {
|
||||
if (inclureInterets) projAmt += netMode ? proj.interets_prevus * (1 - pfuReduction) : proj.interets_prevus;
|
||||
if (withCapital && inclureCapital) projAmt += proj.capital_prevu ?? 0;
|
||||
}
|
||||
const val = real + projAmt;
|
||||
return val > 0 ? { value: val, projected: projAmt > 0 } : null;
|
||||
}
|
||||
|
||||
// Mois passé
|
||||
if (!showActual) return null;
|
||||
const remb = plat.rembourses[moisStr];
|
||||
if (!remb) return null;
|
||||
let v = 0;
|
||||
if (inclureInterets) v += netMode ? remb.interets_nets : remb.interets_bruts;
|
||||
if (inclureCashback) v += remb.cashback ?? 0;
|
||||
if (withCapital && inclureCapital) v += remb.capital ?? 0;
|
||||
return v > 0 ? { value: v, projected: false } : null;
|
||||
};
|
||||
|
||||
const getCellValue = (plat, mIdx) => buildValue(plat, mIdx, { withCapital: true });
|
||||
const getPerfValue = (plat, mIdx) => buildValue(plat, mIdx, { withCapital: false });
|
||||
|
||||
/* ── Grille ──────────────────────────────────────────────────── */
|
||||
const grid = displayPlateformes.map(plat => ({
|
||||
...plat,
|
||||
months: Array.from({ length: 12 }, (_, i) => getCellValue(plat, i)),
|
||||
}));
|
||||
|
||||
const monthTotals = Array.from({ length: 12 }, (_, i) =>
|
||||
grid.reduce((s, row) => s + (row.months[i]?.value ?? 0), 0));
|
||||
const platTotals = grid.map(row =>
|
||||
row.months.reduce((s, v) => s + (v?.value ?? 0), 0));
|
||||
const grandTotal = monthTotals.reduce((s, v) => s + v, 0);
|
||||
|
||||
/* Totaux pour la performance : intérêts + cashback uniquement (sans capital) */
|
||||
const perfMonthTotals = Array.from({ length: 12 }, (_, i) =>
|
||||
displayPlateformes.reduce((s, plat) => s + (getPerfValue(plat, i)?.value ?? 0), 0));
|
||||
const perfGrandTotal = perfMonthTotals.reduce((s, v) => s + v, 0);
|
||||
|
||||
/* ── Capital et performances ─────────────────────────────────── */
|
||||
const capitalValues = capitalMensuel.map(c => c.capital);
|
||||
const nonZeroCap = capitalValues.filter(v => v > 0);
|
||||
const avgCapital = nonZeroCap.length ? nonZeroCap.reduce((s, v) => s + v, 0) / nonZeroCap.length : 0;
|
||||
const lastCapital = [...capitalValues].reverse().find(v => v > 0) ?? avgCapital;
|
||||
|
||||
const perfMensuelle = Array.from({ length: 12 }, (_, i) =>
|
||||
capitalValues[i] > 0 ? perfMonthTotals[i] / capitalValues[i] : null);
|
||||
const perfAnnualisee = perfMensuelle.map(p => p !== null ? p * 12 : null);
|
||||
const perfAnnTotale = lastCapital > 0 ? perfGrandTotal / lastCapital : null;
|
||||
|
||||
/* ── Label total header ──────────────────────────────────────── */
|
||||
const activeTypes = [
|
||||
inclureInterets && { color: chartInterets, label: netMode ? 'Intérêts nets' : 'Intérêts bruts' },
|
||||
inclureCapital && { color: chartCapital, label: 'Capital' },
|
||||
inclureCashback && { color: chartCashback, label: 'Cashback' },
|
||||
].filter(Boolean);
|
||||
|
||||
/* ── Rendu ───────────────────────────────────────────────────── */
|
||||
return (
|
||||
<div className="solde-chart-wrap" style={{ padding: '24px 24px 16px', marginBottom: 24 }}>
|
||||
|
||||
{/* ── Header identique au bar chart ── */}
|
||||
<div className="solde-chart-header">
|
||||
<div className="solde-chart-info">
|
||||
<div style={{ display:'flex', alignItems:'center', gap:5, flexWrap:'wrap', marginBottom:2 }}>
|
||||
{inclureInterets && (
|
||||
<span style={{ display:'inline-flex', alignItems:'center', gap:4,
|
||||
background:hexToRgba(chartInterets,0.12), borderRadius:5, padding:'3px 8px' }}>
|
||||
<span style={{ width:7, height:7, borderRadius:2, background:chartInterets, flexShrink:0 }}/>
|
||||
<span style={{ fontSize:13, color:chartInterets, fontWeight:600 }}>
|
||||
{netMode ? 'Intérêts nets' : 'Intérêts bruts'}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{inclureCapital && (
|
||||
<span style={{ display:'inline-flex', alignItems:'center', gap:4,
|
||||
background:hexToRgba(chartCapital,0.12), borderRadius:5, padding:'3px 8px' }}>
|
||||
<span style={{ width:7, height:7, borderRadius:2, background:chartCapital, flexShrink:0 }}/>
|
||||
<span style={{ fontSize:13, color:chartCapital, fontWeight:600 }}>Capital</span>
|
||||
</span>
|
||||
)}
|
||||
{inclureCashback && (
|
||||
<span style={{ display:'inline-flex', alignItems:'center', gap:4,
|
||||
background:hexToRgba(chartCashback,0.12), borderRadius:5, padding:'3px 8px' }}>
|
||||
<span style={{ width:7, height:7, borderRadius:2, background:chartCashback, flexShrink:0 }}/>
|
||||
<span style={{ fontSize:13, color:chartCashback, fontWeight:600 }}>Cashback</span>
|
||||
</span>
|
||||
)}
|
||||
{!inclureInterets && !inclureCapital && !inclureCashback && (
|
||||
<span style={{ fontSize:13, color:'var(--text-muted)' }}>—</span>
|
||||
)}
|
||||
<span style={{ fontSize:13, color:'var(--text-muted)' }}>· {annee}</span>
|
||||
</div>
|
||||
<div className="solde-chart-value">{fmtEUR(grandTotal)}</div>
|
||||
</div>
|
||||
|
||||
<div className="solde-chart-controls">
|
||||
{/* Bouton intérêts */}
|
||||
<button
|
||||
title={inclureInterets ? 'Intérêts inclus — cliquer pour exclure' : 'Cliquer pour inclure les intérêts'}
|
||||
onClick={() => setInclureInterets(v => !v)}
|
||||
style={{ background: inclureInterets ? hexToRgba(chartInterets,0.13) : 'none',
|
||||
border:'1px solid '+(inclureInterets ? chartInterets : 'transparent'),
|
||||
borderRadius:8, padding:'4px 6px', cursor:'pointer', display:'flex', alignItems:'center',
|
||||
transition:'background .15s,border-color .15s', marginRight:2 }}>
|
||||
<AppIcon name="interets" active={inclureInterets} />
|
||||
</button>
|
||||
{/* Bouton capital */}
|
||||
<button
|
||||
title={inclureCapital ? 'Capital inclus — cliquer pour exclure' : 'Cliquer pour inclure le capital'}
|
||||
onClick={() => setInclureCapital(v => !v)}
|
||||
style={{ background: inclureCapital ? hexToRgba(chartCapital,0.13) : 'none',
|
||||
border:'1px solid '+(inclureCapital ? chartCapital : 'transparent'),
|
||||
borderRadius:8, padding:'4px 6px', cursor:'pointer', display:'flex', alignItems:'center',
|
||||
transition:'background .15s,border-color .15s', marginRight:2 }}>
|
||||
<AppIcon name="capital" active={inclureCapital} />
|
||||
</button>
|
||||
{/* Bouton cashback */}
|
||||
<button
|
||||
title={inclureCashback ? 'Cashback inclus — cliquer pour exclure' : 'Cliquer pour inclure le cashback'}
|
||||
onClick={() => setInclureCashback(v => !v)}
|
||||
style={{ background: inclureCashback ? hexToRgba(chartCashback,0.13) : 'none',
|
||||
border:'1px solid '+(inclureCashback ? chartCashback : 'transparent'),
|
||||
borderRadius:8, padding:'4px 6px', cursor:'pointer', display:'flex', alignItems:'center',
|
||||
transition:'background .15s,border-color .15s', marginRight:2 }}>
|
||||
<AppIcon name="cashback" active={inclureCashback} />
|
||||
</button>
|
||||
|
||||
{/* Sélecteur d'années */}
|
||||
<div className="solde-chart-ranges">
|
||||
<button className="solde-range-btn"
|
||||
onClick={() => setWindowStart(w => Math.max(0, w-1))}
|
||||
disabled={!canPrev} style={{ opacity: canPrev ? 1 : 0.3 }}>‹</button>
|
||||
{visibleYears.map(y => (
|
||||
<button key={y} className={`solde-range-btn${annee === y ? ' active' : ''}`}
|
||||
onClick={() => setAnnee(y)}>
|
||||
{y}
|
||||
</button>
|
||||
))}
|
||||
<button className="solde-range-btn"
|
||||
onClick={() => setWindowStart(w => Math.min(Math.max(0, availableYears.length - 3), w+1))}
|
||||
disabled={!canNext} style={{ opacity: canNext ? 1 : 0.3 }}>›</button>
|
||||
<button className={`solde-range-btn${modeGlobal ? ' active' : ''}`}
|
||||
onClick={() => toggleModeGlobal()}>
|
||||
TOUT
|
||||
</button>
|
||||
{expandButton}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Tableau ── */}
|
||||
<div style={{ overflowX: 'auto', marginTop: 20, position: 'relative', zIndex: 0 }}>
|
||||
<table className="tip-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="tip-th-empty" />
|
||||
<th className="tip-th-year" colSpan={12}>{annee}</th>
|
||||
<th className="tip-th-empty" />
|
||||
<th className="tip-th-empty" />
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="tip-th-name tip-th-name-amber">
|
||||
<span style={{ display:'flex', alignItems:'center', gap:6 }}>
|
||||
Plateforme
|
||||
{multiDetenteur && (
|
||||
<button
|
||||
onClick={() => toggleGroupByNom()}
|
||||
title={groupByNom ? 'Vue consolidée — cliquer pour détailler par détenteur' : 'Vue détaillée — cliquer pour consolider par plateforme'}
|
||||
style={{
|
||||
display:'inline-flex', alignItems:'center', gap:3,
|
||||
background:'rgba(255,255,255,0.15)', border:'1px solid rgba(255,255,255,0.3)',
|
||||
borderRadius:4, padding:'2px 5px', cursor:'pointer',
|
||||
fontSize:10, fontWeight:600, color:'#fff', letterSpacing:'.03em',
|
||||
lineHeight:1.4, whiteSpace:'nowrap', transition:'background .15s',
|
||||
}}>
|
||||
{groupByNom ? 'Consolidé' : 'Détaillé'}
|
||||
<span style={{ display:'inline-flex', transition:'transform .2s', transform: groupByNom ? 'rotate(180deg)' : 'rotate(0deg)' }}>
|
||||
<ChevronDown size={9} />
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</th>
|
||||
{MOIS_LONG.map((m, i) => (
|
||||
<th key={m} className={`tip-th-month${annee === currentYear && i === currentMonth - 1 ? ' tip-th-month-current' : ''}`}>{m}</th>
|
||||
))}
|
||||
<th className="tip-th-total">Total</th>
|
||||
<th className="tip-th-avg">Moy. mensuelle</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{grid.map((plat, pi) => (
|
||||
<tr key={plat.id} className="tip-row-plat">
|
||||
<td className="tip-td-name">
|
||||
{plat.nom}
|
||||
{!groupByNom && multiDetenteur && plat.detenteur_nom && (
|
||||
<span style={{ marginLeft: 6, fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', fontWeight: 400 }}>
|
||||
{plat.detenteur_nom}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
{plat.months.map((v, mi) => {
|
||||
const isCurrent = annee === currentYear && mi === currentMonth - 1;
|
||||
const cellKey = `${plat.id}:${annee}-${String(mi + 1).padStart(2,'0')}`;
|
||||
const isActive = activeCell?.key === cellKey;
|
||||
const clickable = !!v;
|
||||
return (
|
||||
<td key={mi}
|
||||
className={`tip-td-num${v?.projected ? ' tip-projected' : ''}${isCurrent ? ' tip-col-current' : ''}${isActive ? ' tip-td-active' : ''}${clickable ? ' tip-td-clickable' : ''}`}
|
||||
onClick={() => clickable && onCellClick && onCellClick({
|
||||
key: cellKey,
|
||||
platId: plat.id,
|
||||
platNom: plat.nom,
|
||||
annee,
|
||||
mois: String(mi + 1).padStart(2, '0'),
|
||||
moisLabel: MOIS_LONG[mi],
|
||||
})}
|
||||
>
|
||||
{v ? fmtEUR(v.value) : <span className="tip-dash">—</span>}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td className="tip-td-total">
|
||||
{platTotals[pi] > 0 ? fmtEUR(platTotals[pi]) : <span className="tip-dash">—</span>}
|
||||
</td>
|
||||
<td className="tip-td-avg">
|
||||
{platTotals[pi] > 0 ? fmtEUR(platTotals[pi] / 12) : <span className="tip-dash">—</span>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
<tfoot>
|
||||
<tr className="tip-footer-total">
|
||||
<td className="tip-td-name">Toutes les plateformes</td>
|
||||
{monthTotals.map((v, i) => (
|
||||
<td key={i} className={`tip-td-num${annee === currentYear && i === currentMonth - 1 ? ' tip-col-current' : ''}`}>
|
||||
{v > 0 ? fmtEUR(v) : <span className="tip-dash">—</span>}
|
||||
</td>
|
||||
))}
|
||||
<td className="tip-td-total">{fmtEUR(grandTotal)}</td>
|
||||
<td className="tip-td-avg">{grandTotal > 0 ? fmtEUR(grandTotal / 12) : <span className="tip-dash">—</span>}</td>
|
||||
</tr>
|
||||
<tr className="tip-footer-capital">
|
||||
<td className="tip-td-name">Capital investi</td>
|
||||
{capitalValues.map((v, i) => (
|
||||
<td key={i} className={`tip-td-num${annee === currentYear && i === currentMonth - 1 ? ' tip-col-current' : ''}`}>
|
||||
{v > 0 ? fmtEUR(v) : <span className="tip-dash">—</span>}
|
||||
</td>
|
||||
))}
|
||||
<td className="tip-td-total">{lastCapital > 0 ? fmtEUR(lastCapital) : <span className="tip-dash">—</span>}</td>
|
||||
<td className="tip-td-void" />
|
||||
</tr>
|
||||
<tr className="tip-footer-perf">
|
||||
<td className="tip-td-name">{netMode ? "Performance nette mensuelle" : "Performance brute mensuelle"}</td>
|
||||
{perfMensuelle.map((v, i) => (
|
||||
<td key={i} className={`tip-td-num${annee === currentYear && i === currentMonth - 1 ? ' tip-col-current' : ''}`}>
|
||||
{v !== null ? fmtPct(v * 100) : <span className="tip-dash">—</span>}
|
||||
</td>
|
||||
))}
|
||||
<td className="tip-td-total">
|
||||
{perfAnnTotale !== null ? fmtPct((perfAnnTotale / 12) * 100) : <span className="tip-dash">—</span>}
|
||||
</td>
|
||||
<td className="tip-td-void" />
|
||||
</tr>
|
||||
<tr className="tip-footer-perf">
|
||||
<td className="tip-td-name">{netMode ? "Performance nette annualisée" : "Performance brute annualisée"}</td>
|
||||
{perfAnnualisee.map((v, i) => (
|
||||
<td key={i} className={`tip-td-num${annee === currentYear && i === currentMonth - 1 ? ' tip-col-current' : ''}`}>
|
||||
{v !== null ? fmtPct(v * 100) : <span className="tip-dash">—</span>}
|
||||
</td>
|
||||
))}
|
||||
<td className="tip-td-total">
|
||||
{perfAnnTotale !== null ? fmtPct(perfAnnTotale * 100) : <span className="tip-dash">—</span>}
|
||||
</td>
|
||||
<td className="tip-td-void" />
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* ── Sélecteur Reçu / Projeté ── */}
|
||||
<div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginTop:12, gap:12, flexWrap:'wrap' }}>
|
||||
<div style={{
|
||||
display:'inline-flex',
|
||||
background:'#f0f0f0',
|
||||
borderRadius:8,
|
||||
padding:3,
|
||||
gap:2,
|
||||
flexShrink:0,
|
||||
}}>
|
||||
{[
|
||||
{ key:'actual', label:'Reçu', active:showActual, toggle:toggleActual },
|
||||
{ key:'projected', label:'Projeté', active:showProjected, toggle:toggleProjected },
|
||||
].map(btn => (
|
||||
<button key={btn.key} onClick={() => btn.toggle()} style={{
|
||||
border:'none', cursor:'pointer', padding:'5px 14px', borderRadius:6,
|
||||
fontSize:'var(--fs-sm)',
|
||||
fontWeight: btn.active ? 600 : 400,
|
||||
background: btn.active ? '#ffffff' : 'transparent',
|
||||
color: btn.active ? '#1a1a2e' : '#9ca3af',
|
||||
boxShadow: btn.active ? '0 1px 3px rgba(0,0,0,0.13)' : 'none',
|
||||
transition: 'all .15s', lineHeight: 1.4,
|
||||
}}>{btn.label}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user