From b48731ec50ec7e37f8be2d5ce05df75b9485d731 Mon Sep 17 00:00:00 2001 From: Olivier Date: Fri, 21 Aug 2026 10:38:13 +0200 Subject: [PATCH] =?UTF-8?q?Mias=20=C3=A0=20jour=20des=20KPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/routes/dashboard.js | 8 +++- frontend/src/pages/Dashboard.jsx | 76 +++++++++++++++++++++++++++----- 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/backend/src/routes/dashboard.js b/backend/src/routes/dashboard.js index 789ab1b..82260f0 100644 --- a/backend/src/routes/dashboard.js +++ b/backend/src/routes/dashboard.js @@ -152,6 +152,12 @@ router.get('/', (req, res) => { cashByPlatformEnriched.reduce((s, p) => s + p.solde_portefeuille, 0) * 100 ) / 100; + // Total des corrections de solde, toutes plateformes confondues (cumul complet + // quand asOf est absent) — utilisé par le KPI "Gain net depuis le début" du Dashboard. + const corrections_total = Math.round( + correctionsPerPlat.reduce((s, r) => s + r.total_correction, 0) * 100 + ) / 100; + /* ── Portfolio ───────────────────────────────────────────────── */ const portfolio = db.prepare(` SELECT @@ -242,7 +248,7 @@ router.get('/', (req, res) => { ORDER BY i.date_souscription DESC `).all(...invParams); - res.json({ cash, cashByPlatform: cashByPlatformEnriched, solde_portefeuille_total, portfolio, interets, interetsParAnnee, upcoming, enDefaut }); + res.json({ cash, cashByPlatform: cashByPlatformEnriched, solde_portefeuille_total, corrections_total, portfolio, interets, interetsParAnnee, upcoming, enDefaut }); }); diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 6d40929..dd3f5da 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -179,7 +179,7 @@ function KpiCard({ title, value, badge, refValue, onClick, hint }) { } /* ── KPI filtrés par année ── */ -function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activeId, activeView, soldePortefeuilleLive }) { +function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activeId, activeView, soldePortefeuilleLive, correctionsTotal }) { const { annee, modeGlobal, rawDataGlobal, rawData, currentYear, currentMonth, setInclureInterets, setInclureCapital, setInclureCashback, @@ -192,26 +192,46 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ // (GET /dashboard[?asOf=]), formule unique cf. mémoire // project_taxreport_interets_plateforme. "Depuis le début" → valeur live // déjà chargée par le parent ; année précise → snapshot fin d'année. + // + // Comparaison : en vue live (mode global ou année courante), on compare au + // solde de fin du mois calendaire précédent (comme le KPI "Gain net depuis + // le début"). En vue d'une année passée, on garde la comparaison fin + // d'année vs fin d'année précédente (seule comparaison qui a du sens pour + // un snapshot historique figé au 31/12). + const prevMonthDatePM = new Date(currentYear, currentMonth - 2, 1); + const prevMonthEndDatePM = new Date(prevMonthDatePM.getFullYear(), prevMonthDatePM.getMonth() + 1, 0); + const walletPrevMonthKey = `${prevMonthEndDatePM.getFullYear()}-${String(prevMonthEndDatePM.getMonth() + 1).padStart(2, '0')}-${String(prevMonthEndDatePM.getDate()).padStart(2, '0')}`; + const walletPrevMonthLabel = `${['jan.','fév.','mar.','avr.','mai','juin','juil.','août','sep.','oct.','nov.','déc.'][prevMonthDatePM.getMonth()]} ${prevMonthDatePM.getFullYear()}`; + const [walletAsOf, setWalletAsOf] = useState({}); useEffect(() => { - if (modeGlobal) return; if (!activeId && activeView !== 'all') return; const scopeParams = activeView === 'all' ? { scope: 'all' } : undefined; - const prevYear = Number(annee) - 1; - const keys = [`${annee}-12-31`, `${prevYear}-12-31`]; + const keys = []; + if (modeGlobal || isCurrentYear) { + keys.push(walletPrevMonthKey); + } + if (!modeGlobal && !isCurrentYear) { + const prevYear = Number(annee) - 1; + keys.push(`${annee}-12-31`, `${prevYear}-12-31`); + } + if (!keys.length) return; Promise.all(keys.map(asOf => api.get('/dashboard', { ...scopeParams, asOf }).then(d => [asOf, d]) )).then(entries => { setWalletAsOf(prev => ({ ...prev, ...Object.fromEntries(entries) })); }).catch(e => console.error('walletAsOf load error:', e)); - }, [annee, modeGlobal, activeId, activeView]); // eslint-disable-line + }, [annee, modeGlobal, isCurrentYear, walletPrevMonthKey, activeId, activeView]); // eslint-disable-line - const soldePortefeuilleValue = modeGlobal + const soldePortefeuilleValue = (modeGlobal || isCurrentYear) ? soldePortefeuilleLive : (walletAsOf[`${annee}-12-31`]?.solde_portefeuille_total ?? null); - const soldePortefeuillePrev = modeGlobal - ? null + const soldePortefeuillePrev = (modeGlobal || isCurrentYear) + ? (walletAsOf[walletPrevMonthKey]?.solde_portefeuille_total ?? null) : (walletAsOf[`${Number(annee) - 1}-12-31`]?.solde_portefeuille_total ?? null); + const soldePortefeuillePrevLabel = (modeGlobal || isCurrentYear) + ? walletPrevMonthLabel + : String(Number(annee) - 1); // 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) @@ -317,6 +337,32 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ // eslint-disable-next-line react-hooks/exhaustive-deps }, [rawDataGlobal, annee, modeGlobal, pfuRates]); + // ── Gain net depuis le début ────────────────────────────────── + // Toujours net (indépendant du toggle Brut/Net), toujours cumulé sur tout + // l'historique (indépendant du sélecteur d'année) : intérêts nets + cashback + // (investissement + bonus parrainage/plateforme, déjà inclus dans le champ + // "cashback" de interets-annuels) + corrections de solde. Volontairement + // 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(() => { + 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 }); + return totals.interets_nets + totals.cashback + (correctionsTotal || 0); + }, [rawDataGlobal, correctionsTotal]); + + // 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, + // via thisMonthRow, déjà chargé pour l'année courante). Les corrections de solde + // 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); + return gainNetTotal - deltaCeMois; + }, [gainNetTotal, thisMonthRow]); + // ── Valeur affichée ── const getValue = (field) => { if (isCurrentYear) return thisMonthRow?.[field] || 0; @@ -410,11 +456,11 @@ function DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData, activ title="Porte-monnaie" hint="Cash disponible sur les porte-monnaie des plateformes (hors compte courant)" value={soldePortefeuilleValue != null ? fmtEUR(soldePortefeuilleValue) : '…'} - badge={!modeGlobal && soldePortefeuilleValue != null && soldePortefeuillePrev != null + badge={soldePortefeuilleValue != null && soldePortefeuillePrev != null ? : null} - refValue={!modeGlobal && soldePortefeuillePrev != null - ? fmtEUR(soldePortefeuillePrev) + ' en ' + (Number(annee) - 1) + refValue={soldePortefeuillePrev != null + ? fmtEUR(soldePortefeuillePrev) + ' en ' + soldePortefeuillePrevLabel : null} /> { setInclureInterets(false); setInclureCapital(false); setInclureCashback(true); }} /> + } + refValue={fmtEUR(gainNetPrevMonthEnd) + ' en ' + prevMonthLabel} + /> ); } @@ -569,6 +622,7 @@ export default function Dashboard() {