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

+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