Mise en place de la feature de gestion des Frais
This commit is contained in:
1 parent
9ea078302d
commit
0334a63139
27 files changed
+2401
-117
No files matched your search
@@ -183,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, allRembs, allReinvests, allSimulRembs }) {
|
||||
function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activeId, activeView, soldePortefeuilleLive, correctionsTotal, investissements, allRembs, allReinvests, allSimulRembs, allFraisOps }) {
|
||||
// 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à
|
||||
@@ -360,8 +360,19 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ
|
||||
interets_nets: acc.interets_nets + (r.interets_nets || 0),
|
||||
cashback: acc.cashback + (r.cashback || 0),
|
||||
}), { interets_nets: 0, cashback: 0 });
|
||||
return totals.interets_nets + totals.cashback + (correctionsTotal || 0);
|
||||
}, [rawDataGlobal, correctionsTotal]);
|
||||
// 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).
|
||||
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;
|
||||
return sum + (fo.montant || 0);
|
||||
}, 0);
|
||||
return totals.interets_nets + totals.cashback + (correctionsTotal || 0) - fraisHorsRembTotal;
|
||||
}, [rawDataGlobal, correctionsTotal, investissements, allFraisOps]);
|
||||
|
||||
// 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,
|
||||
@@ -369,9 +380,22 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ
|
||||
// n'ont pas de granularité mensuelle côté backend ; elles restent donc dans le
|
||||
// cumul "avant ce mois", ce qui est sans incidence pratique (montants marginaux).
|
||||
const gainNetPrevMonthEnd = useMemo(() => {
|
||||
const deltaCeMois = (thisMonthRow?.interets_nets || 0) + (thisMonthRow?.cashback || 0);
|
||||
// Même granularité mensuelle que interets_nets/cashback pour les frais hors
|
||||
// remboursement (portefeuille/compte_courant) : contrairement aux corrections de
|
||||
// solde (jamais datées finement), frais_operations porte une vraie date_operation —
|
||||
// on peut donc isoler la part du mois en cours plutôt que de la laisser polluer tout
|
||||
// le delta M vs M-1 (cf. gainNetTotal ci-dessus).
|
||||
const thisMonthStr = `${currentYear}-${String(currentMonth).padStart(2, '0')}`;
|
||||
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.date_operation?.startsWith(thisMonthStr)) return sum;
|
||||
return sum + (fo.montant || 0);
|
||||
}, 0);
|
||||
const deltaCeMois = (thisMonthRow?.interets_nets || 0) + (thisMonthRow?.cashback || 0) - fraisCeMois;
|
||||
return gainNetTotal - deltaCeMois;
|
||||
}, [gainNetTotal, thisMonthRow]);
|
||||
}, [gainNetTotal, thisMonthRow, currentYear, currentMonth, investissements, allFraisOps]);
|
||||
|
||||
// ── 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
|
||||
@@ -430,6 +454,16 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ
|
||||
flowsBrut.push({ amount: (rb.capital || 0) + (rb.cashback || 0) + (rb.interets_bruts || 0), date: d });
|
||||
flowsNet.push({ amount: rb.net_recu || 0, date: d });
|
||||
}
|
||||
// Frais réglés en dehors des montants déjà nettés (source à la souscription,
|
||||
// et frais liés à un remboursement déjà déduits de net_recu) : mode 'portefeuille'
|
||||
// ou 'compte_courant' — vraie sortie d'argent, comptée uniquement dans le XIRR NET.
|
||||
for (const fo of (allFraisOps || [])) {
|
||||
if (!ids.has(fo.investissement_id)) continue;
|
||||
if (fo.mode_reglement !== 'portefeuille' && fo.mode_reglement !== 'compte_courant') continue;
|
||||
const d = fo.date_operation?.slice(0, 10);
|
||||
if (!d || d > cutoff) continue;
|
||||
flowsNet.push({ amount: -(fo.montant || 0), date: d });
|
||||
}
|
||||
let estime = false;
|
||||
for (const r of invs) {
|
||||
const capInv = r.montant_investi + (reinvestCumulParInvGlobal[r.id] || 0);
|
||||
@@ -445,7 +479,7 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ
|
||||
flowsBrut.sort(byDate);
|
||||
flowsNet.sort(byDate);
|
||||
return { brut: xirr(flowsBrut), net: xirr(flowsNet), estime, cutoff };
|
||||
}, [investissements, allRembs, allReinvests, reinvestCumulParInvGlobal, capRembParInvGlobal]);
|
||||
}, [investissements, allRembs, allReinvests, allFraisOps, reinvestCumulParInvGlobal, capRembParInvGlobal]);
|
||||
|
||||
const xirrGlobalAttendu = useMemo(() => {
|
||||
const cutoff = today();
|
||||
@@ -472,6 +506,16 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ
|
||||
flowsBrut.push({ amount: (rb.capital || 0) + (rb.cashback || 0) + (rb.interets_bruts || 0), date: d });
|
||||
flowsNet.push({ amount: rb.net_recu || 0, date: d });
|
||||
}
|
||||
// Frais réglés en dehors des montants déjà nettés (source à la souscription,
|
||||
// et frais liés à un remboursement déjà déduits de net_recu) : mode 'portefeuille'
|
||||
// ou 'compte_courant' — vraie sortie d'argent, comptée uniquement dans le XIRR NET.
|
||||
for (const fo of (allFraisOps || [])) {
|
||||
if (!ids.has(fo.investissement_id)) continue;
|
||||
if (fo.mode_reglement !== 'portefeuille' && fo.mode_reglement !== 'compte_courant') continue;
|
||||
const d = fo.date_operation?.slice(0, 10);
|
||||
if (!d || d > cutoff) continue;
|
||||
flowsNet.push({ amount: -(fo.montant || 0), date: d });
|
||||
}
|
||||
const futureSimulParInv = {};
|
||||
for (const sr of (allSimulRembs || [])) {
|
||||
if (!ids.has(sr.investissement_id)) continue;
|
||||
@@ -504,7 +548,7 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ
|
||||
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]);
|
||||
}, [investissements, allRembs, allReinvests, allFraisOps, allSimulRembs, reinvestCumulParInvGlobal, capRembParInvGlobal, pfuRates]);
|
||||
|
||||
// ── Valeur affichée ──
|
||||
const getValue = (field) => {
|
||||
@@ -640,7 +684,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, 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)."
|
||||
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)."
|
||||
value={fmtEUR(gainNetTotal)}
|
||||
badge={<TrendBadge current={gainNetTotal} prev={gainNetPrevMonthEnd} />}
|
||||
refValue={fmtEUR(gainNetPrevMonthEnd) + ' en ' + prevMonthLabel}
|
||||
@@ -648,10 +692,11 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ
|
||||
{(() => {
|
||||
const rendement = netMode ? xirrGlobal.net : xirrGlobal.brut;
|
||||
const rendementAttendu = netMode ? xirrGlobalAttendu.net : xirrGlobalAttendu.brut;
|
||||
const fraisExplication = ` Les frais réglés hors remboursement (via le porte-monnaie ou un compte courant) sont eux aussi comptés comme une sortie d'argent dans ce calcul net — en plus des frais déjà déduits à la source ou d'un remboursement, toujours pris en compte.`;
|
||||
const xirrExplication =
|
||||
`XIRR (taux de rendement interne actualisé) : taux annualisé calculé à partir de l'ensemble des flux réels datés ` +
|
||||
`des prêts du portefeuille (versements initiaux, réinvestissements, remboursements ${netMode ? 'nets, après fiscalité' : 'bruts, avant fiscalité'}), ` +
|
||||
`depuis le début. Chaque flux est pondéré par sa date exacte.\n\n` +
|
||||
`depuis le début. Chaque flux est pondéré par sa date exacte.` + (netMode ? fraisExplication : '') + `\n\n` +
|
||||
(xirrGlobal.estime
|
||||
? `Certains prêts ne sont pas encore soldés : leur capital restant dû est ajouté en flux final, valorisé à aujourd'hui — c'est donc une estimation, pas un taux définitif.`
|
||||
: 'Tous les prêts du portefeuille sont soldés : taux définitif.');
|
||||
@@ -660,7 +705,7 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ
|
||||
`(ex. "Mon taux de rendement interne" chez Bienprêter, le %/an affiché chez La Première Brique), ce calcul suppose ` +
|
||||
`que toutes les échéances prévues (capital + intérêts) des prêts encore en cours seront honorées exactement à la ` +
|
||||
`date prévue par l'échéancier — contrairement au XIRR ci-dessus, qui valorise prudemment le capital restant dû ` +
|
||||
`"comme racheté aujourd'hui", sans compter les intérêts futurs.\n\n` +
|
||||
`"comme racheté aujourd'hui", sans compter les intérêts futurs.` + (netMode ? fraisExplication : '') + `\n\n` +
|
||||
`C'est donc une projection optimiste : elle ne tient pas compte d'un éventuel retard ou défaut de paiement ` +
|
||||
`(voir le KPI "Capital en risque" pour ce risque). Si un prêt n'a pas d'échéancier prévisionnel en base, il est ` +
|
||||
`valorisé de la même façon que dans le XIRR réel.`;
|
||||
@@ -736,6 +781,7 @@ export default function Dashboard() {
|
||||
const [allRembs, setAllRembs] = useState([]);
|
||||
const [allReinvests, setAllReinvests] = useState([]);
|
||||
const [allSimulRembs, setAllSimulRembs] = useState([]);
|
||||
const [allFraisOps, setAllFraisOps] = useState([]);
|
||||
const [drillRefreshKey, setDrillRefreshKey] = useState(0);
|
||||
|
||||
/* ── drillCell : cellule sélectionnée dans le TIP — par défaut mois courant toutes plateformes ── */
|
||||
@@ -792,6 +838,10 @@ export default function Dashboard() {
|
||||
api.get('/remboursements', scopeParams).then(setAllRembs).catch(() => setAllRembs([])),
|
||||
api.get('/reinvestissements', { scope: 'all' }).then(setAllReinvests).catch(() => setAllReinvests([])),
|
||||
api.get('/simul/all', scopeParams).then(setAllSimulRembs).catch(() => setAllSimulRembs([])),
|
||||
// Nécessaire au XIRR net (frais réglés par portefeuille/compte courant, cf. Étape 5) —
|
||||
// sans investissement_id, ne connaît que l'utilisateur (tous investisseurs), filtré
|
||||
// ensuite via `ids` au moment du calcul, même pattern que allFraisOps dans Plateformes.jsx.
|
||||
api.get('/frais-operations').then(setAllFraisOps).catch(() => setAllFraisOps([])),
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -849,7 +899,7 @@ export default function Dashboard() {
|
||||
portfolio={portfolio} netMode={netMode} pfuRates={pfuRates} capitalMensuelData={capitalMensuelData}
|
||||
activeId={activeId} activeView={activeView} soldePortefeuilleLive={data?.solde_portefeuille_total ?? null}
|
||||
correctionsTotal={data?.corrections_total ?? 0} investissements={investissements}
|
||||
allRembs={allRembs} allReinvests={allReinvests} allSimulRembs={allSimulRembs}
|
||||
allRembs={allRembs} allReinvests={allReinvests} allSimulRembs={allSimulRembs} allFraisOps={allFraisOps}
|
||||
/>
|
||||
<YearSelectorKpi />
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user