Amélioration du traitement des mouvements

This commit is contained in:
ocroguennec committed 2026-08-21 09:20:48 +02:00
1 parent 9540221577
commit 6c96e023a9
8 files changed
+348 -82

No files matched your search

+54 -22
View File
@@ -11,14 +11,14 @@ import Modal from '../components/Modal.jsx';
import ConfirmModal from '../components/ConfirmModal.jsx';
import SoldeChart from '../components/SoldeChart.jsx';
import DistributionChart from '../components/DistributionChart.jsx';
import { fmtEUR, fmtDate, today } from '../utils/format.js';
import { fmtEUR, fmtDate, today, rangeFromDate } from '../utils/format.js';
import DepotsMensuelTable from '../components/DepotsMensuelTable.jsx';
import SuiviObjectifs from '../components/SuiviObjectifs.jsx';
import * as XLSX from 'xlsx';
import {
DEBIT_TYPES, signedMontant, MOUV_TYPE_LABELS, mouvToCSV, mouvToXLS, mouvToJSON,
ExportDropdown, AddDropdown, CorrectionPickerModal, DetailPanel, CorrectionModal, MOIS_FR, emptyMouvementForm,
timestampSuffix, dlBlob,
timestampSuffix, dlBlob, sortMouvements, computeSoldeIndicatifMap,
} from '../components/MouvementsShared.jsx';
/* ── Indicateur de progression ── */
@@ -109,15 +109,18 @@ export default function DepotsRetraits() {
// cf. mémoire project_taxreport_interets_plateforme.
const [dashDataAsOf, setDashDataAsOf] = useState({});
// Année sélectionnée, partagée entre l'onglet Plateformes et le filtre "Année" de Mouvements
const currentYear = String(new Date().getFullYear());
const [filter, setFilter] = useState({ type: '', plateforme_ids: [], year: currentYear, month: '' });
// — vide par défaut ("Toutes les années") au chargement initial de la page.
const [filter, setFilter] = useState({ type: '', plateforme_ids: [], year: '', month: '' });
const [modalOpen, setModalOpen] = useState(false);
const [editingId, setEditingId] = useState(null);
const [form, setForm] = useState(emptyMouvementForm);
const [err, setErr] = useState(null);
const [loading, setLoading] = useState(false);
const [selectedRow, setSelectedRow] = useState(null);
const [drPlatYear, setDrPlatYear] = useState(currentYear);
const [drPlatYear, setDrPlatYear] = useState('');
// Plage du graphique de solde (1J/7J/1M/3M/YTD/1A/TOUT), partagée avec la
// Distribution pour qu'elle affiche toujours la même période que le graphique.
const [chartRange, setChartRange] = useState('TOUT');
const [allRemb, setAllRemb] = useState([]);
const [allInv, setAllInv] = useState([]);
const [allReinvests, setAllReinvests] = useState([]);
@@ -236,12 +239,26 @@ export default function DepotsRetraits() {
return true;
});
// chartRows : kpiRows + filtre type (pour les graphiques)
// chartRows : plateforme + type + année sélectionnée (comme sur Remboursements —
// choisir une année dans le sélecteur "Répartition par plateforme" recentre aussi
// le graphique de solde et la Distribution sur cette année). Par défaut, l'année
// vaut "Toutes les années" (drPlatYear === ''), donc "Tout" dans le graphique de
// solde remonte bien jusqu'au tout premier mouvement.
const chartRows = kpiRows.filter(r => {
if (filter.type && r.type !== filter.type) return false;
return true;
});
// distributionRows : chartRows restreint à la plage du graphique de solde
// (1J/7J/1M/3M/YTD/1A/TOUT) — pour que la Distribution reflète toujours la
// même période que le graphique auquel elle est associée.
const distributionRows = useMemo(() => {
const from = rangeFromDate(chartRange);
if (!from) return chartRows;
return chartRows.filter(r => r.date_operation.slice(0, 10) >= from);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chartRows, chartRange]);
// rows : pour le tableau Mouvements (tous les filtres y compris mois)
const rows = chartRows.filter(r => {
const [y, m] = r.date_operation.split('-');
@@ -306,6 +323,7 @@ export default function DepotsRetraits() {
const plat = plats.find(p => p.id === r.plateforme_id);
const base = {
_rembId: r.id,
_invId: r.investissement_id ?? null,
plateforme_id: r.plateforme_id,
plateforme_nom: r.plateforme_nom,
plateforme_detenteur_nom: r.plateforme_detenteur_nom,
@@ -435,6 +453,16 @@ export default function DepotsRetraits() {
return true;
}), [normalizedSouscriptionMouvements, filter.plateforme_ids, filter.year, filter.month]);
// Solde indicatif du porte-monnaie après chaque mouvement — calculé sur l'historique
// COMPLET et NON FILTRÉ (allRows/normalizedCorrections/normalizedRembMouvements/
// normalizedSouscriptionMouvements, pas rows/filteredCorrections/...), sinon le cumul
// démarrerait arbitrairement au milieu de l'historique quand un filtre année/mois/type
// est actif. Purement indicatif (affichage/export) — ne remplace pas kpiSoldePortefeuille.
// Voir [[project_mouvements_solde_indicatif]].
const soldeIndicatifByRowId = useMemo(() => computeSoldeIndicatifMap([
...allRows, ...normalizedCorrections, ...normalizedRembMouvements, ...normalizedSouscriptionMouvements,
]), [allRows, normalizedCorrections, normalizedRembMouvements, normalizedSouscriptionMouvements]);
// tableRows : dépôts/retraits + corrections + remboursements/bonus + souscriptions, triés par date desc.
// _projet : nom du projet quand cela a du sens (remboursement, souscription/réinvestissement,
// et retrait AUTO lié à un remboursement via remboursement_id) — vide pour dépôt/retrait
@@ -452,10 +480,9 @@ export default function DepotsRetraits() {
const subPart = filteredSouscriptionMouvements
.filter(r => !filter.type || r.type === filter.type)
.map(r => ({ ...r, _projet: r.libelle || '' }));
return [...drPart, ...corrPart, ...rembPart, ...subPart].sort((a, b) =>
b.date_operation.localeCompare(a.date_operation) || String(b.id).localeCompare(String(a.id))
);
}, [rows, filteredCorrections, filteredRembMouvements, filteredSouscriptionMouvements, filter.type, rembProjetById]);
return sortMouvements([...drPart, ...corrPart, ...rembPart, ...subPart])
.map(r => ({ ...r, _soldeIndicatif: soldeIndicatifByRowId.get(r.id) }));
}, [rows, filteredCorrections, filteredRembMouvements, filteredSouscriptionMouvements, filter.type, rembProjetById, soldeIndicatifByRowId]);
/* ── Pagination mouvements ── */
const {
@@ -880,8 +907,8 @@ export default function DepotsRetraits() {
{/* ── Graphiques ── */}
{!listFocused && <div className="charts-row">
<SoldeChart rows={chartRows} />
<DistributionChart rows={chartRows} />
<SoldeChart rows={chartRows} range={chartRange} onRangeChange={setChartRange} />
<DistributionChart rows={distributionRows} />
</div>}
{/* ── KPIs ── */}
@@ -894,9 +921,9 @@ export default function DepotsRetraits() {
<div className="label">Dépôts</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.depots)}</span>
<TrendBadge current={totals.depots} prev={prevTotals.depots} />
{drPlatYear && <TrendBadge current={totals.depots} prev={prevTotals.depots} />}
</div>
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.depots)} en {prevYear}</div>
{drPlatYear && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.depots)} en {prevYear}</div>}
</div>
<div
className={`kpi dr-kpi-clickable${filter.type === 'retrait' ? ' dr-kpi-active dr-kpi-active-danger' : ''}`}
@@ -906,9 +933,9 @@ export default function DepotsRetraits() {
<div className="label">Retraits</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.retraits)}</span>
<TrendBadge current={totals.retraits} prev={prevTotals.retraits} invert={true} />
{drPlatYear && <TrendBadge current={totals.retraits} prev={prevTotals.retraits} invert={true} />}
</div>
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.retraits)} en {prevYear}</div>
{drPlatYear && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.retraits)} en {prevYear}</div>}
</div>
<div
className={`kpi${(filter.type || filter.plateforme_ids.length > 0 || filter.month) ? ' dr-kpi-clickable dr-kpi-active dr-kpi-active-neutral' : ''}`}
@@ -919,9 +946,9 @@ export default function DepotsRetraits() {
<div className="label">Diff. Dépôts vs Retraits</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.depots - totals.retraits)}</span>
<TrendBadge current={totals.depots - totals.retraits} prev={prevTotals.depots - prevTotals.retraits} />
{drPlatYear && <TrendBadge current={totals.depots - totals.retraits} prev={prevTotals.depots - prevTotals.retraits} />}
</div>
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.depots - prevTotals.retraits)} en {prevYear}</div>
{drPlatYear && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.depots - prevTotals.retraits)} en {prevYear}</div>}
{objectifsActifs && kpiEcartObjectif != null && (
<div style={{
fontSize: '0.8em', marginTop: 4, fontWeight: 600,
@@ -939,9 +966,9 @@ export default function DepotsRetraits() {
<div className="label">Porte-monnaie</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{kpiSoldePortefeuille !== null ? fmtEUR(kpiSoldePortefeuille) : '…'}</span>
{kpiSoldePortefeuille !== null && <TrendBadge current={kpiSoldePortefeuille} prev={prevKpiSoldePortefeuille} />}
{drPlatYear && kpiSoldePortefeuille !== null && <TrendBadge current={kpiSoldePortefeuille} prev={prevKpiSoldePortefeuille} />}
</div>
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevKpiSoldePortefeuille)} en {prevYear}</div>
{drPlatYear && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevKpiSoldePortefeuille)} en {prevYear}</div>}
</div>
</div>}
@@ -1250,12 +1277,12 @@ export default function DepotsRetraits() {
<table>
<thead>
<tr>
<th>Date</th><th>Plateforme</th>{multiDetenteur && <th>Détenteur</th>}<th>Type de mouvement</th><th>Projet</th><th className="num">Montant</th><th style={{ width: 36 }}></th>
<th>Date</th><th>Plateforme</th>{multiDetenteur && <th>Détenteur</th>}<th>Type de mouvement</th><th>Projet</th><th className="num">Montant</th><th className="num" title="Solde du porte-monnaie de la plateforme après ce mouvement — calculé à titre indicatif, pas une donnée officielle">Solde indicatif</th><th style={{ width: 36 }}></th>
</tr>
</thead>
<tbody>
{tableRows.length === 0 && (
<tr><td colSpan={multiDetenteur ? 7 : 6} className="text-muted" style={{ textAlign: 'center', padding: 24 }}>
<tr><td colSpan={multiDetenteur ? 8 : 7} className="text-muted" style={{ textAlign: 'center', padding: 24 }}>
{loading ? 'Chargement…' : 'Aucun mouvement'}
</td></tr>
)}
@@ -1283,6 +1310,7 @@ export default function DepotsRetraits() {
<td className={`num ${isPos ? 'success' : 'danger'}`}>
{isPos ? '+' : '−'} {fmtEUR(Math.abs(r.montant))}
</td>
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
<td style={{ width: 36, textAlign: 'center' }} onClick={e => e.stopPropagation()}>
<button
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px 6px', borderRadius: 4, color: 'var(--text-muted)', lineHeight: 1, fontSize: 16 }}
@@ -1320,6 +1348,7 @@ export default function DepotsRetraits() {
<td className="num success">
+ {fmtEUR(r.montant)}
</td>
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
<td style={{ width: 36, textAlign: 'center' }} />
</tr>
);
@@ -1349,6 +1378,7 @@ export default function DepotsRetraits() {
<td className="num danger">
− {fmtEUR(r.montant)}
</td>
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
<td style={{ width: 36, textAlign: 'center' }} />
</tr>
);
@@ -1385,6 +1415,7 @@ export default function DepotsRetraits() {
<td className="num danger">
− {fmtEUR(r.montant)}
</td>
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
<td style={{ width: 36, textAlign: 'center' }} />
</tr>
);
@@ -1416,6 +1447,7 @@ export default function DepotsRetraits() {
style={ isAuto ? { fontStyle: 'italic' } : undefined }>
{r.type === 'depot' ? '+' : '−'} {fmtEUR(r.montant)}
</td>
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
<td style={{ width: 36, textAlign: 'center' }} onClick={e => e.stopPropagation()}>
{!isAuto && (
<button
+10 -10
View File
@@ -825,9 +825,9 @@ export default function Investissements() {
<div className="label">Capital investi</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.encours)}</span>
{prevTotals && <TrendBadge current={totals.encours} prev={prevTotals.encours} />}
{platYear && <TrendBadge current={totals.encours} prev={prevTotals.encours} />}
</div>
{prevTotals && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.encours)} en {platYear ? Number(platYear) - 1 : new Date().getFullYear() - 1}</div>}
{platYear && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.encours)} en {Number(platYear) - 1}</div>}
</div>
{/* 2 — Investissements à risque */}
@@ -844,9 +844,9 @@ export default function Investissements() {
<div className="label">Investissements à risque</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }} className={totals.defaut > 0 ? 'danger' : ''}>{fmtEUR(totals.defaut)}</span>
{prevTotals && totals.defaut > 0 && <TrendBadge current={totals.defaut} prev={prevTotals.defaut} invert={true} />}
{platYear && totals.defaut > 0 && <TrendBadge current={totals.defaut} prev={prevTotals.defaut} invert={true} />}
</div>
{prevTotals && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.defaut)} en {platYear ? Number(platYear) - 1 : new Date().getFullYear() - 1}</div>}
{platYear && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.defaut)} en {Number(platYear) - 1}</div>}
</div>
{/* 3 — Investissements depuis le début */}
@@ -859,9 +859,9 @@ export default function Investissements() {
<div className="label">Investissements depuis le début</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.investi)}</span>
{prevTotals && <TrendBadge current={totals.investi} prev={prevTotals.investi} />}
{platYear && <TrendBadge current={totals.investi} prev={prevTotals.investi} />}
</div>
{prevTotals && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.investi)} en {platYear ? Number(platYear) - 1 : new Date().getFullYear() - 1}</div>}
{platYear && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.investi)} en {Number(platYear) - 1}</div>}
</div>
{/* 4 — Investissements remboursés */}
@@ -869,9 +869,9 @@ export default function Investissements() {
<div className="label">Investissements remboursés</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.cap_remb)}</span>
{prevTotals && <TrendBadge current={totals.cap_remb} prev={prevTotals.cap_remb} />}
{platYear && <TrendBadge current={totals.cap_remb} prev={prevTotals.cap_remb} />}
</div>
{prevTotals && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.cap_remb)} en {platYear ? Number(platYear) - 1 : new Date().getFullYear() - 1}</div>}
{platYear && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.cap_remb)} en {Number(platYear) - 1}</div>}
</div>
{/* 5 — Intérêts perçus */}
@@ -879,9 +879,9 @@ export default function Investissements() {
<div className="label">Intérêts perçus — {netMode ? 'Net' : 'Brut'}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(netMode ? totals.int_perc_net : totals.int_perc)}</span>
{prevTotals && <TrendBadge current={netMode ? totals.int_perc_net : totals.int_perc} prev={netMode ? prevTotals.int_perc_net : prevTotals.int_perc} />}
{platYear && <TrendBadge current={netMode ? totals.int_perc_net : totals.int_perc} prev={netMode ? prevTotals.int_perc_net : prevTotals.int_perc} />}
</div>
{prevTotals && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(netMode ? prevTotals.int_perc_net : prevTotals.int_perc)} en {platYear ? Number(platYear) - 1 : new Date().getFullYear() - 1}</div>}
{platYear && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(netMode ? prevTotals.int_perc_net : prevTotals.int_perc)} en {Number(platYear) - 1}</div>}
</div>
</div>}
+65 -36
View File
@@ -20,7 +20,7 @@ import ConfirmModal from '../components/ConfirmModal.jsx';
import {
mouvToCSV, mouvToXLS, mouvToJSON,
ExportDropdown, AddDropdown, CorrectionPickerModal, DetailPanel, CorrectionModal, MOIS_FR, emptyMouvementForm,
timestampSuffix, dlBlob,
timestampSuffix, dlBlob, sortMouvements, computeSoldeIndicatifMap,
} from '../components/MouvementsShared.jsx';
const LOGOS_BASE = '/api/logos/';
@@ -1039,6 +1039,7 @@ export default function Plateformes() {
const plat = plats.find(p => p.id === r.plateforme_id);
const base = {
_rembId: r.id,
_invId: r.investissement_id ?? null,
plateforme_id: r.plateforme_id,
plateforme_nom: r.plateforme_nom,
plateforme_detenteur_nom: r.plateforme_detenteur_nom,
@@ -1177,6 +1178,16 @@ export default function Plateformes() {
return [...set].sort((a, b) => a.localeCompare(b, 'fr'));
}, [mvNormalizedRembMouvements, mvNormalizedSouscriptionMouvements, allDepots, mvRembProjetById, mvFilter.plateforme_ids]);
// Solde indicatif du porte-monnaie après chaque mouvement — calculé sur l'historique
// COMPLET et NON FILTRÉ (allDepots/mvNormalizedCorrections/mvNormalizedRembMouvements/
// mvNormalizedSouscriptionMouvements), pas sur les variantes déjà filtrées année/mois/
// type/projet — sinon le cumul démarrerait arbitrairement au milieu de l'historique.
// Purement indicatif (affichage/export) — ne remplace pas computeSoldePortefeuille.
// Voir [[project_mouvements_solde_indicatif]].
const mvSoldeIndicatifByRowId = useMemo(() => computeSoldeIndicatifMap([
...allDepots, ...mvNormalizedCorrections, ...mvNormalizedRembMouvements, ...mvNormalizedSouscriptionMouvements,
]), [allDepots, mvNormalizedCorrections, mvNormalizedRembMouvements, mvNormalizedSouscriptionMouvements]);
const mvTableRows = useMemo(() => {
const showDR = !mvFilter.type || mvFilter.type === 'depot' || mvFilter.type === 'retrait';
const showCorr = !mvFilter.type || mvFilter.type === 'correction';
@@ -1190,12 +1201,11 @@ export default function Plateformes() {
const subPart = mvFilteredSouscriptionMouvements
.filter(r => !mvFilter.type || r.type === mvFilter.type)
.map(r => ({ ...r, _projet: r.libelle || '' }));
return [...drPart, ...corrPart, ...rembPart, ...subPart]
.filter(r => !mvFilter.projet || r._projet === mvFilter.projet)
.sort((a, b) =>
b.date_operation.localeCompare(a.date_operation) || String(b.id).localeCompare(String(a.id))
);
}, [mvRows, mvFilteredCorrections, mvFilteredRembMouvements, mvFilteredSouscriptionMouvements, mvFilter.type, mvFilter.projet, mvRembProjetById]);
return sortMouvements(
[...drPart, ...corrPart, ...rembPart, ...subPart]
.filter(r => !mvFilter.projet || r._projet === mvFilter.projet)
).map(r => ({ ...r, _soldeIndicatif: mvSoldeIndicatifByRowId.get(r.id) }));
}, [mvRows, mvFilteredCorrections, mvFilteredRembMouvements, mvFilteredSouscriptionMouvements, mvFilter.type, mvFilter.projet, mvRembProjetById, mvSoldeIndicatifByRowId]);
const {
pagedItems: mvPagedTableRows, page: mvPage, setPage: setMvPage,
@@ -1445,11 +1455,13 @@ export default function Plateformes() {
<div className="label">Capital investi</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.encours)}</span>
<TrendBadge current={totals.encours} prev={prevTotals.encours} />
</div>
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
{fmtEUR(prevTotals.encours)} en {prevYearLabel}
{selectedYear && <TrendBadge current={totals.encours} prev={prevTotals.encours} />}
</div>
{selectedYear && (
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
{fmtEUR(prevTotals.encours)} en {prevYearLabel}
</div>
)}
{objectifsActifs && kpiEcartCapitalObjectif != null && (
<div style={{
fontSize: '0.8em', marginTop: 4, fontWeight: 600,
@@ -1471,11 +1483,13 @@ export default function Plateformes() {
<span style={{ fontSize: '1.35rem', fontWeight: 700 }} className={totals.defaut > 0 ? 'danger' : ''}>
{fmtEUR(totals.defaut)}
</span>
{totals.defaut > 0 && <TrendBadge current={totals.defaut} prev={prevTotals.defaut} invert={true} />}
</div>
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
{fmtEUR(prevTotals.defaut)} en {prevYearLabel}
{selectedYear && totals.defaut > 0 && <TrendBadge current={totals.defaut} prev={prevTotals.defaut} invert={true} />}
</div>
{selectedYear && (
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
{fmtEUR(prevTotals.defaut)} en {prevYearLabel}
</div>
)}
</div>
{/* 3 — Porte-monnaie */}
@@ -1483,11 +1497,13 @@ export default function Plateformes() {
<div className="label">Porte-monnaie</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(soldePortefeuille)}</span>
<TrendBadge current={soldePortefeuille} prev={prevSoldePortefeuille} />
</div>
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
{fmtEUR(prevSoldePortefeuille)} en {prevYearLabel}
{selectedYear && <TrendBadge current={soldePortefeuille} prev={prevSoldePortefeuille} />}
</div>
{selectedYear && (
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
{fmtEUR(prevSoldePortefeuille)} en {prevYearLabel}
</div>
)}
</div>
{/* 4 — Investissements depuis le début */}
@@ -1495,11 +1511,13 @@ export default function Plateformes() {
<div className="label">Investissements depuis le début</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.investi)}</span>
<TrendBadge current={totals.investi} prev={prevTotals.investi} />
</div>
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
{fmtEUR(prevTotals.investi)} en {prevYearLabel}
{selectedYear && <TrendBadge current={totals.investi} prev={prevTotals.investi} />}
</div>
{selectedYear && (
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
{fmtEUR(prevTotals.investi)} en {prevYearLabel}
</div>
)}
</div>
{/* 5 — Capital remboursé */}
@@ -1507,11 +1525,13 @@ export default function Plateformes() {
<div className="label">Capital remboursé</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.cap_remb)}</span>
<TrendBadge current={totals.cap_remb} prev={prevTotals.cap_remb} />
</div>
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
{fmtEUR(prevTotals.cap_remb)} en {prevYearLabel}
{selectedYear && <TrendBadge current={totals.cap_remb} prev={prevTotals.cap_remb} />}
</div>
{selectedYear && (
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
{fmtEUR(prevTotals.cap_remb)} en {prevYearLabel}
</div>
)}
</div>
{/* 6 — Intérêts perçus */}
@@ -1521,14 +1541,18 @@ export default function Plateformes() {
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>
{fmtEUR(netMode ? totals.int_perc_net : totals.int_perc)}
</span>
<TrendBadge
current={netMode ? totals.int_perc_net : totals.int_perc}
prev={netMode ? prevTotals.int_perc_net : prevTotals.int_perc}
/>
</div>
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
{fmtEUR(netMode ? prevTotals.int_perc_net : prevTotals.int_perc)} en {prevYearLabel}
{selectedYear && (
<TrendBadge
current={netMode ? totals.int_perc_net : totals.int_perc}
prev={netMode ? prevTotals.int_perc_net : prevTotals.int_perc}
/>
)}
</div>
{selectedYear && (
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
{fmtEUR(netMode ? prevTotals.int_perc_net : prevTotals.int_perc)} en {prevYearLabel}
</div>
)}
</div>
{/* 7 — XIRR */}
@@ -2566,12 +2590,12 @@ export default function Plateformes() {
<table>
<thead>
<tr>
<th>Date</th><th>Plateforme</th>{multiDetenteur && <th>Détenteur</th>}<th>Type de mouvement</th><th>Projet</th><th className="num">Montant</th><th style={{ width: 36 }}></th>
<th>Date</th><th>Plateforme</th>{multiDetenteur && <th>Détenteur</th>}<th>Type de mouvement</th><th>Projet</th><th className="num">Montant</th><th className="num" title="Solde du porte-monnaie de la plateforme après ce mouvement — calculé à titre indicatif, pas une donnée officielle">Solde indicatif</th><th style={{ width: 36 }}></th>
</tr>
</thead>
<tbody>
{mvTableRows.length === 0 && (
<tr><td colSpan={multiDetenteur ? 7 : 6} className="text-muted" style={{ textAlign: 'center', padding: 24 }}>
<tr><td colSpan={multiDetenteur ? 8 : 7} className="text-muted" style={{ textAlign: 'center', padding: 24 }}>
{loading ? 'Chargement…' : 'Aucun mouvement'}
</td></tr>
)}
@@ -2599,6 +2623,7 @@ export default function Plateformes() {
<td className={`num ${isPos ? 'success' : 'danger'}`}>
{isPos ? '+' : '−'} {fmtEUR(Math.abs(r.montant))}
</td>
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
<td style={{ width: 36, textAlign: 'center' }} onClick={e => e.stopPropagation()}>
<button
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px 6px', borderRadius: 4, color: 'var(--text-muted)', lineHeight: 1, fontSize: 16 }}
@@ -2636,6 +2661,7 @@ export default function Plateformes() {
<td className="num success">
+ {fmtEUR(r.montant)}
</td>
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
<td style={{ width: 36, textAlign: 'center' }} />
</tr>
);
@@ -2665,6 +2691,7 @@ export default function Plateformes() {
<td className="num danger">
− {fmtEUR(r.montant)}
</td>
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
<td style={{ width: 36, textAlign: 'center' }} />
</tr>
);
@@ -2701,6 +2728,7 @@ export default function Plateformes() {
<td className="num danger">
− {fmtEUR(r.montant)}
</td>
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
<td style={{ width: 36, textAlign: 'center' }} />
</tr>
);
@@ -2732,6 +2760,7 @@ export default function Plateformes() {
style={ isAuto ? { fontStyle: 'italic' } : undefined }>
{r.type === 'depot' ? '+' : '−'} {fmtEUR(r.montant)}
</td>
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
<td style={{ width: 36, textAlign: 'center' }} onClick={e => e.stopPropagation()}>
{!isAuto && (
<button
+7 -7
View File
@@ -236,7 +236,7 @@ export default function Remboursements() {
const [tableFilter, setTableFilter] = useState({ plateforme_id: '', investissement_id: '', year: '', month: '', type: '' });
/* Filtre année du tableau Plateformes */
const [rembPlatYear, setRembPlatYear] = useState(String(new Date().getFullYear()));
const [rembPlatYear, setRembPlatYear] = useState(''); // 'Toutes les années' par défaut
/* Modal */
const [modalOpen, setModalOpen] = useState(false);
@@ -847,25 +847,25 @@ export default function Remboursements() {
<div className="label">Capital remboursé</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.capital)}</span>
<TrendBadge current={totals.capital} prev={prevTotals.capital} />
{rembPlatYear && <TrendBadge current={totals.capital} prev={prevTotals.capital} />}
</div>
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.capital)} en {rembPrevYear}</div>
{rembPlatYear && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.capital)} en {rembPrevYear}</div>}
</div>
<div className="kpi">
<div className="label">Cashback</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.cashback)}</span>
<TrendBadge current={totals.cashback} prev={prevTotals.cashback} />
{rembPlatYear && <TrendBadge current={totals.cashback} prev={prevTotals.cashback} />}
</div>
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.cashback)} en {rembPrevYear}</div>
{rembPlatYear && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.cashback)} en {rembPrevYear}</div>}
</div>
<div className="kpi">
<div className="label">Intérêts — {netMode ? 'Net' : 'Brut'}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(netMode ? totals.interets_nets : totals.interets)}</span>
<TrendBadge current={netMode ? totals.interets_nets : totals.interets} prev={netMode ? prevTotals.interets_nets : prevTotals.interets} />
{rembPlatYear && <TrendBadge current={netMode ? totals.interets_nets : totals.interets} prev={netMode ? prevTotals.interets_nets : prevTotals.interets} />}
</div>
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(netMode ? prevTotals.interets_nets : prevTotals.interets)} en {rembPrevYear}</div>
{rembPlatYear && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(netMode ? prevTotals.interets_nets : prevTotals.interets)} en {rembPrevYear}</div>}
</div>
</div>}