import { useEffect, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { api } from '../api.js'; import { useInvestisseur } from '../context/InvestisseurContext.jsx'; import { useWorkspace } from '../context/WorkspaceContext.jsx'; import Modal from '../components/Modal.jsx'; import ConfirmModal from '../components/ConfirmModal.jsx'; import PageIcon from '../components/PageIcon.jsx'; import InvestissementPeFormModal from '../components/InvestissementPeFormModal.jsx'; import FraisFormModal from '../components/FraisFormModal.jsx'; import { FRAIS_CATEGORIE_LABELS } from '../components/MouvementsShared.jsx'; import { useAsyncAction } from '../hooks/useAsyncAction.js'; import { fmtEUR, fmtDate, fmtOctets, today } from '../utils/format.js'; import { emptyDealForm, AUTO_VAL_NOTE, AUTO_VAL_NOTE_LEGACY } from './InvestissementsPe.jsx'; /** * Fiche détaillée d'un deal Private Equity (chantier "fiche détail PE", 17/09/26 — demande * Olivier : "une page investissement détail adapté qui reprend un peu l'esprit de crowdlending * ... il faut que je vois tous les remboursements, tous les frais associés et que j'aie la * possibilité de stocker des documents"). Première passe volontairement sobre (structure posée, * à enrichir ensuite) — pas encore de reprise des notions Déploiement / TVPI du sous-jacent / * Obligation / Valeur totale cible visibles sur les captures Fundora d'Olivier, toujours hors * périmètre comme convenu lors du chantier précédent. * * Route : /investissements/:id, branchée ici pour tout workspace non-crowdlending par * InvestissementDetailRouter (App.jsx) — miroir de InvestissementDetail.jsx (crowdlending), en * beaucoup plus court : pas d'échéancier, de XIRR, de révisions de prêt ni de réinvestissements * (notions sans équivalent PE à ce stade). * * Modifier l'investissement réutilise directement InvestissementPeFormModal (le même assistant * par étapes que la liste), y compris son étape "Frais" (souscription/gestion réglés à la * source) — le bloc "Frais d'opération" ci-dessous gère lui les frais complémentaires * (gestion/distribution/autre), exactement comme côté crowdlending : les lignes 'souscription' * et 'gestion'-à-l'origine-souscription y apparaissent en lecture seule, redirigeant vers * "Modifier l'investissement" plutôt que vers la modale FraisFormModal (qui n'propose pas la * catégorie 'souscription' dans son sélecteur). */ const fmtMultiple = (n) => (n != null ? `x${n.toFixed(2).replace('.', ',')}` : '—'); const STATUT_LABELS = { en_attente: 'En attente', valide: 'Validé', cloture: 'Clôturé', perte_definitive: 'Perte définitive', }; const FRAIS_MODE_LABELS = { source: 'À la source', portefeuille: 'Porte-monnaie', compte_courant: 'Compte courant', remboursement: 'Retenu sur remboursement', }; const emptyValForm = { date_valorisation: today(), valorisation_nette: '', capital_deploye: '', plus_value_latente: '', notes: '' }; const emptyFraisForm = { montant: '', mode_reglement: 'source', compte_id: '', date_operation: today(), categorie: 'gestion', notes: '' }; // ── Tableau de bord "Montant souscrit / Valorisation nette" (18/09/26, demande Olivier — // reprise de la disposition des captures Fundora : 2 cartes avec barre de répartition + 3 // colonnes Déploiement/Performance/Frais). Petits composants de présentation partagés par les // deux cartes et les 3 colonnes ci-dessous. function LegendDot({ color, outline, label, value }) { return ( {label} {fmtEUR(value)} ); } function StatRow({ label, value, caption, valueColor }) { return (
{label}
{value}
{caption &&
{caption}
}
); } export default function InvestissementPeDetail() { const { id } = useParams(); const navigate = useNavigate(); const { investisseurs } = useInvestisseur(); const { active: activeWorkspace } = useWorkspace() || {}; const [deal, setDeal] = useState(null); const [allPlats, setAllPlats] = useState([]); const [remboursements, setRemboursements] = useState([]); const [fraisOps, setFraisOps] = useState([]); const [valorisations, setValorisations] = useState([]); const [documents, setDocuments] = useState([]); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); const plats = allPlats.filter(p => (p.workspaces || []).some(w => w.id === activeWorkspace?.id)); const load = async () => { setLoading(true); try { const [d, pl, remb, frais, val, docs] = await Promise.all([ api.get(`/investissements-pe/${id}`), api.get('/plateformes'), api.get('/remboursements'), api.get('/frais-operations', { investissement_pe_id: id }), api.get(`/investissements-pe/${id}/valorisations`), api.get('/documents', { entity_type: 'investissement_pe', entity_id: id }), ]); setDeal(d); setAllPlats(pl); setRemboursements(remb.filter(r => String(r.investissement_pe_id) === String(id))); setFraisOps(frais); setValorisations(val); setDocuments(docs); } catch (e) { setErr(e.message); } finally { setLoading(false); } }; useEffect(() => { load(); /* eslint-disable-next-line */ }, [id]); /* ── Modifier l'investissement (réutilise le même assistant que la liste) ── */ const [modalOpen, setModalOpen] = useState(false); const [form, setForm] = useState(emptyDealForm); const [formErr, setFormErr] = useState(null); const [comptesInvestisseur, setComptesInvestisseur] = useState([]); const { run: runSubmit, pending: submitting } = useAsyncAction(); const [deleteConfirm, setDeleteConfirm] = useState(false); useEffect(() => { if (!form.investisseur_id || !modalOpen) { setComptesInvestisseur([]); return; } api.get(`/investissements/comptes-par-investisseur/${form.investisseur_id}`) .then(setComptesInvestisseur) .catch(() => setComptesInvestisseur([])); }, [form.investisseur_id, modalOpen]); const openEditDeal = () => { if (!deal) return; const entree = fraisOps.find(f => f.categorie === 'souscription'); const gestion = fraisOps.find(f => f.categorie === 'gestion' && f.origine === 'souscription'); const autoVal = valorisations.find(v => v.notes === AUTO_VAL_NOTE || v.notes === AUTO_VAL_NOTE_LEGACY); setForm({ ...emptyDealForm, plateforme_id: String(deal.plateforme_id || ''), investisseur_id: String(deal.investisseur_id || ''), nom_deal: deal.nom_deal, strategie: deal.strategie || '', date_souscription: deal.date_souscription, duree_annees: deal.duree_mois ? String(Math.round((deal.duree_mois / 12) * 10) / 10) : '', montant_investi: String(deal.montant_investi ?? ''), objectif: deal.objectif || '', statut: deal.statut, reference: deal.reference || '', notes: deal.notes || '', valorisation_initiale_id: autoVal ? autoVal.id : null, ...(entree ? { frais_entree_valeur: String(entree.montant), frais_entree_mode_reglement: entree.mode_reglement, frais_entree_compte_id: entree.compte_id ? String(entree.compte_id) : '', frais_entree_id: entree.id, } : {}), ...(gestion ? { frais_gestion_valeur: String(gestion.montant), frais_gestion_mode_reglement: gestion.mode_reglement, frais_gestion_compte_id: gestion.compte_id ? String(gestion.compte_id) : '', frais_gestion_id: gestion.id, } : {}), }); setFormErr(null); setModalOpen(true); }; const closeEditDeal = () => { setModalOpen(false); setFormErr(null); }; const investisseurForPlat = (platId) => { const plat = plats.find(p => String(p.id) === String(platId)); return plat?.investisseur_id ? String(plat.investisseur_id) : String(deal?.investisseur_id || ''); }; const computeFraisTTC = (modeSaisie, valeur) => { if (valeur === '' || Number(valeur) <= 0) return 0; return Math.round((modeSaisie === 'pourcentage' ? Number(form.montant_investi) * Number(valeur) / 100 : Number(valeur)) * 100) / 100; }; const upsertFrais = async (has, ttc, categorie, modeReglement, compteId, fraisId) => { try { if (has) { const payload = { investissement_pe_id: Number(id), montant: ttc, mode_reglement: modeReglement, compte_id: modeReglement === 'compte_courant' && compteId ? Number(compteId) : null, date_operation: form.date_souscription, origine: 'souscription', categorie, }; if (fraisId) await api.put(`/frais-operations/${fraisId}`, payload); else await api.post('/frais-operations', payload); } else if (fraisId) { await api.del(`/frais-operations/${fraisId}`); } } catch { /* non bloquant */ } }; // Toujours créée, même sans frais (= montant investi tel quel) — décision Olivier 18/09/26, // cf. InvestissementsPe.jsx::syncValorisationInitiale (même logique, dupliquée ici pour la // modale d'édition de la fiche détail). const syncValorisationInitiale = async (totalFraisSource, valorisationInitialeId) => { try { if (valorisationInitialeId) await api.del(`/investissements-pe/${id}/valorisations/${valorisationInitialeId}`); const initiale = Math.max(0, Math.round((Number(form.montant_investi) - totalFraisSource) * 100) / 100); await api.post(`/investissements-pe/${id}/valorisations`, { date_valorisation: form.date_souscription, valorisation_nette: initiale, notes: AUTO_VAL_NOTE, }); } catch { /* non bloquant */ } }; const submitDeal = async (e) => { e?.preventDefault?.(); setFormErr(null); const platFraisType = plats.find(p => String(p.id) === String(form.plateforme_id))?.frais_plateforme; const fraisApplicable = !!platFraisType && platFraisType !== 'aucun'; const hasFraisEntree = fraisApplicable && form.frais_entree_valeur !== '' && Number(form.frais_entree_valeur) > 0; const hasFraisGestion = fraisApplicable && form.frais_gestion_valeur !== '' && Number(form.frais_gestion_valeur) > 0; const fraisEntreeTTC = hasFraisEntree ? computeFraisTTC(form.frais_entree_mode_saisie, form.frais_entree_valeur) : 0; const fraisGestionTTC = hasFraisGestion ? computeFraisTTC(form.frais_gestion_mode_saisie, form.frais_gestion_valeur) : 0; const totalFraisSource = (hasFraisEntree && form.frais_entree_mode_reglement === 'source' ? fraisEntreeTTC : 0) + (hasFraisGestion && form.frais_gestion_mode_reglement === 'source' ? fraisGestionTTC : 0); // Capital réellement disponible à déployer = montant investi net des frais prélevés "à la // source" — même correctif que InvestissementsPe.jsx::submit (18/09/26, Olivier). const capitalInvestissable = Math.max(0, Math.round((Number(form.montant_investi) - totalFraisSource) * 100) / 100); const payload = { plateforme_id: Number(form.plateforme_id), investisseur_id: Number(form.investisseur_id) || undefined, nom_deal: form.nom_deal, strategie: form.strategie || undefined, date_souscription: form.date_souscription, duree_mois: form.duree_annees ? Math.round(Number(form.duree_annees) * 12) : undefined, montant_investi: Number(form.montant_investi), capital_investissable: capitalInvestissable, objectif: form.objectif || undefined, statut: form.statut, reference: form.reference || undefined, notes: form.notes || undefined, }; try { await api.put(`/investissements-pe/${id}`, payload); await upsertFrais(hasFraisEntree, fraisEntreeTTC, 'souscription', form.frais_entree_mode_reglement, form.frais_entree_compte_id, form.frais_entree_id); await upsertFrais(hasFraisGestion, fraisGestionTTC, 'gestion', form.frais_gestion_mode_reglement, form.frais_gestion_compte_id, form.frais_gestion_id); await syncValorisationInitiale(totalFraisSource, form.valorisation_initiale_id); closeEditDeal(); await load(); } catch (e) { setFormErr(e.message); } }; const onDeleteDeal = async () => { try { await api.del(`/investissements-pe/${id}`); navigate('/investissements'); } catch (e) { setErr(e.message); } }; /* ── Valorisations ── */ const [valForm, setValForm] = useState(emptyValForm); const [valErr, setValErr] = useState(null); const [valDeleteTarget, setValDeleteTarget] = useState(null); const { run: runValSubmit, pending: valSubmitting } = useAsyncAction(); const submitVal = async (e) => { e?.preventDefault?.(); setValErr(null); try { // Répartition optionnelle (18/09/26, demande Olivier) : si Capital déployé est renseigné, // la Valorisation nette est recalculée ici à partir des 3 composantes plutôt que reprise // du champ (qui est alors désactivé côté UI) — recalculée aussi côté serveur, qui reste la // source de vérité finale (cf. POST /:id/valorisations). const hasBreakdown = valForm.capital_deploye !== ''; const capitalDeploye = hasBreakdown ? (Number(valForm.capital_deploye) || 0) : undefined; const plusValueLatente = hasBreakdown ? (Number(valForm.plus_value_latente) || 0) : undefined; // Capital investissable (net des frais "à la source"), pas le montant investi brut — // correctif 18/09/26, cf. deal.capital_investissable. const capitalInvestissable = Number(deal.capital_investissable ?? deal.montant_investi); const valorisationNette = hasBreakdown ? Math.round((capitalDeploye + plusValueLatente + Math.max(0, capitalInvestissable - capitalDeploye)) * 100) / 100 : Number(valForm.valorisation_nette); await api.post(`/investissements-pe/${id}/valorisations`, { date_valorisation: valForm.date_valorisation, valorisation_nette: valorisationNette, capital_deploye: capitalDeploye, plus_value_latente: plusValueLatente, notes: valForm.notes || undefined, }); setValForm(emptyValForm); await load(); } catch (e) { setValErr(e.message); } }; const onDeleteVal = async () => { if (!valDeleteTarget) return; try { await api.del(`/investissements-pe/${id}/valorisations/${valDeleteTarget.id}`); setValDeleteTarget(null); await load(); } catch (e) { setValErr(e.message); } }; /* ── Frais d'opération complémentaires (gestion/distribution/autre) ── */ const [fraisModalOpen, setFraisModalOpen] = useState(false); const [fraisEditingId, setFraisEditingId] = useState(null); const [fraisForm, setFraisForm] = useState(emptyFraisForm); const [fraisErr, setFraisErr] = useState(null); const [fraisConfirmingDelete, setFraisConfirmingDelete] = useState(false); const { run: runFraisSubmit, pending: fraisSubmitting } = useAsyncAction(); const openAddFrais = () => { setFraisEditingId(null); setFraisForm({ ...emptyFraisForm, date_operation: today() }); setFraisErr(null); setFraisConfirmingDelete(false); setFraisModalOpen(true); }; const openEditFrais = (f) => { setFraisEditingId(f.id); setFraisForm({ montant: f.montant, mode_reglement: f.mode_reglement, compte_id: f.compte_id || '', date_operation: f.date_operation, categorie: f.categorie || 'autre', notes: f.notes || '', }); setFraisErr(null); setFraisConfirmingDelete(false); setFraisModalOpen(true); }; const closeFraisModal = () => { setFraisModalOpen(false); setFraisErr(null); setFraisConfirmingDelete(false); }; const submitFrais = async (e) => { e?.preventDefault?.(); setFraisErr(null); try { const payload = { investissement_pe_id: Number(id), montant: Number(fraisForm.montant), mode_reglement: fraisForm.mode_reglement, compte_id: fraisForm.mode_reglement === 'compte_courant' && fraisForm.compte_id ? Number(fraisForm.compte_id) : null, date_operation: fraisForm.date_operation, origine: 'manuel', categorie: fraisForm.categorie || 'autre', notes: fraisForm.notes || undefined, }; if (fraisEditingId) await api.put(`/frais-operations/${fraisEditingId}`, payload); else await api.post('/frais-operations', payload); closeFraisModal(); await load(); } catch (e) { setFraisErr(e.message); } }; const onDeleteFrais = async () => { try { await api.del(`/frais-operations/${fraisEditingId}`); closeFraisModal(); await load(); } catch (e) { setFraisErr(e.message); } }; /* ── Documents (réutilise le système générique /documents, entity_type 'investissement_pe', * porté depuis InvestissementDetail.jsx en version simplifiée : bouton "+ Ajouter" direct * plutôt que menu ⋮, pas d'action "tout supprimer" pour cette première passe) ── */ const [docsErr, setDocsErr] = useState(null); const [uploadingDoc, setUploadingDoc] = useState(false); const [renamingDocId, setRenamingDocId] = useState(null); const [renameValue, setRenameValue] = useState(''); const [docDeleteTarget, setDocDeleteTarget] = useState(null); const docFileInputRef = useRef(null); const refreshDocuments = async () => { try { setDocuments(await api.get('/documents', { entity_type: 'investissement_pe', entity_id: id })); } catch { /* la fiche reste utilisable même si le rafraîchissement échoue */ } }; const handleDocFileChange = async (e) => { const file = e.target.files?.[0]; e.target.value = ''; if (!file) return; setUploadingDoc(true); setDocsErr(null); try { const fd = new FormData(); fd.append('file', file); fd.append('entity_type', 'investissement_pe'); fd.append('entity_id', id); fd.append('categorie', 'investissement_pe'); fd.append('nom_affichage', file.name.replace(/\.[^./\\]+$/, '')); await api.postForm('/documents', fd); await refreshDocuments(); } catch (e) { setDocsErr(e.message); } finally { setUploadingDoc(false); } }; const handleDocRenameStart = (doc) => { setRenamingDocId(doc.id); setRenameValue(doc.nom_affichage); }; const handleDocRenameCommit = async (docId) => { const value = renameValue.trim(); setRenamingDocId(null); const current = documents.find(d => d.id === docId); if (!value || !current || value === current.nom_affichage) return; try { await api.put(`/documents/${docId}`, { nom_affichage: value }); await refreshDocuments(); } catch (e) { setDocsErr(e.message); } }; const handleDocDownload = async (doc) => { try { const blob = await api.blob(`/documents/${doc.id}/download`); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${doc.nom_affichage}.${doc.extension}`; a.click(); URL.revokeObjectURL(url); } catch (e) { setDocsErr(e.message); } }; const handleDocDelete = async () => { if (!docDeleteTarget) return; try { await api.del(`/documents/${docDeleteTarget.id}`); setDocDeleteTarget(null); await refreshDocuments(); } catch (e) { setDocsErr(e.message); } }; if (loading) return
Chargement…
; if (!deal) return ( <>

Investissement introuvable

{err &&
{err}
} ); const totalFrais = fraisOps.reduce((s, f) => s + (f.montant || 0), 0); // ── Valorisation : détail de répartition (18/09/26, demande Olivier) ── // Formulaire "Ajouter une valorisation" : aperçu live de Capital non déployé / Valorisation // nette pendant la saisie, dès que Capital déployé est renseigné (mode "détaillé" — sinon // saisie simple d'un seul montant, comme avant). Capital non déployé n'est jamais saisi : // toujours déduit du montant investi du deal moins le capital déployé. const valFormHasBreakdown = valForm.capital_deploye !== ''; const valFormCapitalDeploye = valFormHasBreakdown ? (Number(valForm.capital_deploye) || 0) : 0; const valFormPlusValue = valFormHasBreakdown ? (Number(valForm.plus_value_latente) || 0) : 0; const valFormCapitalNonDeploye = valFormHasBreakdown ? Math.max(0, Number(deal.capital_investissable ?? deal.montant_investi) - valFormCapitalDeploye) : 0; const valFormComputedNette = valFormHasBreakdown ? Math.round((valFormCapitalDeploye + valFormPlusValue + valFormCapitalNonDeploye) * 100) / 100 : null; // Carte "Valorisation nette" (remplace l'ancienne tuile "Valorisation actuelle" du bloc // Résumé) : reprend la dernière valorisation connue (valorisations[0], déjà triée DESC par // l'API) et n'affiche la barre de répartition que si CETTE valorisation a le détail — sinon // retombe sur un simple montant, comme avant (cas des valorisations saisies en mode simple, ou // de celles antérieures à cette fonctionnalité). const latestVal = valorisations[0]; const latestValHasBreakdown = latestVal && latestVal.capital_deploye != null; const latestValTotal = latestVal ? latestVal.valorisation_nette : deal.montant_investi; const latestValCapitalDeploye = latestValHasBreakdown ? latestVal.capital_deploye : 0; const latestValPlusValue = latestValHasBreakdown ? (latestVal.plus_value_latente || 0) : 0; const latestValCapitalNonDeploye = latestValHasBreakdown ? Math.max(0, latestValTotal - latestValCapitalDeploye - latestValPlusValue) : 0; const valBarTotal = latestValCapitalDeploye + Math.max(0, latestValPlusValue) + latestValCapitalNonDeploye; const pctDeploye = valBarTotal > 0 ? (latestValCapitalDeploye / valBarTotal) * 100 : 0; const pctPlusValue = valBarTotal > 0 ? (Math.max(0, latestValPlusValue) / valBarTotal) * 100 : 0; // Carte "Montant souscrit" + colonnes Déploiement/Performance/Frais (18/09/26, reprise du // tableau de bord Fundora, demande Olivier — même disposition, même graphique, même tableau à // 3 colonnes). Frais d'entrée/de gestion "à la source" : mêmes lignes frais_operations que // celles pré-remplies dans "Modifier l'investissement" (openEditDeal ci-dessus). const fraisEntreeRow = fraisOps.find(f => f.categorie === 'souscription'); const fraisGestionRow = fraisOps.find(f => f.categorie === 'gestion' && f.origine === 'souscription'); const fraisEntreeMontant = fraisEntreeRow?.montant || 0; const fraisGestionMontant = fraisGestionRow?.montant || 0; const capitalInvestissable = Number(deal.capital_investissable ?? deal.montant_investi); const pctCapitalAuTravail = capitalInvestissable > 0 ? (latestValCapitalDeploye / capitalInvestissable) * 100 : 0; // TVPI "du sous-jacent" = multiple sur le seul capital déployé (contrairement au TVPI global, // qui rapporte à tout le montant souscrit) — dérivé de la plus-value latente saisie, pas un // champ à part : (capital déployé + plus-value latente) / capital déployé. const tvpiSousJacent = latestValHasBreakdown && latestValCapitalDeploye > 0 ? (latestValCapitalDeploye + latestValPlusValue) / latestValCapitalDeploye : null; // "Valeur totale cible" = montant souscrit × objectif, uniquement quand l'objectif est un // multiple simple ("x4,5") — objectif reste volontairement en texte libre côté deal (peut // aussi être une fourchette de taux, ex. "9-12 %/an" : pas de calcul possible dans ce cas, // affiche "—"). const objectifMultipleMatch = deal.objectif && deal.objectif.match(/^x\s*(\d+(?:[.,]\d+)?)/i); const valeurTotaleCible = objectifMultipleMatch ? deal.montant_investi * parseFloat(objectifMultipleMatch[1].replace(',', '.')) : null; // Barre "Montant souscrit" : mêmes 4 composantes que le camembert Fundora, mais rapportées au // montant souscrit BRUT (pas au capital investissable) puisque les frais en font partie. const pctFraisEntreeSouscrit = deal.montant_investi > 0 ? (fraisEntreeMontant / deal.montant_investi) * 100 : 0; const pctFraisGestionSouscrit = deal.montant_investi > 0 ? (fraisGestionMontant / deal.montant_investi) * 100 : 0; const pctDeployeSouscrit = deal.montant_investi > 0 ? (latestValCapitalDeploye / deal.montant_investi) * 100 : 0; // "Obligation" (18/09/26, demande Olivier) : sous-ensemble des distributions classées "intérêt // sur capital non déployé" (cf. distribution_nature dans RembFormModal.jsx), reconstituant la // ligne du même nom sur les captures Fundora — calculé par le backend (obligation_cumulee, // somme des remboursements de ce deal tagués ainsi), jamais une estimation/un taux fabriqué // localement. "Distributions" affiché ci-dessous devient donc le reste (capital + non classé), // les deux sommant toujours à distributions_cumulees (qui reste la base du calcul du TVPI, // inchangée — cf. GET /:id, investissementsPe.js). const obligationCumulee = deal.obligation_cumulee || 0; const distributionsCapital = (deal.distributions_cumulees || 0) - obligationCumulee; return ( <>

{deal.nom_deal}

{err &&
{err}
}
{/* ── Résumé ── */}
{deal.plateforme_nom} — {deal.investisseur_nom} · Souscrit le {fmtDate(deal.date_souscription)} {deal.strategie ? ` · ${deal.strategie}` : ''} {deal.objectif ? ` · Objectif : ${deal.objectif}` : ''} {deal.reference ? ` · Réf. ${deal.reference}` : ''} {deal.statut !== 'valide' ? ` · ${STATUT_LABELS[deal.statut] || deal.statut}` : ''}
{deal.notes &&
{deal.notes}
} {/* Tableau de bord "Montant souscrit / Valorisation nette" (18/09/26, reprise de la disposition des captures Fundora d'Olivier : 2 cartes avec barre de répartition, puis 3 colonnes Déploiement/Performance/Frais). N'affiche les graphiques que si la dernière valorisation a le détail de répartition (saisie optionnelle, formulaire "Valorisations" plus bas) — sinon retombe sur un résumé minimal. */}

1. Montant souscrit

Engagement total, décaissé en totalité à la souscription
{fmtEUR(deal.montant_investi)}

2. Valorisation nette

Capital déployé + plus-value latente + capital non déployé
{fmtEUR(deal.derniere_valorisation ?? deal.montant_investi)}
{latestValHasBreakdown ? ( <>
= 0 ? 'var(--success)' : 'var(--danger)' }} />
= 0 ? 'var(--success)' : 'var(--danger)'} label={latestValPlusValue >= 0 ? 'Plus-value latente' : 'Moins-value latente'} value={latestValPlusValue} />
) : (
{valorisations.length === 0 ? 'Aucune valorisation enregistrée.' : 'Pas de détail de répartition pour la dernière valorisation (saisie simple).'}
)}
Déploiement
Performance
Capital déployé + plus-value latente + capital non déployé {tvpiSousJacent != null && <>
TVPI du sous-jacent {fmtMultiple(tvpiSousJacent)}} } />
Frais
{/* ── Valorisations ── */}

Valorisations

{valErr &&
{valErr}
}
runValSubmit(submitVal, e)} className="form-grid" style={{ marginBottom: 16 }}>
setValForm({ ...valForm, date_valorisation: e.target.value })} />
setValForm({ ...valForm, valorisation_nette: e.target.value })} />
setValForm({ ...valForm, capital_deploye: e.target.value })} />
{valFormHasBreakdown && (
setValForm({ ...valForm, plus_value_latente: e.target.value })} />
)}
setValForm({ ...valForm, notes: e.target.value })} />
{valorisations.length === 0 && ( )} {valorisations.map(v => ( ))}
DateValorisation netteNotes
Aucune valorisation enregistrée
{fmtDate(v.date_valorisation)} {fmtEUR(v.valorisation_nette)} {v.capital_deploye != null && (
{fmtEUR(v.capital_deploye)} déployé · {(v.plus_value_latente || 0) >= 0 ? '+' : ''}{fmtEUR(v.plus_value_latente || 0)}
)}
{v.notes || '—'}
{/* ── Remboursements ── */}

Remboursements

{remboursements.length === 0 && ( )} {remboursements.map(r => ( navigate('/remboursements')} title="Voir dans Remboursements"> ))}
DateMontant verséNotes
Aucun remboursement enregistré pour ce deal
{fmtDate(r.date_remb)} {fmtEUR(r.net_recu)} {r.notes || '—'}
{/* ── Frais d'opération ── */}

Frais d'opération {fraisOps.length > 0 && Total : {fmtEUR(totalFrais)}}

{fraisOps.length === 0 ? (
Aucun frais journalisé pour l'instant.
) : (
{fraisOps.map(f => { const isManaged = f.categorie === 'souscription' || (f.categorie === 'gestion' && f.origine === 'souscription'); return (
isManaged ? openEditDeal() : openEditFrais(f)} title={isManaged ? "Modifiable depuis « Modifier l'investissement »" : 'Cliquer pour modifier'}>
{fmtEUR(f.montant)} {FRAIS_MODE_LABELS[f.mode_reglement] || f.mode_reglement} {f.mode_reglement === 'compte_courant' && f.compte_nom ? ` (${f.compte_nom})` : ''}
{fmtDate(f.date_operation)} · {FRAIS_CATEGORIE_LABELS[f.categorie] || f.categorie} {f.notes ? ` · ${f.notes}` : ''}
{!isManaged && ( )}
); })}
)}
{/* ── Documents ── */}

Mes documents

{docsErr &&
{docsErr}
} {documents.length === 0 ? (
Aucun document pour l'instant.
) : (
{documents.map(doc => (
{renamingDocId === doc.id ? ( setRenameValue(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleDocRenameCommit(doc.id); if (e.key === 'Escape') setRenamingDocId(null); }} onBlur={() => handleDocRenameCommit(doc.id)} style={{ width: '100%', fontSize: 'var(--fs-sm)' }} /> ) : (
handleDocRenameStart(doc)} title="Cliquer pour renommer" style={{ fontWeight: 500, fontSize: 'var(--fs-sm)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer' }}> {doc.nom_affichage}.{doc.extension}
)}
{fmtOctets(doc.taille_octets)} · {fmtDate(doc.created_at)}
))}
)}
p.investisseur_id)).size > 1} comptesInvestisseur={comptesInvestisseur} err={formErr} submitting={submitting} onSubmit={() => runSubmit(submitDeal)} onDelete={() => { closeEditDeal(); setDeleteConfirm(true); }} investisseurForPlat={investisseurForPlat} /> setDeleteConfirm(false)} /> setValDeleteTarget(null)} /> setDocDeleteTarget(null)} /> setFraisForm(f => ({ ...f, [k]: v }))} err={fraisErr} onSubmit={e => runFraisSubmit(submitFrais, e)} onDelete={() => runFraisSubmit(onDeleteFrais)} submitting={fraisSubmitting} comptesInvestisseur={comptesInvestisseur} locked investissementOptions={[]} confirmingDelete={fraisConfirmingDelete} setConfirmingDelete={setFraisConfirmingDelete} /> ); }