Amélioration du traitement des mouvements
This commit is contained in:
1 parent
9540221577
commit
6c96e023a9
8 files changed
+325
-59
No files matched your search
@@ -71,14 +71,25 @@ export default function DistributionChart({ rows }) {
|
||||
const svgRef = useRef(null);
|
||||
const wrapRef = useRef(null);
|
||||
|
||||
/* Solde net par plateforme */
|
||||
/* Solde net par plateforme.
|
||||
Deux usages du composant :
|
||||
- lignes avec `type` ('depot'/'retrait', page Dépôts/Retraits) : si le
|
||||
jeu de lignes mélange dépôts ET retraits (aucun filtre de type actif),
|
||||
on calcule le NET (dépôts − retraits) par plateforme, pas la somme
|
||||
brute — sinon un retrait s'additionnait au lieu de se soustraire.
|
||||
- lignes sans `type` (ex. capital restant dû sur Investissements.jsx) :
|
||||
comportement inchangé, simple somme de `montant`. */
|
||||
const { data, allRetraits } = useMemo(() => {
|
||||
if (!rows?.length) return { data: [], allRetraits: false };
|
||||
const allRetraits = rows.every(r => r.type === 'retrait');
|
||||
const hasTypes = rows.some(r => r.type === 'depot' || r.type === 'retrait');
|
||||
const allDepots = hasTypes && rows.every(r => r.type === 'depot');
|
||||
const allRetraits = hasTypes && rows.every(r => r.type === 'retrait');
|
||||
const mixedTypes = hasTypes && !allDepots && !allRetraits;
|
||||
const byPlat = {};
|
||||
for (const r of rows) {
|
||||
const key = r.plateforme_nom || `#${r.plateforme_id}`;
|
||||
byPlat[key] = (byPlat[key] || 0) + r.montant;
|
||||
const delta = mixedTypes ? (r.type === 'depot' ? r.montant : -r.montant) : r.montant;
|
||||
byPlat[key] = (byPlat[key] || 0) + delta;
|
||||
}
|
||||
const data = Object.entries(byPlat)
|
||||
.filter(([, v]) => v > 0)
|
||||
|
||||
@@ -41,13 +41,179 @@ export const MOUV_TYPE_LABELS = {
|
||||
interets_plateforme: 'Intérêts plateforme',
|
||||
};
|
||||
|
||||
/* ── Tri des mouvements pour une même date ───────────────────────────
|
||||
1) Toutes les lignes d'une même plateforme, pour une même date, sont regroupées
|
||||
ensemble (ordre alphabétique des plateformes entre elles — décidé avec Olivier
|
||||
le 2026-08-21, cf. incident du 20/08 où Indemo et La première brique
|
||||
s'entrelaçaient).
|
||||
2) À l'intérieur d'un bloc plateforme+date, TOUT est ordonné du plus récent au plus
|
||||
ancien, à tous les niveaux — familles, chaînes distinctes, ET position au sein
|
||||
d'une même chaîne (cohérent avec le tri global le plus récent d'abord) :
|
||||
1. Correction de solde
|
||||
2. Retrait (manuel)
|
||||
3. Souscription / Réinvestissement — argent qui sort pour investir
|
||||
└─ Cashback "prime à la souscription" du même jour sur le même
|
||||
investissement (affiché AVANT sa souscription : chronologiquement
|
||||
postérieur), si ce remboursement n'a par ailleurs aucune ligne
|
||||
crédit propre (cf. ci-dessous)
|
||||
4. Dépôt (manuel) — alimente le compte
|
||||
5. Remboursement / Intérêts plateforme / Bonus — argent qui entre
|
||||
└─ Retrait AUTO (sweep compte_courant) qui reverse ce remboursement
|
||||
└─ Prélèvement CSG
|
||||
└─ Prélèvement IR
|
||||
└─ Prélèvement impôts à l'étranger
|
||||
└─ Cashback (si rattaché à ce remboursement)
|
||||
└─ (le remboursement lui-même, ligne la plus ancienne de sa chaîne,
|
||||
apparaît donc en dernier — ses prélèvements/son retrait AUTO sont
|
||||
chronologiquement postérieurs)
|
||||
Plusieurs chaînes de même famille le même jour (ex. deux souscriptions) sont
|
||||
ordonnées par id décroissant (la plus récemment créée en premier). L'ordre
|
||||
interne d'une chaîne (constante MOUV_REMB_CHAIN_POS) reste défini "parent=0,
|
||||
enfants=1,2,3…" mais est PARCOURU EN DÉCROISSANT par le comparateur — inverser
|
||||
MOUV_REMB_CHAIN_POS lui-même serait une erreur, seul le sens du tri doit changer.
|
||||
Correction du 2026-08-21 (2e retour d'Olivier) : la 1ère version de cette règle
|
||||
laissait le sous-ordre d'une chaîne "parent puis enfants" non inversé, ce qui
|
||||
affichait "remboursement, puis ses prélèvements" — incohérent avec le reste de
|
||||
la journée en "plus récent d'abord".
|
||||
3) DÉPÔT ENTRE SOUSCRIPTION ET REMBOURSEMENT (et non plus toujours le plus ancien) :
|
||||
cas typique signalé par Olivier le 2026-08-21 sur BienPrêter/17-08 — un
|
||||
remboursement (Tradimex) déclenche un dépôt pour "relancer des investissements",
|
||||
qui finance ensuite 2 nouvelles souscriptions le même jour. Chronologiquement :
|
||||
remboursement → dépôt → souscriptions. Il n'existe AUCUN lien en base entre un
|
||||
dépôt manuel et le remboursement/les souscriptions qu'il finance (contrairement
|
||||
au retrait AUTO ou au cashback de souscription, qui ont un `remboursement_id`/
|
||||
`investissement_id` explicite) — impossible donc de rattacher précisément CE
|
||||
dépôt à CE remboursement. Le compromis retenu est une RÈGLE GÉNÉRALE (pas un
|
||||
chaînage par ligne) : un dépôt manuel est toujours supposé plus récent qu'un
|
||||
remboursement de la même plateforme/journée (donc affiché au-dessus de sa
|
||||
chaîne), mais plus ancien qu'une souscription de la même plateforme/journée
|
||||
(donc affiché en dessous). C'est une approximation assumée : si un dépôt sans
|
||||
lien avec un remboursement du jour se trouve être réellement antérieur à ce
|
||||
remboursement, l'affichage sera dans le mauvais sens — aucune donnée ne permet
|
||||
de trancher. Règle validée avec Olivier (retour direct sur capture, pas
|
||||
d'AskUserQuestion cette fois — cas jugé suffisamment clair). ── */
|
||||
const MOUV_FAMILY_RANK = {
|
||||
remboursement: 1, interets_plateforme: 1, bonus: 1, cashback: 1,
|
||||
prelevement_local: 1, prelevement_ir: 1, prelevement_csg: 1,
|
||||
depot: 2,
|
||||
souscription: 3,
|
||||
retrait: 4,
|
||||
correction: 5,
|
||||
};
|
||||
|
||||
// Position au sein d'une chaîne "remboursement" (lignes partageant le même _rembId)
|
||||
const MOUV_REMB_CHAIN_POS = {
|
||||
remboursement: 0, interets_plateforme: 0, bonus: 0,
|
||||
cashback: 1, prelevement_local: 2, prelevement_ir: 3, prelevement_csg: 4,
|
||||
};
|
||||
|
||||
export function sortMouvements(list) {
|
||||
// _rembId des remboursements qui ont une vraie ligne crédit (remboursement / intérêts
|
||||
// plateforme) — sert à NE PAS rattacher leur cashback à une souscription du même jour :
|
||||
// un cashback qui accompagne un vrai remboursement reste dans la chaîne de ce remboursement.
|
||||
const rembHasCreditLine = new Set(
|
||||
list.filter(r => r.type === 'remboursement' || r.type === 'interets_plateforme').map(r => r._rembId)
|
||||
);
|
||||
// Souscriptions/réinvestissements par (investissement, date) — pour rattacher un cashback
|
||||
// "prime à la souscription" (remboursement sans ligne crédit propre, même investissement,
|
||||
// même date) à la souscription qu'il accompagne.
|
||||
const subByInvAndDate = new Map(
|
||||
list.filter(r => r.type === 'souscription').map(r => [`${r._invId}|${r.date_operation}`, r])
|
||||
);
|
||||
|
||||
const enriched = list.map(row => {
|
||||
// Retrait AUTO (sweep compte_courant) : reste collé juste après le remboursement qui le déclenche.
|
||||
if (row.type === 'retrait' && row.source === 'auto_remboursement' && row.remboursement_id != null) {
|
||||
return { row, family: MOUV_FAMILY_RANK.remboursement, anchor: row.remboursement_id, pos: 5 };
|
||||
}
|
||||
// Cashback "prime à la souscription" : pas de ligne crédit propre + souscription du même
|
||||
// jour sur le même investissement → rattaché à cette souscription plutôt qu'isolé.
|
||||
if (row.type === 'cashback' && !rembHasCreditLine.has(row._rembId) && row._invId != null) {
|
||||
const sub = subByInvAndDate.get(`${row._invId}|${row.date_operation}`);
|
||||
if (sub) return { row, family: MOUV_FAMILY_RANK.souscription, anchor: sub._invId, pos: 1 };
|
||||
}
|
||||
// Lignes d'un remboursement (parent + cashback resté attaché + prélèvements)
|
||||
if (row._rembId != null) {
|
||||
return {
|
||||
row,
|
||||
family: MOUV_FAMILY_RANK[row.type] ?? MOUV_FAMILY_RANK.remboursement,
|
||||
anchor: row._rembId,
|
||||
pos: MOUV_REMB_CHAIN_POS[row.type] ?? 0,
|
||||
};
|
||||
}
|
||||
// Souscription / réinvestissement (parent de sa propre chaîne)
|
||||
if (row.type === 'souscription') {
|
||||
return { row, family: MOUV_FAMILY_RANK.souscription, anchor: row._invId, pos: 0 };
|
||||
}
|
||||
// Correction de solde
|
||||
if (row.type === 'correction') {
|
||||
return { row, family: MOUV_FAMILY_RANK.correction, anchor: row._correctionId, pos: 0 };
|
||||
}
|
||||
// Dépôt / retrait manuel (hors sweep AUTO déjà traité plus haut)
|
||||
return { row, family: MOUV_FAMILY_RANK[row.type] ?? MOUV_FAMILY_RANK.retrait, anchor: row.id, pos: 0 };
|
||||
});
|
||||
|
||||
return enriched
|
||||
.sort((a, b) =>
|
||||
b.row.date_operation.localeCompare(a.row.date_operation) || // date : plus récent d'abord
|
||||
(a.row.plateforme_nom || '').localeCompare(b.row.plateforme_nom || '', 'fr') || // plateforme : ordre alphabétique
|
||||
b.family - a.family || // famille : plus récent de la journée d'abord
|
||||
(Number(b.anchor) - Number(a.anchor)) || // chaînes distinctes : id décroissant (plus récente d'abord)
|
||||
b.pos - a.pos || // au sein d'une chaîne : le plus récent (enfant) d'abord
|
||||
String(b.row.id).localeCompare(String(a.row.id)) // filet de sécurité
|
||||
)
|
||||
.map(e => e.row);
|
||||
}
|
||||
|
||||
/* ── Solde indicatif du porte-monnaie, ligne par ligne ────────────────────────────
|
||||
Calcule, pour CHAQUE mouvement d'une plateforme, le solde du porte-monnaie de cette
|
||||
plateforme immédiatement après ce mouvement — à titre indicatif (aide à repérer des
|
||||
anomalies visuellement), PAS une source de vérité chiffrée : c'est une somme cumulée
|
||||
de signedMontant() dans l'ordre chronologique probable (cf. sortMouvements ci-dessus,
|
||||
lui-même basé sur des règles de départage à date égale qui restent des approximations
|
||||
en l'absence d'horodatage précis dans les données).
|
||||
IMPORTANT : `list` DOIT être la liste COMPLÈTE et NON FILTRÉE (aucun filtre
|
||||
année/mois/type) des mouvements du scope investisseur courant — sinon le cumul
|
||||
démarrerait arbitrairement au milieu de l'historique et le solde n'aurait aucun sens.
|
||||
Le filtre plateforme n'a en revanche pas d'importance : le calcul est de toute façon
|
||||
fait indépendamment pour chaque plateforme.
|
||||
Ne touche PAS aux formules officielles du solde porte-monnaie (kpiSoldePortefeuille /
|
||||
computeSoldePortefeuille / walletMap, cf. [[project_solde_portefeuille]] et
|
||||
[[project_mouvements_tab_wallet_events]]) : ce solde-ci est un indicateur d'affichage
|
||||
indépendant, recalculé à partir des mêmes lignes déjà utilisées pour l'affichage et
|
||||
l'export — jamais l'inverse. Retourne une Map id de ligne → solde après cette ligne.
|
||||
Décidé avec Olivier le 2026-08-21. ── */
|
||||
export function computeSoldeIndicatifMap(list) {
|
||||
const byPlateforme = new Map();
|
||||
for (const row of list) {
|
||||
const key = row.plateforme_id ?? row.plateforme_nom ?? '?';
|
||||
if (!byPlateforme.has(key)) byPlateforme.set(key, []);
|
||||
byPlateforme.get(key).push(row);
|
||||
}
|
||||
|
||||
const soldeById = new Map();
|
||||
for (const rows of byPlateforme.values()) {
|
||||
// sortMouvements() trie du plus récent au plus ancien ; on inverse pour cumuler
|
||||
// dans l'ordre chronologique probable (du plus ancien au plus récent).
|
||||
const chrono = sortMouvements(rows).slice().reverse();
|
||||
let running = 0;
|
||||
for (const row of chrono) {
|
||||
running += signedMontant(row);
|
||||
soldeById.set(row.id, running);
|
||||
}
|
||||
}
|
||||
return soldeById;
|
||||
}
|
||||
|
||||
export function mouvToCSV(rows) {
|
||||
const BOM = ''; const sep = ';';
|
||||
const q = v => `"${String(v ?? '').replace(/"/g, '""')}"`;
|
||||
const headers = ['Date','Plateforme','Type','Projet','Montant (€)','Libellé','Référence','Source'];
|
||||
const headers = ['Date','Plateforme','Type','Projet','Montant (€)','Solde porte-monnaie indicatif (€)','Libellé','Référence','Source'];
|
||||
const data = rows.map(r => [
|
||||
r.date_operation, r.plateforme_nom||'', MOUV_TYPE_LABELS[r.type] || r.type, r._projet||'',
|
||||
String(signedMontant(r)).replace('.',','), r.libelle||'', r.reference||'', r.source||'',
|
||||
String(signedMontant(r)).replace('.',','),
|
||||
r._soldeIndicatif != null ? String(r._soldeIndicatif).replace('.',',') : '',
|
||||
r.libelle||'', r.reference||'', r.source||'',
|
||||
]);
|
||||
return BOM + [headers,...data].map(r => r.map(q).join(sep)).join('\r\n');
|
||||
}
|
||||
@@ -60,6 +226,7 @@ export function mouvToXLS(rows, multiDetenteur) {
|
||||
'Type': MOUV_TYPE_LABELS[r.type] || r.type,
|
||||
'Projet': r._projet || '',
|
||||
'Montant (€)': signedMontant(r),
|
||||
'Solde porte-monnaie indicatif (€)': r._soldeIndicatif ?? '',
|
||||
}));
|
||||
const ws = XLSX.utils.json_to_sheet(data);
|
||||
const wb = XLSX.utils.book_new();
|
||||
@@ -74,6 +241,7 @@ export function mouvToJSON(rows) {
|
||||
type: r.type,
|
||||
projet: r._projet || null,
|
||||
montant: signedMontant(r),
|
||||
solde_indicatif: r._soldeIndicatif ?? null,
|
||||
libelle: r.libelle || null,
|
||||
reference: r.reference || null,
|
||||
source: r.source || null,
|
||||
|
||||
@@ -38,8 +38,13 @@ function fmtTodayFull() {
|
||||
}
|
||||
|
||||
/* ── Composant ──────────────────────────────────────────────── */
|
||||
export default function SoldeChart({ rows }) {
|
||||
const [range, setRange] = useState('TOUT');
|
||||
export default function SoldeChart({ rows, range: rangeProp, onRangeChange }) {
|
||||
// Contrôlé si le parent fournit `range`/`onRangeChange` (pour synchroniser
|
||||
// d'autres composants, ex. DistributionChart, sur la même plage) — sinon
|
||||
// repli sur un state interne (usage autonome).
|
||||
const [rangeState, setRangeState] = useState('TOUT');
|
||||
const range = rangeProp !== undefined ? rangeProp : rangeState;
|
||||
const setRange = (r) => { if (onRangeChange) onRangeChange(r); else setRangeState(r); };
|
||||
const [hover, setHover] = useState(null); // { x, y, value, date }
|
||||
const svgRef = useRef(null);
|
||||
const wrapRef = useRef(null);
|
||||
|
||||
@@ -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]
|
||||
return sortMouvements(
|
||||
[...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]);
|
||||
).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} />
|
||||
{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} />}
|
||||
{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} />
|
||||
{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} />
|
||||
{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} />
|
||||
{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>
|
||||
{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>}
|
||||
|
||||
|
||||
@@ -11,6 +11,27 @@ export const fmtEUR = (n) => {
|
||||
// "-0,00 €" pour un montant nul.
|
||||
export const fmtRetrait = (n) => (n > 0 ? `-${fmtEUR(n)}` : fmtEUR(n));
|
||||
|
||||
// Borne de date basse (YYYY-MM-DD) pour une plage de type sélecteur de graphique
|
||||
// ('1J'|'7J'|'1M'|'3M'|'YTD'|'1A'|'TOUT'). Retourne null pour 'TOUT' (pas de borne).
|
||||
// Logique identique à celle utilisée en interne par SoldeChart/InvChart, extraite
|
||||
// ici pour pouvoir filtrer d'autres composants (ex. DistributionChart) sur la même
|
||||
// plage que le graphique auquel ils sont associés.
|
||||
export const rangeFromDate = (range) => {
|
||||
if (!range || range === 'TOUT') return null;
|
||||
const now = new Date();
|
||||
let fromDate = new Date(now);
|
||||
switch (range) {
|
||||
case '1J': fromDate.setDate(now.getDate() - 1); break;
|
||||
case '7J': fromDate.setDate(now.getDate() - 7); break;
|
||||
case '1M': fromDate.setMonth(now.getMonth() - 1); break;
|
||||
case '3M': fromDate.setMonth(now.getMonth() - 3); break;
|
||||
case 'YTD': fromDate = new Date(now.getFullYear(), 0, 1); break;
|
||||
case '1A': fromDate.setFullYear(now.getFullYear() - 1); break;
|
||||
default: return null;
|
||||
}
|
||||
return fromDate.toISOString().slice(0, 10);
|
||||
};
|
||||
|
||||
export const fmtNum = (n, digits = 2) =>
|
||||
(n == null ? '' : Number(n).toLocaleString('fr-FR', { minimumFractionDigits: digits, maximumFractionDigits: digits }));
|
||||
|
||||
|
||||
Reference in new issue
Block a user