Amélioration du traitement des mouvements
This commit is contained in:
1 parent
9540221577
commit
6c96e023a9
8 files changed
+348
-82
No files matched your search
@@ -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
|
||||
|
||||
@@ -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>}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>}
|
||||
|
||||
|
||||
Reference in new issue
Block a user