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
@@ -71,14 +71,25 @@ export default function DistributionChart({ rows }) {
|
|||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
const wrapRef = 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(() => {
|
const { data, allRetraits } = useMemo(() => {
|
||||||
if (!rows?.length) return { data: [], allRetraits: false };
|
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 = {};
|
const byPlat = {};
|
||||||
for (const r of rows) {
|
for (const r of rows) {
|
||||||
const key = r.plateforme_nom || `#${r.plateforme_id}`;
|
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)
|
const data = Object.entries(byPlat)
|
||||||
.filter(([, v]) => v > 0)
|
.filter(([, v]) => v > 0)
|
||||||
|
|||||||
@@ -41,13 +41,179 @@ export const MOUV_TYPE_LABELS = {
|
|||||||
interets_plateforme: 'Intérêts plateforme',
|
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) {
|
export function mouvToCSV(rows) {
|
||||||
const BOM = ''; const sep = ';';
|
const BOM = ''; const sep = ';';
|
||||||
const q = v => `"${String(v ?? '').replace(/"/g, '""')}"`;
|
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 => [
|
const data = rows.map(r => [
|
||||||
r.date_operation, r.plateforme_nom||'', MOUV_TYPE_LABELS[r.type] || r.type, r._projet||'',
|
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');
|
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,
|
'Type': MOUV_TYPE_LABELS[r.type] || r.type,
|
||||||
'Projet': r._projet || '',
|
'Projet': r._projet || '',
|
||||||
'Montant (€)': signedMontant(r),
|
'Montant (€)': signedMontant(r),
|
||||||
|
'Solde porte-monnaie indicatif (€)': r._soldeIndicatif ?? '',
|
||||||
}));
|
}));
|
||||||
const ws = XLSX.utils.json_to_sheet(data);
|
const ws = XLSX.utils.json_to_sheet(data);
|
||||||
const wb = XLSX.utils.book_new();
|
const wb = XLSX.utils.book_new();
|
||||||
@@ -74,6 +241,7 @@ export function mouvToJSON(rows) {
|
|||||||
type: r.type,
|
type: r.type,
|
||||||
projet: r._projet || null,
|
projet: r._projet || null,
|
||||||
montant: signedMontant(r),
|
montant: signedMontant(r),
|
||||||
|
solde_indicatif: r._soldeIndicatif ?? null,
|
||||||
libelle: r.libelle || null,
|
libelle: r.libelle || null,
|
||||||
reference: r.reference || null,
|
reference: r.reference || null,
|
||||||
source: r.source || null,
|
source: r.source || null,
|
||||||
|
|||||||
@@ -38,8 +38,13 @@ function fmtTodayFull() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Composant ──────────────────────────────────────────────── */
|
/* ── Composant ──────────────────────────────────────────────── */
|
||||||
export default function SoldeChart({ rows }) {
|
export default function SoldeChart({ rows, range: rangeProp, onRangeChange }) {
|
||||||
const [range, setRange] = useState('TOUT');
|
// 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 [hover, setHover] = useState(null); // { x, y, value, date }
|
||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
const wrapRef = useRef(null);
|
const wrapRef = useRef(null);
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ import Modal from '../components/Modal.jsx';
|
|||||||
import ConfirmModal from '../components/ConfirmModal.jsx';
|
import ConfirmModal from '../components/ConfirmModal.jsx';
|
||||||
import SoldeChart from '../components/SoldeChart.jsx';
|
import SoldeChart from '../components/SoldeChart.jsx';
|
||||||
import DistributionChart from '../components/DistributionChart.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 DepotsMensuelTable from '../components/DepotsMensuelTable.jsx';
|
||||||
import SuiviObjectifs from '../components/SuiviObjectifs.jsx';
|
import SuiviObjectifs from '../components/SuiviObjectifs.jsx';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import {
|
import {
|
||||||
DEBIT_TYPES, signedMontant, MOUV_TYPE_LABELS, mouvToCSV, mouvToXLS, mouvToJSON,
|
DEBIT_TYPES, signedMontant, MOUV_TYPE_LABELS, mouvToCSV, mouvToXLS, mouvToJSON,
|
||||||
ExportDropdown, AddDropdown, CorrectionPickerModal, DetailPanel, CorrectionModal, MOIS_FR, emptyMouvementForm,
|
ExportDropdown, AddDropdown, CorrectionPickerModal, DetailPanel, CorrectionModal, MOIS_FR, emptyMouvementForm,
|
||||||
timestampSuffix, dlBlob,
|
timestampSuffix, dlBlob, sortMouvements, computeSoldeIndicatifMap,
|
||||||
} from '../components/MouvementsShared.jsx';
|
} from '../components/MouvementsShared.jsx';
|
||||||
|
|
||||||
/* ── Indicateur de progression ── */
|
/* ── Indicateur de progression ── */
|
||||||
@@ -109,15 +109,18 @@ export default function DepotsRetraits() {
|
|||||||
// cf. mémoire project_taxreport_interets_plateforme.
|
// cf. mémoire project_taxreport_interets_plateforme.
|
||||||
const [dashDataAsOf, setDashDataAsOf] = useState({});
|
const [dashDataAsOf, setDashDataAsOf] = useState({});
|
||||||
// Année sélectionnée, partagée entre l'onglet Plateformes et le filtre "Année" de Mouvements
|
// Année sélectionnée, partagée entre l'onglet Plateformes et le filtre "Année" de Mouvements
|
||||||
const currentYear = String(new Date().getFullYear());
|
// — vide par défaut ("Toutes les années") au chargement initial de la page.
|
||||||
const [filter, setFilter] = useState({ type: '', plateforme_ids: [], year: currentYear, month: '' });
|
const [filter, setFilter] = useState({ type: '', plateforme_ids: [], year: '', month: '' });
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [editingId, setEditingId] = useState(null);
|
const [editingId, setEditingId] = useState(null);
|
||||||
const [form, setForm] = useState(emptyMouvementForm);
|
const [form, setForm] = useState(emptyMouvementForm);
|
||||||
const [err, setErr] = useState(null);
|
const [err, setErr] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [selectedRow, setSelectedRow] = useState(null);
|
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 [allRemb, setAllRemb] = useState([]);
|
||||||
const [allInv, setAllInv] = useState([]);
|
const [allInv, setAllInv] = useState([]);
|
||||||
const [allReinvests, setAllReinvests] = useState([]);
|
const [allReinvests, setAllReinvests] = useState([]);
|
||||||
@@ -236,12 +239,26 @@ export default function DepotsRetraits() {
|
|||||||
return true;
|
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 => {
|
const chartRows = kpiRows.filter(r => {
|
||||||
if (filter.type && r.type !== filter.type) return false;
|
if (filter.type && r.type !== filter.type) return false;
|
||||||
return true;
|
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)
|
// rows : pour le tableau Mouvements (tous les filtres y compris mois)
|
||||||
const rows = chartRows.filter(r => {
|
const rows = chartRows.filter(r => {
|
||||||
const [y, m] = r.date_operation.split('-');
|
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 plat = plats.find(p => p.id === r.plateforme_id);
|
||||||
const base = {
|
const base = {
|
||||||
_rembId: r.id,
|
_rembId: r.id,
|
||||||
|
_invId: r.investissement_id ?? null,
|
||||||
plateforme_id: r.plateforme_id,
|
plateforme_id: r.plateforme_id,
|
||||||
plateforme_nom: r.plateforme_nom,
|
plateforme_nom: r.plateforme_nom,
|
||||||
plateforme_detenteur_nom: r.plateforme_detenteur_nom,
|
plateforme_detenteur_nom: r.plateforme_detenteur_nom,
|
||||||
@@ -435,6 +453,16 @@ export default function DepotsRetraits() {
|
|||||||
return true;
|
return true;
|
||||||
}), [normalizedSouscriptionMouvements, filter.plateforme_ids, filter.year, filter.month]);
|
}), [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.
|
// 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,
|
// _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
|
// 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
|
const subPart = filteredSouscriptionMouvements
|
||||||
.filter(r => !filter.type || r.type === filter.type)
|
.filter(r => !filter.type || r.type === filter.type)
|
||||||
.map(r => ({ ...r, _projet: r.libelle || '' }));
|
.map(r => ({ ...r, _projet: r.libelle || '' }));
|
||||||
return [...drPart, ...corrPart, ...rembPart, ...subPart].sort((a, b) =>
|
return sortMouvements([...drPart, ...corrPart, ...rembPart, ...subPart])
|
||||||
b.date_operation.localeCompare(a.date_operation) || String(b.id).localeCompare(String(a.id))
|
.map(r => ({ ...r, _soldeIndicatif: soldeIndicatifByRowId.get(r.id) }));
|
||||||
);
|
}, [rows, filteredCorrections, filteredRembMouvements, filteredSouscriptionMouvements, filter.type, rembProjetById, soldeIndicatifByRowId]);
|
||||||
}, [rows, filteredCorrections, filteredRembMouvements, filteredSouscriptionMouvements, filter.type, rembProjetById]);
|
|
||||||
|
|
||||||
/* ── Pagination mouvements ── */
|
/* ── Pagination mouvements ── */
|
||||||
const {
|
const {
|
||||||
@@ -880,8 +907,8 @@ export default function DepotsRetraits() {
|
|||||||
|
|
||||||
{/* ── Graphiques ── */}
|
{/* ── Graphiques ── */}
|
||||||
{!listFocused && <div className="charts-row">
|
{!listFocused && <div className="charts-row">
|
||||||
<SoldeChart rows={chartRows} />
|
<SoldeChart rows={chartRows} range={chartRange} onRangeChange={setChartRange} />
|
||||||
<DistributionChart rows={chartRows} />
|
<DistributionChart rows={distributionRows} />
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
{/* ── KPIs ── */}
|
{/* ── KPIs ── */}
|
||||||
@@ -894,9 +921,9 @@ export default function DepotsRetraits() {
|
|||||||
<div className="label">Dépôts</div>
|
<div className="label">Dépôts</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
||||||
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.depots)}</span>
|
<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>
|
||||||
<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>
|
||||||
<div
|
<div
|
||||||
className={`kpi dr-kpi-clickable${filter.type === 'retrait' ? ' dr-kpi-active dr-kpi-active-danger' : ''}`}
|
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 className="label">Retraits</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
||||||
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.retraits)}</span>
|
<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>
|
||||||
<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>
|
||||||
<div
|
<div
|
||||||
className={`kpi${(filter.type || filter.plateforme_ids.length > 0 || filter.month) ? ' dr-kpi-clickable dr-kpi-active dr-kpi-active-neutral' : ''}`}
|
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 className="label">Diff. Dépôts vs Retraits</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<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>
|
<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>
|
||||||
<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 && (
|
{objectifsActifs && kpiEcartObjectif != null && (
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: '0.8em', marginTop: 4, fontWeight: 600,
|
fontSize: '0.8em', marginTop: 4, fontWeight: 600,
|
||||||
@@ -939,9 +966,9 @@ export default function DepotsRetraits() {
|
|||||||
<div className="label">Porte-monnaie</div>
|
<div className="label">Porte-monnaie</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
||||||
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{kpiSoldePortefeuille !== null ? fmtEUR(kpiSoldePortefeuille) : '…'}</span>
|
<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>
|
||||||
<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>
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
@@ -1250,12 +1277,12 @@ export default function DepotsRetraits() {
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{tableRows.length === 0 && (
|
{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'}
|
{loading ? 'Chargement…' : 'Aucun mouvement'}
|
||||||
</td></tr>
|
</td></tr>
|
||||||
)}
|
)}
|
||||||
@@ -1283,6 +1310,7 @@ export default function DepotsRetraits() {
|
|||||||
<td className={`num ${isPos ? 'success' : 'danger'}`}>
|
<td className={`num ${isPos ? 'success' : 'danger'}`}>
|
||||||
{isPos ? '+' : '−'} {fmtEUR(Math.abs(r.montant))}
|
{isPos ? '+' : '−'} {fmtEUR(Math.abs(r.montant))}
|
||||||
</td>
|
</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()}>
|
<td style={{ width: 36, textAlign: 'center' }} onClick={e => e.stopPropagation()}>
|
||||||
<button
|
<button
|
||||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px 6px', borderRadius: 4, color: 'var(--text-muted)', lineHeight: 1, fontSize: 16 }}
|
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">
|
<td className="num success">
|
||||||
+ {fmtEUR(r.montant)}
|
+ {fmtEUR(r.montant)}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
|
||||||
<td style={{ width: 36, textAlign: 'center' }} />
|
<td style={{ width: 36, textAlign: 'center' }} />
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -1349,6 +1378,7 @@ export default function DepotsRetraits() {
|
|||||||
<td className="num danger">
|
<td className="num danger">
|
||||||
− {fmtEUR(r.montant)}
|
− {fmtEUR(r.montant)}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
|
||||||
<td style={{ width: 36, textAlign: 'center' }} />
|
<td style={{ width: 36, textAlign: 'center' }} />
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -1385,6 +1415,7 @@ export default function DepotsRetraits() {
|
|||||||
<td className="num danger">
|
<td className="num danger">
|
||||||
− {fmtEUR(r.montant)}
|
− {fmtEUR(r.montant)}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
|
||||||
<td style={{ width: 36, textAlign: 'center' }} />
|
<td style={{ width: 36, textAlign: 'center' }} />
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -1416,6 +1447,7 @@ export default function DepotsRetraits() {
|
|||||||
style={ isAuto ? { fontStyle: 'italic' } : undefined }>
|
style={ isAuto ? { fontStyle: 'italic' } : undefined }>
|
||||||
{r.type === 'depot' ? '+' : '−'} {fmtEUR(r.montant)}
|
{r.type === 'depot' ? '+' : '−'} {fmtEUR(r.montant)}
|
||||||
</td>
|
</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()}>
|
<td style={{ width: 36, textAlign: 'center' }} onClick={e => e.stopPropagation()}>
|
||||||
{!isAuto && (
|
{!isAuto && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -825,9 +825,9 @@ export default function Investissements() {
|
|||||||
<div className="label">Capital investi</div>
|
<div className="label">Capital investi</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
||||||
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.encours)}</span>
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* 2 — Investissements à risque */}
|
{/* 2 — Investissements à risque */}
|
||||||
@@ -844,9 +844,9 @@ export default function Investissements() {
|
|||||||
<div className="label">Investissements à risque</div>
|
<div className="label">Investissements à risque</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* 3 — Investissements depuis le début */}
|
{/* 3 — Investissements depuis le début */}
|
||||||
@@ -859,9 +859,9 @@ export default function Investissements() {
|
|||||||
<div className="label">Investissements depuis le début</div>
|
<div className="label">Investissements depuis le début</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
||||||
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.investi)}</span>
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* 4 — Investissements remboursés */}
|
{/* 4 — Investissements remboursés */}
|
||||||
@@ -869,9 +869,9 @@ export default function Investissements() {
|
|||||||
<div className="label">Investissements remboursés</div>
|
<div className="label">Investissements remboursés</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
||||||
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.cap_remb)}</span>
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* 5 — Intérêts perçus */}
|
{/* 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 className="label">Intérêts perçus — {netMode ? 'Net' : 'Brut'}</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<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>
|
<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>
|
</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>
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import ConfirmModal from '../components/ConfirmModal.jsx';
|
|||||||
import {
|
import {
|
||||||
mouvToCSV, mouvToXLS, mouvToJSON,
|
mouvToCSV, mouvToXLS, mouvToJSON,
|
||||||
ExportDropdown, AddDropdown, CorrectionPickerModal, DetailPanel, CorrectionModal, MOIS_FR, emptyMouvementForm,
|
ExportDropdown, AddDropdown, CorrectionPickerModal, DetailPanel, CorrectionModal, MOIS_FR, emptyMouvementForm,
|
||||||
timestampSuffix, dlBlob,
|
timestampSuffix, dlBlob, sortMouvements, computeSoldeIndicatifMap,
|
||||||
} from '../components/MouvementsShared.jsx';
|
} from '../components/MouvementsShared.jsx';
|
||||||
|
|
||||||
const LOGOS_BASE = '/api/logos/';
|
const LOGOS_BASE = '/api/logos/';
|
||||||
@@ -1039,6 +1039,7 @@ export default function Plateformes() {
|
|||||||
const plat = plats.find(p => p.id === r.plateforme_id);
|
const plat = plats.find(p => p.id === r.plateforme_id);
|
||||||
const base = {
|
const base = {
|
||||||
_rembId: r.id,
|
_rembId: r.id,
|
||||||
|
_invId: r.investissement_id ?? null,
|
||||||
plateforme_id: r.plateforme_id,
|
plateforme_id: r.plateforme_id,
|
||||||
plateforme_nom: r.plateforme_nom,
|
plateforme_nom: r.plateforme_nom,
|
||||||
plateforme_detenteur_nom: r.plateforme_detenteur_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'));
|
return [...set].sort((a, b) => a.localeCompare(b, 'fr'));
|
||||||
}, [mvNormalizedRembMouvements, mvNormalizedSouscriptionMouvements, allDepots, mvRembProjetById, mvFilter.plateforme_ids]);
|
}, [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 mvTableRows = useMemo(() => {
|
||||||
const showDR = !mvFilter.type || mvFilter.type === 'depot' || mvFilter.type === 'retrait';
|
const showDR = !mvFilter.type || mvFilter.type === 'depot' || mvFilter.type === 'retrait';
|
||||||
const showCorr = !mvFilter.type || mvFilter.type === 'correction';
|
const showCorr = !mvFilter.type || mvFilter.type === 'correction';
|
||||||
@@ -1190,12 +1201,11 @@ export default function Plateformes() {
|
|||||||
const subPart = mvFilteredSouscriptionMouvements
|
const subPart = mvFilteredSouscriptionMouvements
|
||||||
.filter(r => !mvFilter.type || r.type === mvFilter.type)
|
.filter(r => !mvFilter.type || r.type === mvFilter.type)
|
||||||
.map(r => ({ ...r, _projet: r.libelle || '' }));
|
.map(r => ({ ...r, _projet: r.libelle || '' }));
|
||||||
return [...drPart, ...corrPart, ...rembPart, ...subPart]
|
return sortMouvements(
|
||||||
.filter(r => !mvFilter.projet || r._projet === mvFilter.projet)
|
[...drPart, ...corrPart, ...rembPart, ...subPart]
|
||||||
.sort((a, b) =>
|
.filter(r => !mvFilter.projet || r._projet === mvFilter.projet)
|
||||||
b.date_operation.localeCompare(a.date_operation) || String(b.id).localeCompare(String(a.id))
|
).map(r => ({ ...r, _soldeIndicatif: mvSoldeIndicatifByRowId.get(r.id) }));
|
||||||
);
|
}, [mvRows, mvFilteredCorrections, mvFilteredRembMouvements, mvFilteredSouscriptionMouvements, mvFilter.type, mvFilter.projet, mvRembProjetById, mvSoldeIndicatifByRowId]);
|
||||||
}, [mvRows, mvFilteredCorrections, mvFilteredRembMouvements, mvFilteredSouscriptionMouvements, mvFilter.type, mvFilter.projet, mvRembProjetById]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
pagedItems: mvPagedTableRows, page: mvPage, setPage: setMvPage,
|
pagedItems: mvPagedTableRows, page: mvPage, setPage: setMvPage,
|
||||||
@@ -1445,11 +1455,13 @@ export default function Plateformes() {
|
|||||||
<div className="label">Capital investi</div>
|
<div className="label">Capital investi</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
||||||
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.encours)}</span>
|
<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>
|
|
||||||
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
|
|
||||||
{fmtEUR(prevTotals.encours)} en {prevYearLabel}
|
|
||||||
</div>
|
</div>
|
||||||
|
{selectedYear && (
|
||||||
|
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
|
||||||
|
{fmtEUR(prevTotals.encours)} en {prevYearLabel}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{objectifsActifs && kpiEcartCapitalObjectif != null && (
|
{objectifsActifs && kpiEcartCapitalObjectif != null && (
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: '0.8em', marginTop: 4, fontWeight: 600,
|
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' : ''}>
|
<span style={{ fontSize: '1.35rem', fontWeight: 700 }} className={totals.defaut > 0 ? 'danger' : ''}>
|
||||||
{fmtEUR(totals.defaut)}
|
{fmtEUR(totals.defaut)}
|
||||||
</span>
|
</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>
|
|
||||||
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
|
|
||||||
{fmtEUR(prevTotals.defaut)} en {prevYearLabel}
|
|
||||||
</div>
|
</div>
|
||||||
|
{selectedYear && (
|
||||||
|
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
|
||||||
|
{fmtEUR(prevTotals.defaut)} en {prevYearLabel}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 3 — Porte-monnaie */}
|
{/* 3 — Porte-monnaie */}
|
||||||
@@ -1483,11 +1497,13 @@ export default function Plateformes() {
|
|||||||
<div className="label">Porte-monnaie</div>
|
<div className="label">Porte-monnaie</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
||||||
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(soldePortefeuille)}</span>
|
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(soldePortefeuille)}</span>
|
||||||
<TrendBadge current={soldePortefeuille} prev={prevSoldePortefeuille} />
|
{selectedYear && <TrendBadge current={soldePortefeuille} prev={prevSoldePortefeuille} />}
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
|
|
||||||
{fmtEUR(prevSoldePortefeuille)} en {prevYearLabel}
|
|
||||||
</div>
|
</div>
|
||||||
|
{selectedYear && (
|
||||||
|
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
|
||||||
|
{fmtEUR(prevSoldePortefeuille)} en {prevYearLabel}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 4 — Investissements depuis le début */}
|
{/* 4 — Investissements depuis le début */}
|
||||||
@@ -1495,11 +1511,13 @@ export default function Plateformes() {
|
|||||||
<div className="label">Investissements depuis le début</div>
|
<div className="label">Investissements depuis le début</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
||||||
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.investi)}</span>
|
<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>
|
|
||||||
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
|
|
||||||
{fmtEUR(prevTotals.investi)} en {prevYearLabel}
|
|
||||||
</div>
|
</div>
|
||||||
|
{selectedYear && (
|
||||||
|
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
|
||||||
|
{fmtEUR(prevTotals.investi)} en {prevYearLabel}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 5 — Capital remboursé */}
|
{/* 5 — Capital remboursé */}
|
||||||
@@ -1507,11 +1525,13 @@ export default function Plateformes() {
|
|||||||
<div className="label">Capital remboursé</div>
|
<div className="label">Capital remboursé</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
||||||
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.cap_remb)}</span>
|
<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>
|
|
||||||
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
|
|
||||||
{fmtEUR(prevTotals.cap_remb)} en {prevYearLabel}
|
|
||||||
</div>
|
</div>
|
||||||
|
{selectedYear && (
|
||||||
|
<div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>
|
||||||
|
{fmtEUR(prevTotals.cap_remb)} en {prevYearLabel}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 6 — Intérêts perçus */}
|
{/* 6 — Intérêts perçus */}
|
||||||
@@ -1521,14 +1541,18 @@ export default function Plateformes() {
|
|||||||
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>
|
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>
|
||||||
{fmtEUR(netMode ? totals.int_perc_net : totals.int_perc)}
|
{fmtEUR(netMode ? totals.int_perc_net : totals.int_perc)}
|
||||||
</span>
|
</span>
|
||||||
<TrendBadge
|
{selectedYear && (
|
||||||
current={netMode ? totals.int_perc_net : totals.int_perc}
|
<TrendBadge
|
||||||
prev={netMode ? prevTotals.int_perc_net : prevTotals.int_perc}
|
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}
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* 7 — XIRR */}
|
{/* 7 — XIRR */}
|
||||||
@@ -2566,12 +2590,12 @@ export default function Plateformes() {
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{mvTableRows.length === 0 && (
|
{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'}
|
{loading ? 'Chargement…' : 'Aucun mouvement'}
|
||||||
</td></tr>
|
</td></tr>
|
||||||
)}
|
)}
|
||||||
@@ -2599,6 +2623,7 @@ export default function Plateformes() {
|
|||||||
<td className={`num ${isPos ? 'success' : 'danger'}`}>
|
<td className={`num ${isPos ? 'success' : 'danger'}`}>
|
||||||
{isPos ? '+' : '−'} {fmtEUR(Math.abs(r.montant))}
|
{isPos ? '+' : '−'} {fmtEUR(Math.abs(r.montant))}
|
||||||
</td>
|
</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()}>
|
<td style={{ width: 36, textAlign: 'center' }} onClick={e => e.stopPropagation()}>
|
||||||
<button
|
<button
|
||||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px 6px', borderRadius: 4, color: 'var(--text-muted)', lineHeight: 1, fontSize: 16 }}
|
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">
|
<td className="num success">
|
||||||
+ {fmtEUR(r.montant)}
|
+ {fmtEUR(r.montant)}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
|
||||||
<td style={{ width: 36, textAlign: 'center' }} />
|
<td style={{ width: 36, textAlign: 'center' }} />
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -2665,6 +2691,7 @@ export default function Plateformes() {
|
|||||||
<td className="num danger">
|
<td className="num danger">
|
||||||
− {fmtEUR(r.montant)}
|
− {fmtEUR(r.montant)}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
|
||||||
<td style={{ width: 36, textAlign: 'center' }} />
|
<td style={{ width: 36, textAlign: 'center' }} />
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -2701,6 +2728,7 @@ export default function Plateformes() {
|
|||||||
<td className="num danger">
|
<td className="num danger">
|
||||||
− {fmtEUR(r.montant)}
|
− {fmtEUR(r.montant)}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
|
||||||
<td style={{ width: 36, textAlign: 'center' }} />
|
<td style={{ width: 36, textAlign: 'center' }} />
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -2732,6 +2760,7 @@ export default function Plateformes() {
|
|||||||
style={ isAuto ? { fontStyle: 'italic' } : undefined }>
|
style={ isAuto ? { fontStyle: 'italic' } : undefined }>
|
||||||
{r.type === 'depot' ? '+' : '−'} {fmtEUR(r.montant)}
|
{r.type === 'depot' ? '+' : '−'} {fmtEUR(r.montant)}
|
||||||
</td>
|
</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()}>
|
<td style={{ width: 36, textAlign: 'center' }} onClick={e => e.stopPropagation()}>
|
||||||
{!isAuto && (
|
{!isAuto && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ export default function Remboursements() {
|
|||||||
const [tableFilter, setTableFilter] = useState({ plateforme_id: '', investissement_id: '', year: '', month: '', type: '' });
|
const [tableFilter, setTableFilter] = useState({ plateforme_id: '', investissement_id: '', year: '', month: '', type: '' });
|
||||||
|
|
||||||
/* Filtre année du tableau Plateformes */
|
/* 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 */
|
/* Modal */
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
@@ -847,25 +847,25 @@ export default function Remboursements() {
|
|||||||
<div className="label">Capital remboursé</div>
|
<div className="label">Capital remboursé</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
||||||
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.capital)}</span>
|
<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>
|
||||||
<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>
|
||||||
<div className="kpi">
|
<div className="kpi">
|
||||||
<div className="label">Cashback</div>
|
<div className="label">Cashback</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
||||||
<span style={{ fontSize: '1.35rem', fontWeight: 700 }}>{fmtEUR(totals.cashback)}</span>
|
<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>
|
||||||
<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>
|
||||||
<div className="kpi">
|
<div className="kpi">
|
||||||
<div className="label">Intérêts — {netMode ? 'Net' : 'Brut'}</div>
|
<div className="label">Intérêts — {netMode ? 'Net' : 'Brut'}</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '4px 0 0' }}>
|
<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>
|
<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>
|
||||||
<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>
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,27 @@ export const fmtEUR = (n) => {
|
|||||||
// "-0,00 €" pour un montant nul.
|
// "-0,00 €" pour un montant nul.
|
||||||
export const fmtRetrait = (n) => (n > 0 ? `-${fmtEUR(n)}` : fmtEUR(n));
|
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) =>
|
export const fmtNum = (n, digits = 2) =>
|
||||||
(n == null ? '' : Number(n).toLocaleString('fr-FR', { minimumFractionDigits: digits, maximumFractionDigits: digits }));
|
(n == null ? '' : Number(n).toLocaleString('fr-FR', { minimumFractionDigits: digits, maximumFractionDigits: digits }));
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user