MAJ
This commit is contained in:
1 parent
6bba34396c
commit
1d5737703b
9 files changed
+347
-45
No files matched your search
@@ -559,6 +559,7 @@ export default function FraisPe() {
|
||||
investissementOptions={investissementOptions}
|
||||
confirmingDelete={confirmingDelete}
|
||||
setConfirmingDelete={setConfirmingDelete}
|
||||
pe
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1015,6 +1015,7 @@ export default function InvestissementPeDetail() {
|
||||
submitting={fraisSubmitting}
|
||||
comptesInvestisseur={comptesInvestisseur}
|
||||
locked
|
||||
pe
|
||||
investissementOptions={[]}
|
||||
confirmingDelete={fraisConfirmingDelete}
|
||||
setConfirmingDelete={setFraisConfirmingDelete}
|
||||
|
||||
@@ -306,6 +306,12 @@ export default function Plateformes() {
|
||||
const [allDepots, setAllDepots] = useState([]);
|
||||
const [allCorrections, setAllCorrections] = useState([]);
|
||||
const [allFraisOps, setAllFraisOps] = useState([]); // frais_operations, tous investisseurs (filtré via mvInvById)
|
||||
// Mouvements porte-monnaie PE (souscriptions/distributions/frais) vivant hors du workspace
|
||||
// crowdlending actif — cf. GET /investissements-pe/mouvements-portefeuille. Une plateforme
|
||||
// (ex. Fundora) peut faire du crowdlending ET du PE avec un porte-monnaie physique unique ;
|
||||
// sans ça, le solde indicatif de l'onglet "Mouvements Porte-monnaie" ignorait tout l'argent
|
||||
// côté PE (bug signalé par Olivier le 19/09/26).
|
||||
const [allPeWalletMouvements, setAllPeWalletMouvements] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
// Snapshots backend du solde porte-monnaie par plateforme, clé 'live' ou 'YYYY-12-31'
|
||||
// (GET /dashboard[?asOf=]) — remplace le recalcul client historique de
|
||||
@@ -377,7 +383,7 @@ export default function Plateformes() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const scopeParams = activeView === 'all' ? { scope: 'all' } : {};
|
||||
const [rows, p, rembs, reinvests, depots, corrections, simulRembs, pfu, fraisOps] = await Promise.all([
|
||||
const [rows, p, rembs, reinvests, depots, corrections, simulRembs, pfu, fraisOps, peWalletMouvements] = await Promise.all([
|
||||
api.get('/investissements', scopeParams),
|
||||
api.get('/plateformes'),
|
||||
api.get('/remboursements', scopeParams),
|
||||
@@ -389,6 +395,10 @@ export default function Plateformes() {
|
||||
// /frais-operations sans investissement_id ne connaît lui aussi que l'utilisateur —
|
||||
// même pattern que /reinvestissements, filtré côté client via mvInvById.
|
||||
api.get('/frais-operations').catch(() => []),
|
||||
// Toujours hors scopeParams (scope=all/X-Investisseur-Id géré côté backend comme les
|
||||
// autres endpoints ci-dessus) mais jamais borné au workspace actif, volontairement —
|
||||
// cf. commentaire de la route.
|
||||
api.get('/investissements-pe/mouvements-portefeuille', scopeParams).catch(() => []),
|
||||
]);
|
||||
setAllRows(rows);
|
||||
setPlats(p);
|
||||
@@ -399,6 +409,7 @@ export default function Plateformes() {
|
||||
setAllSimulRembs(Array.isArray(simulRembs) ? simulRembs : []);
|
||||
setPfuRates(Array.isArray(pfu) ? pfu : []);
|
||||
setAllFraisOps(Array.isArray(fraisOps) ? fraisOps : []);
|
||||
setAllPeWalletMouvements(Array.isArray(peWalletMouvements) ? peWalletMouvements : []);
|
||||
} finally { setLoading(false); setDrillRefreshKey(k => k + 1); }
|
||||
};
|
||||
|
||||
@@ -1078,7 +1089,7 @@ export default function Plateformes() {
|
||||
allReinvests, allCorrections, plats). Toute évolution de la logique de
|
||||
décomposition des mouvements doit être répercutée dans les deux fichiers.
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
const [mvFilter, setMvFilter] = useState({ type: '', plateforme_ids: [], projet: '', year: '', month: '' });
|
||||
const [mvFilter, setMvFilter] = useState({ type: '', investType: '', plateforme_ids: [], projet: '', year: '', month: '' });
|
||||
const [mvListFocused, setMvListFocused] = useState(false);
|
||||
const [mvModalOpen, setMvModalOpen] = useState(false);
|
||||
const [mvEditingId, setMvEditingId] = useState(null);
|
||||
@@ -1327,6 +1338,32 @@ export default function Plateformes() {
|
||||
return true;
|
||||
}), [mvNormalizedFraisMouvements, mvFilter.plateforme_ids, mvFilter.year, mvFilter.month]);
|
||||
|
||||
// Mouvements PE (19/09/26, demande Olivier — porte-monnaie unifié) : déjà mis en forme côté
|
||||
// backend (GET /investissements-pe/mouvements-portefeuille, type/montant/date_operation/
|
||||
// plateforme_id/libelle prêts à l'emploi), on ajoute juste ici _investType (pour le filtre et
|
||||
// le badge) et `source` (même convention que les lignes crowdlending, pour l'export).
|
||||
// Depuis le 19/09/26, allPeWalletMouvements inclut aussi des lignes type='depot'/'retrait'
|
||||
// (dépôts manuels enregistrés côté workspace PE, ex. les 4 x 800 € de Fundora) : on leur
|
||||
// reconstitue un `source` cohérent avec les dépôts crowdlending (mvRows, via raw_source =
|
||||
// valeur brute de depots_retraits.source) pour que le badge "AUTO" et l'export CSV/XLS
|
||||
// restent corrects.
|
||||
const mvNormalizedPeMouvements = useMemo(() => allPeWalletMouvements.map(r => ({
|
||||
...r,
|
||||
_investType: 'pe',
|
||||
source: r.type === 'souscription' ? 'souscription'
|
||||
: r.type === 'frais' ? 'frais_operation'
|
||||
: (r.type === 'depot' || r.type === 'retrait') ? (r.raw_source || 'manuel')
|
||||
: 'remboursement',
|
||||
})), [allPeWalletMouvements]);
|
||||
|
||||
const mvFilteredPeMouvements = useMemo(() => mvNormalizedPeMouvements.filter(r => {
|
||||
if (mvFilter.plateforme_ids.length > 0 && !mvFilter.plateforme_ids.includes(String(r.plateforme_id))) return false;
|
||||
const [y, m] = r.date_operation.split('-');
|
||||
if (mvFilter.year && y !== mvFilter.year) return false;
|
||||
if (mvFilter.month && m !== mvFilter.month.padStart(2,'0')) return false;
|
||||
return true;
|
||||
}), [mvNormalizedPeMouvements, mvFilter.plateforme_ids, mvFilter.year, mvFilter.month]);
|
||||
|
||||
// Projets disponibles pour le filtre "Projet" — dépend de la plateforme (verrouillée) mais
|
||||
// pas des autres filtres (type/année/mois), pour ne pas faire disparaître d'options en cours
|
||||
// d'affinage. Inclut les remboursements/souscriptions/réinvestissements ainsi que les
|
||||
@@ -1338,46 +1375,55 @@ export default function Plateformes() {
|
||||
for (const r of mvNormalizedRembMouvements) if (inScope(r.plateforme_id) && r.libelle) set.add(r.libelle);
|
||||
for (const r of mvNormalizedSouscriptionMouvements) if (inScope(r.plateforme_id) && r.libelle) set.add(r.libelle);
|
||||
for (const r of mvNormalizedFraisMouvements) if (inScope(r.plateforme_id) && r.libelle) set.add(r.libelle);
|
||||
for (const r of mvNormalizedPeMouvements) if (inScope(r.plateforme_id) && r.libelle) set.add(r.libelle);
|
||||
for (const d of allDepots) {
|
||||
if (!inScope(d.plateforme_id) || !d.remboursement_id) continue;
|
||||
const p = mvRembProjetById.get(d.remboursement_id);
|
||||
if (p) set.add(p);
|
||||
}
|
||||
return [...set].sort((a, b) => a.localeCompare(b, 'fr'));
|
||||
}, [mvNormalizedRembMouvements, mvNormalizedSouscriptionMouvements, mvNormalizedFraisMouvements, allDepots, mvRembProjetById, mvFilter.plateforme_ids]);
|
||||
}, [mvNormalizedRembMouvements, mvNormalizedSouscriptionMouvements, mvNormalizedFraisMouvements, mvNormalizedPeMouvements, 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.
|
||||
// mvNormalizedSouscriptionMouvements/mvNormalizedFraisMouvements/mvNormalizedPeMouvements),
|
||||
// pas sur les variantes déjà filtrées année/mois/type/projet/investType — sinon le cumul
|
||||
// démarrerait arbitrairement au milieu de l'historique. mvNormalizedPeMouvements est inclus
|
||||
// ici (19/09/26, demande Olivier) même s'il n'a pas d'onglet dédié pour l'afficher seul : sur
|
||||
// une plateforme partagée, ces lignes PE font partie du MÊME porte-monnaie physique, donc du
|
||||
// même cumul, qu'on les affiche ou non selon le filtre "Type d'investissement" du moment.
|
||||
// Purement indicatif (affichage/export) — ne remplace pas computeSoldePortefeuille.
|
||||
// Voir [[project_mouvements_solde_indicatif]].
|
||||
const mvSoldeIndicatifByRowId = useMemo(() => computeSoldeIndicatifMap([
|
||||
...allDepots, ...mvNormalizedCorrections, ...mvNormalizedRembMouvements, ...mvNormalizedSouscriptionMouvements,
|
||||
...mvNormalizedFraisMouvements,
|
||||
]), [allDepots, mvNormalizedCorrections, mvNormalizedRembMouvements, mvNormalizedSouscriptionMouvements, mvNormalizedFraisMouvements]);
|
||||
...mvNormalizedFraisMouvements, ...mvNormalizedPeMouvements,
|
||||
]), [allDepots, mvNormalizedCorrections, mvNormalizedRembMouvements, mvNormalizedSouscriptionMouvements, mvNormalizedFraisMouvements, mvNormalizedPeMouvements]);
|
||||
|
||||
const mvTableRows = useMemo(() => {
|
||||
const showDR = !mvFilter.type || mvFilter.type === 'depot' || mvFilter.type === 'retrait';
|
||||
const showCorr = !mvFilter.type || mvFilter.type === 'correction';
|
||||
const drPart = showDR
|
||||
? mvRows.map(r => ({ ...r, _projet: r.remboursement_id ? (mvRembProjetById.get(r.remboursement_id) || '') : '' }))
|
||||
const showCl = mvFilter.investType !== 'pe';
|
||||
const showPe = mvFilter.investType !== 'crowdlending';
|
||||
const drPart = showDR && showCl
|
||||
? mvRows.map(r => ({ ...r, _projet: r.remboursement_id ? (mvRembProjetById.get(r.remboursement_id) || '') : '', _investType: 'crowdlending' }))
|
||||
: [];
|
||||
const corrPart = showCorr ? mvFilteredCorrections.map(c => ({ ...c, _projet: '' })) : [];
|
||||
const rembPart = mvFilteredRembMouvements
|
||||
const corrPart = showCorr && showCl ? mvFilteredCorrections.map(c => ({ ...c, _projet: '', _investType: 'crowdlending' })) : [];
|
||||
const rembPart = showCl ? mvFilteredRembMouvements
|
||||
.filter(r => !mvFilter.type || r.type === mvFilter.type)
|
||||
.map(r => ({ ...r, _projet: r.libelle || '' }));
|
||||
const subPart = mvFilteredSouscriptionMouvements
|
||||
.map(r => ({ ...r, _projet: r.libelle || '', _investType: 'crowdlending' })) : [];
|
||||
const subPart = showCl ? mvFilteredSouscriptionMouvements
|
||||
.filter(r => !mvFilter.type || r.type === mvFilter.type)
|
||||
.map(r => ({ ...r, _projet: r.libelle || '' }));
|
||||
const fraisPart = mvFilteredFraisMouvements
|
||||
.map(r => ({ ...r, _projet: r.libelle || '', _investType: 'crowdlending' })) : [];
|
||||
const fraisPart = showCl ? mvFilteredFraisMouvements
|
||||
.filter(r => !mvFilter.type || r.type === mvFilter.type)
|
||||
.map(r => ({ ...r, _projet: r.libelle || '' }));
|
||||
.map(r => ({ ...r, _projet: r.libelle || '', _investType: 'crowdlending' })) : [];
|
||||
const pePart = showPe ? mvFilteredPeMouvements
|
||||
.filter(r => !mvFilter.type || r.type === mvFilter.type)
|
||||
.map(r => ({ ...r, _projet: r.libelle || '' })) : [];
|
||||
return sortMouvements(
|
||||
[...drPart, ...corrPart, ...rembPart, ...subPart, ...fraisPart]
|
||||
[...drPart, ...corrPart, ...rembPart, ...subPart, ...fraisPart, ...pePart]
|
||||
.filter(r => !mvFilter.projet || r._projet === mvFilter.projet)
|
||||
).map(r => ({ ...r, _soldeIndicatif: mvSoldeIndicatifByRowId.get(r.id) }));
|
||||
}, [mvRows, mvFilteredCorrections, mvFilteredRembMouvements, mvFilteredSouscriptionMouvements, mvFilteredFraisMouvements, mvFilter.type, mvFilter.projet, mvRembProjetById, mvSoldeIndicatifByRowId]);
|
||||
}, [mvRows, mvFilteredCorrections, mvFilteredRembMouvements, mvFilteredSouscriptionMouvements, mvFilteredFraisMouvements, mvFilteredPeMouvements, mvFilter.type, mvFilter.projet, mvFilter.investType, mvRembProjetById, mvSoldeIndicatifByRowId]);
|
||||
|
||||
const {
|
||||
pagedItems: mvPagedTableRows, page: mvPage, setPage: setMvPage,
|
||||
@@ -2759,6 +2805,14 @@ export default function Plateformes() {
|
||||
<option value="prelevement_local">Prélèvements étranger</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 130 }}>
|
||||
<label>Type d'investissement</label>
|
||||
<select value={mvFilter.investType} onChange={e => setMvFilter(f => ({ ...f, investType: e.target.value }))}>
|
||||
<option value="">Tous</option>
|
||||
<option value="crowdlending">Crowdlending</option>
|
||||
<option value="pe">Private Equity</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ flex: 2, minWidth: 160 }}>
|
||||
<label>Plateforme</label>
|
||||
<div style={{
|
||||
@@ -2866,7 +2920,7 @@ export default function Plateformes() {
|
||||
color: (isBonus || isCashback) ? 'var(--warning)' : isInteretsPlateforme ? 'var(--primary)' : 'var(--success)',
|
||||
}}>{isBonus ? 'bonus' : isCashback ? 'cashback' : isInteretsPlateforme ? 'intérêts plateforme' : 'remboursement'}</span>
|
||||
</td>
|
||||
<td className="text-muted">{r._projet || '—'}</td>
|
||||
<td className="text-muted">{r._investType === 'pe' && <span style={{ display: 'inline-block', fontSize: 10, fontWeight: 700, letterSpacing: '0.03em', padding: '1px 5px', borderRadius: 4, marginRight: 6, background: 'color-mix(in srgb, var(--primary) 15%, transparent)', color: 'var(--primary)' }} title="Mouvement Private Equity">PE</span>}{r._projet || '—'}</td>
|
||||
<td className="num success">
|
||||
+ {fmtEUR(r.montant)}
|
||||
</td>
|
||||
@@ -2896,7 +2950,7 @@ export default function Plateformes() {
|
||||
color,
|
||||
}}>{label}</span>
|
||||
</td>
|
||||
<td className="text-muted">{r._projet || '—'}</td>
|
||||
<td className="text-muted">{r._investType === 'pe' && <span style={{ display: 'inline-block', fontSize: 10, fontWeight: 700, letterSpacing: '0.03em', padding: '1px 5px', borderRadius: 4, marginRight: 6, background: 'color-mix(in srgb, var(--primary) 15%, transparent)', color: 'var(--primary)' }} title="Mouvement Private Equity">PE</span>}{r._projet || '—'}</td>
|
||||
<td className="num danger">
|
||||
− {fmtEUR(r.montant)}
|
||||
</td>
|
||||
@@ -2933,7 +2987,7 @@ export default function Plateformes() {
|
||||
}}>RÉINVEST.</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-muted">{r._projet || '—'}</td>
|
||||
<td className="text-muted">{r._investType === 'pe' && <span style={{ display: 'inline-block', fontSize: 10, fontWeight: 700, letterSpacing: '0.03em', padding: '1px 5px', borderRadius: 4, marginRight: 6, background: 'color-mix(in srgb, var(--primary) 15%, transparent)', color: 'var(--primary)' }} title="Mouvement Private Equity">PE</span>}{r._projet || '—'}</td>
|
||||
<td className="num danger">
|
||||
− {fmtEUR(r.montant)}
|
||||
</td>
|
||||
@@ -2945,11 +2999,17 @@ export default function Plateformes() {
|
||||
if (r.type === 'frais') {
|
||||
const horsPM = r.mode_reglement === 'compte_courant';
|
||||
const rembLinked = r.mode_reglement === 'remboursement';
|
||||
// Frais PE "à la source" (19/09/26) : déjà inclus dans le montant de la
|
||||
// souscription (-800 € par ex.) — pas un débit supplémentaire du
|
||||
// porte-monnaie. Même traitement visuel que HORS PM/DÉDUIT REMB. (grisé,
|
||||
// badge, 0 d'effet sur le solde indicatif via signedMontant).
|
||||
const dejaInclusSouscr = r._investType === 'pe' && r.mode_reglement === 'source';
|
||||
const zeroImpact = horsPM || dejaInclusSouscr;
|
||||
return (
|
||||
<tr key={r.id}
|
||||
className={`dr-row${mvSelectedRow?.id === r.id ? ' dr-row-selected' : ''}`}
|
||||
onClick={() => setMvSelectedRow(mvSelectedRow?.id === r.id ? null : r)}
|
||||
style={{ cursor: 'pointer', opacity: horsPM ? 0.75 : 1 }}>
|
||||
style={{ cursor: 'pointer', opacity: zeroImpact ? 0.75 : 1 }}>
|
||||
<td>{fmtDate(r.date_operation)}</td>
|
||||
<td>{r.plateforme_nom}</td>
|
||||
{multiDetenteur && <td className="text-muted" style={{ fontSize: 'var(--fs-sm)' }}>{r.plateforme_detenteur_nom || '—'}</td>}
|
||||
@@ -2981,9 +3041,19 @@ export default function Plateformes() {
|
||||
verticalAlign: 'middle',
|
||||
}} title="Déjà déduit du montant net du remboursement — aucun effet supplémentaire sur le solde du porte-monnaie">DÉDUIT REMB.</span>
|
||||
)}
|
||||
{dejaInclusSouscr && (
|
||||
<span style={{
|
||||
marginLeft: 6,
|
||||
fontSize: 10, fontWeight: 600, letterSpacing: '0.03em',
|
||||
padding: '1px 5px', borderRadius: 3,
|
||||
background: 'var(--surface-2)',
|
||||
color: 'var(--text-muted)',
|
||||
verticalAlign: 'middle',
|
||||
}} title="Déjà inclus dans le montant de la souscription — aucun effet supplémentaire sur le solde du porte-monnaie">DÉDUIT SOUSCR.</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-muted">{r._projet || '—'}</td>
|
||||
<td className={horsPM ? 'num text-muted' : 'num danger'}>
|
||||
<td className="text-muted">{r._investType === 'pe' && <span style={{ display: 'inline-block', fontSize: 10, fontWeight: 700, letterSpacing: '0.03em', padding: '1px 5px', borderRadius: 4, marginRight: 6, background: 'color-mix(in srgb, var(--primary) 15%, transparent)', color: 'var(--primary)' }} title="Mouvement Private Equity">PE</span>}{r._projet || '—'}</td>
|
||||
<td className={zeroImpact ? 'num text-muted' : 'num danger'}>
|
||||
− {fmtEUR(r.montant)}
|
||||
</td>
|
||||
<td className="num text-muted" style={{ fontStyle: 'italic' }}>{r._soldeIndicatif != null ? fmtEUR(r._soldeIndicatif) : '—'}</td>
|
||||
@@ -2992,6 +3062,12 @@ export default function Plateformes() {
|
||||
);
|
||||
}
|
||||
const isAuto = r.source === 'auto_remboursement';
|
||||
// Dépôt/retrait PE (19/09/26) : même rendu que les dépôts crowdlending,
|
||||
// mais SANS le menu d'édition/suppression — cette ligne vit dans la table
|
||||
// depots_retraits sous le workspace PE, et l'éditer/la supprimer depuis ce
|
||||
// menu (pensé pour le workspace crowdlending) réattribuerait sa gestion au
|
||||
// mauvais endroit. Modifiable uniquement depuis le workspace PE d'origine.
|
||||
const isForeign = r._investType === 'pe';
|
||||
return (
|
||||
<tr key={r.id}
|
||||
className={`dr-row${mvSelectedRow?.id === r.id ? ' dr-row-selected' : ''}`}
|
||||
@@ -3013,14 +3089,14 @@ export default function Plateformes() {
|
||||
}}>AUTO</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-muted">{r._projet || '—'}</td>
|
||||
<td className="text-muted">{isForeign && <span style={{ display: 'inline-block', fontSize: 10, fontWeight: 700, letterSpacing: '0.03em', padding: '1px 5px', borderRadius: 4, marginRight: 6, background: 'color-mix(in srgb, var(--primary) 15%, transparent)', color: 'var(--primary)' }} title="Mouvement Private Equity">PE</span>}{r._projet || '—'}</td>
|
||||
<td className={`num ${r.type === 'depot' ? 'success' : 'danger'}`}
|
||||
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 && (
|
||||
{!isAuto && !isForeign && (
|
||||
<button
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px 6px', borderRadius: 4, color: 'var(--text-muted)', lineHeight: 1, fontSize: 16 }}
|
||||
onClick={e => mvOpenRowMenu(e, r)}>⋮</button>
|
||||
|
||||
Reference in new issue
Block a user