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