Correctif

This commit is contained in:
ocroguennec committed 2026-09-02 08:28:15 +02:00
1 parent 0334a63139
commit 981ddf5830
7 files changed
+115 -42

No files matched your search

+61 -13
View File
@@ -157,17 +157,31 @@ function TrendBadge({ current, prev, invert = false }) {
}
/* ── Carte KPI individuelle ── */
function KpiCard({ title, value, badge, refValue, note, onClick, hint }) {
function KpiCard({ title, value, badge, refValue, note, onClick, hint, infoTooltip }) {
return (
<div
className="kpi"
title={hint}
title={infoTooltip ? undefined : hint}
onClick={onClick}
style={onClick ? { cursor: 'pointer', transition: 'box-shadow 0.15s, opacity 0.15s' } : undefined}
onMouseEnter={onClick ? (e) => { 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}
>
<div className="label">{title}</div>
<div className="label" style={infoTooltip ? { display: 'flex', alignItems: 'center', gap: 4 } : undefined}>
<span>{title}</span>
{infoTooltip && (
<span
className="cell-tooltip tooltip-down"
data-tooltip={infoTooltip}
style={{ display: 'inline-flex', color: 'var(--text-muted)' }}
onClick={(e) => e.stopPropagation()}
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
</span>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{value}</span>
{badge}
@@ -355,24 +369,58 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ
// indépendant du capital encore investi et du solde porte-monnaie courant :
// reste juste même si tout l'argent est retiré des plateformes.
// Voir mémoire project_gain_net_depuis_debut pour la dérivation complète.
const gainNetTotal = useMemo(() => {
// gainNetDetail : mêmes totaux que gainNetTotal ci-dessous, mais conservés en détail
// (pas seulement le résultat final) pour alimenter le popup ⓘ de la carte KPI — demande
// Olivier du 02/09/26, juste après le correctif de la règle de déduction des frais.
const gainNetDetail = useMemo(() => {
const totals = (rawDataGlobal.rembourses ?? []).reduce((acc, r) => ({
interets_nets: acc.interets_nets + (r.interets_nets || 0),
cashback: acc.cashback + (r.cashback || 0),
}), { interets_nets: 0, cashback: 0 });
// Frais réglés en dehors des montants déjà nettés (frais 'source' à la souscription,
// et frais liés à un remboursement déjà déduits de interets_nets/net_recu) : mode
// 'portefeuille' ou 'compte_courant' — vraie sortie d'argent pour l'investisseur,
// jusque-là absente de ce total (même trou que celui corrigé sur le XIRR net à
// l'Étape 5, cf. xirrGlobal plus bas — signalé par Olivier le 01/09/26).
// Tous les frais réels réglés par l'investisseur SAUF ceux retenus directement sur un
// remboursement (mode_reglement='remboursement') — ceux-là sont déjà déduits de
// interets_nets/net_recu au moment du passage brut → net par la plateforme (ex.
// Fundora) : les soustraire ici referait une 2ᵉ diminution du gain, ce qui ne
// correspond à aucune sortie d'argent réelle supplémentaire (piège signalé par
// Olivier le 02/09/26). En revanche mode 'source' (déduit du versement à la
// souscription — déjà netté dans montant_investi, une donnée de CAPITAL que ce
// total n'utilise jamais) et 'portefeuille'/'compte_courant' (Baltis, réglés à part)
// sont chacun une vraie sortie d'argent jamais reflétée ailleurs dans cette somme
// (qui ne fait intervenir que des flux de revenu : intérêts, cashback, corrections)
// — les trois doivent donc être déduits. Ne PAS réutiliser tel quel le filtre
// ('portefeuille','compte_courant') de xirrGlobal (Étape 5) : le XIRR exclut
// 'source' pour une raison différente et spécifique au XIRR (déjà représenté par un
// flux de capital investi plus petit à t0), qui ne s'applique pas ici puisque ce
// total ne comporte aucun flux de capital.
const idsGainNet = new Set((investissements || []).map(r => r.id));
const fraisHorsRembTotal = (allFraisOps || []).reduce((sum, fo) => {
if (!idsGainNet.has(fo.investissement_id)) return sum;
if (fo.mode_reglement !== 'portefeuille' && fo.mode_reglement !== 'compte_courant') return sum;
if (fo.mode_reglement === 'remboursement') return sum;
return sum + (fo.montant || 0);
}, 0);
return totals.interets_nets + totals.cashback + (correctionsTotal || 0) - fraisHorsRembTotal;
const corrections = correctionsTotal || 0;
const avantFrais = totals.interets_nets + totals.cashback + corrections;
return {
interetsNets: totals.interets_nets,
cashback: totals.cashback,
corrections,
avantFrais,
frais: fraisHorsRembTotal,
total: avantFrais - fraisHorsRembTotal,
};
}, [rawDataGlobal, correctionsTotal, investissements, allFraisOps]);
const gainNetTotal = gainNetDetail.total;
const gainNetInfoTooltip =
`Intérêts nets + cashback + bonus parrainage/plateforme + corrections de solde, moins ` +
`tous les frais réellement payés hors du passage brut → net d'un remboursement — cumulé ` +
`depuis le début, indépendant du capital encore investi ou du solde actuel.\n\n` +
`Détail du calcul :\n\n` +
`Intérêts nets perçus : ${fmtEUR(gainNetDetail.interetsNets)}\n` +
`+ Cashback et bonus (parrainage/plateforme) : ${fmtEUR(gainNetDetail.cashback)}\n` +
`+ Corrections de solde : ${fmtEUR(gainNetDetail.corrections)}\n` +
`= Gain avant frais : ${fmtEUR(gainNetDetail.avantFrais)}\n\n` +
`− Frais pris en compte (hors frais déjà déduits d'un remboursement) : ${fmtEUR(gainNetDetail.frais)}\n\n` +
`= Gain net depuis le début : ${fmtEUR(gainNetDetail.total)}`;
// Valeur "Gain net" telle qu'elle aurait été affichée fin du mois précédent —
// on retire du cumul total la part perçue ce mois-ci (intérêts nets + cashback,
@@ -389,7 +437,7 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ
const idsGainNet = new Set((investissements || []).map(r => r.id));
const fraisCeMois = (allFraisOps || []).reduce((sum, fo) => {
if (!idsGainNet.has(fo.investissement_id)) return sum;
if (fo.mode_reglement !== 'portefeuille' && fo.mode_reglement !== 'compte_courant') return sum;
if (fo.mode_reglement === 'remboursement') return sum;
if (!fo.date_operation?.startsWith(thisMonthStr)) return sum;
return sum + (fo.montant || 0);
}, 0);
@@ -684,7 +732,7 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ
/>
<KpiCard
title="Gain net depuis le début"
hint="Intérêts nets + cashback + bonus parrainage/plateforme + corrections de solde, moins les frais réglés hors remboursement (porte-monnaie ou compte courant du détenteur), cumulés depuis le début — indépendant du capital encore investi ou du solde actuel sur les plateformes (reste juste même en cas de retrait complet)."
infoTooltip={gainNetInfoTooltip}
value={fmtEUR(gainNetTotal)}
badge={<TrendBadge current={gainNetTotal} prev={gainNetPrevMonthEnd} />}
refValue={fmtEUR(gainNetPrevMonthEnd) + ' en ' + prevMonthLabel}