This commit is contained in:
ocroguennec committed 2026-09-18 08:45:15 +02:00
1 parent 337d0c1a2c
commit d5792fb3e2
3 files changed
+163 -11

No files matched your search

+113 -6
View File
@@ -52,7 +52,7 @@ const FRAIS_MODE_LABELS = {
remboursement: 'Retenu sur remboursement',
};
const emptyValForm = { date_valorisation: today(), valorisation_nette: '', notes: '' };
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: '' };
export default function InvestissementPeDetail() {
@@ -243,9 +243,21 @@ export default function InvestissementPeDetail() {
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;
const valorisationNette = hasBreakdown
? Math.round((capitalDeploye + plusValueLatente + Math.max(0, Number(deal.montant_investi) - capitalDeploye)) * 100) / 100
: Number(valForm.valorisation_nette);
await api.post(`/investissements-pe/${id}/valorisations`, {
date_valorisation: valForm.date_valorisation,
valorisation_nette: Number(valForm.valorisation_nette),
valorisation_nette: valorisationNette,
capital_deploye: capitalDeploye,
plus_value_latente: plusValueLatente,
notes: valForm.notes || undefined,
});
setValForm(emptyValForm);
@@ -381,6 +393,36 @@ export default function InvestissementPeDetail() {
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.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;
return (
<>
<div className="topbar">
@@ -400,8 +442,6 @@ export default function InvestissementPeDetail() {
<div className="dr-kpi-row" style={{ marginBottom: 0 }}>
<div className="kpi"><div className="label">Montant investi</div>
<div style={{ fontSize: '1.2rem', fontWeight: 700, marginTop: 4 }}>{fmtEUR(deal.montant_investi)}</div></div>
<div className="kpi"><div className="label">Valorisation actuelle</div>
<div style={{ fontSize: '1.2rem', fontWeight: 700, marginTop: 4 }}>{fmtEUR(deal.derniere_valorisation ?? deal.montant_investi)}</div></div>
<div className="kpi"><div className="label">TVPI</div>
<div style={{ fontSize: '1.2rem', fontWeight: 700, marginTop: 4 }}>{fmtMultiple(deal.tvpi)}</div></div>
<div className="kpi"><div className="label">Distributions cumulées</div>
@@ -409,6 +449,46 @@ export default function InvestissementPeDetail() {
<div className="kpi"><div className="label">Statut</div>
<div style={{ fontSize: '1.2rem', fontWeight: 700, marginTop: 4 }}>{STATUT_LABELS[deal.statut] || deal.statut}</div></div>
</div>
{/* Valorisation nette — remplace l'ancienne tuile "Valorisation actuelle" (18/09/26,
demande Olivier : reprendre l'esprit des captures Fundora — répartition capital
déployé / plus-value latente / capital non déployé). Barre affichée seulement quand
la dernière valorisation a ce détail (saisie optionnelle, formulaire ci-dessous). */}
<div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid var(--border)' }}>
<div className="label">Valorisation nette</div>
<div style={{ fontSize: '1.3rem', fontWeight: 700, marginTop: 4, marginBottom: latestValHasBreakdown ? 10 : 4 }}>
{fmtEUR(deal.derniere_valorisation ?? deal.montant_investi)}
</div>
{latestValHasBreakdown ? (
<>
<div style={{ display: 'flex', height: 8, borderRadius: 4, overflow: 'hidden', background: 'var(--surface-2)' }}>
<div style={{ width: `${pctDeploye}%`, background: 'var(--primary)' }} />
<div style={{ width: `${pctPlusValue}%`, background: latestValPlusValue >= 0 ? 'var(--success)' : 'var(--danger)' }} />
</div>
<div style={{ display: 'flex', gap: 20, marginTop: 10, fontSize: 13, flexWrap: 'wrap' }}>
<span>
<span style={{ display: 'inline-block', width: 9, height: 9, borderRadius: '50%', background: 'var(--primary)', marginRight: 6 }} />
Capital déployé <b>{fmtEUR(latestValCapitalDeploye)}</b>
</span>
<span>
<span style={{ display: 'inline-block', width: 9, height: 9, borderRadius: '50%', background: latestValPlusValue >= 0 ? 'var(--success)' : 'var(--danger)', marginRight: 6 }} />
{latestValPlusValue >= 0 ? 'Plus-value latente' : 'Moins-value latente'} <b>{fmtEUR(latestValPlusValue)}</b>
</span>
<span>
<span style={{ display: 'inline-block', width: 9, height: 9, borderRadius: '50%', background: 'var(--surface-2)', border: '1px solid var(--border)', marginRight: 6 }} />
Capital non déployé <b>{fmtEUR(latestValCapitalNonDeploye)}</b>
</span>
</div>
</>
) : (
<div className="text-muted" style={{ fontSize: 13 }}>
{valorisations.length === 0
? 'Aucune valorisation enregistrée.'
: 'Pas de détail de répartition pour la dernière valorisation (saisie simple).'}
</div>
)}
</div>
<div className="text-muted" style={{ marginTop: 14, fontSize: 13 }}>
{deal.plateforme_nom} — {deal.investisseur_nom} · Souscrit le {fmtDate(deal.date_souscription)}
{deal.strategie ? ` · ${deal.strategie}` : ''}
@@ -429,8 +509,28 @@ export default function InvestissementPeDetail() {
</div>
<div>
<label>Valorisation nette (€) *</label>
<input type="number" step="0.01" min="0" required value={valForm.valorisation_nette} onChange={e => setValForm({ ...valForm, valorisation_nette: e.target.value })} />
<input type="number" step="0.01" min="0" required disabled={valFormHasBreakdown}
value={valFormHasBreakdown ? valFormComputedNette : valForm.valorisation_nette}
onChange={e => setValForm({ ...valForm, valorisation_nette: e.target.value })} />
</div>
<div>
<label>Capital déployé (€)</label>
<input type="number" step="0.01" min="0" placeholder="optionnel — pour le détail de répartition"
value={valForm.capital_deploye}
onChange={e => setValForm({ ...valForm, capital_deploye: e.target.value })} />
</div>
<div>
<label>Capital non déployé (€)</label>
<input type="text" disabled value={valFormHasBreakdown ? fmtEUR(valFormCapitalNonDeploye) : '—'} />
</div>
{valFormHasBreakdown && (
<div style={{ gridColumn: '1 / -1' }}>
<label>Plus-value latente (€)</label>
<input type="number" step="0.01" placeholder="négatif si moins-value"
value={valForm.plus_value_latente}
onChange={e => setValForm({ ...valForm, plus_value_latente: e.target.value })} />
</div>
)}
<div style={{ gridColumn: '1 / -1' }}>
<label>Notes</label>
<input value={valForm.notes} onChange={e => setValForm({ ...valForm, notes: e.target.value })} />
@@ -450,7 +550,14 @@ export default function InvestissementPeDetail() {
{valorisations.map(v => (
<tr key={v.id}>
<td>{fmtDate(v.date_valorisation)}</td>
<td className="num">{fmtEUR(v.valorisation_nette)}</td>
<td className="num">
{fmtEUR(v.valorisation_nette)}
{v.capital_deploye != null && (
<div className="text-muted" style={{ fontSize: 11, fontWeight: 400 }}>
{fmtEUR(v.capital_deploye)} déployé · {(v.plus_value_latente || 0) >= 0 ? '+' : ''}{fmtEUR(v.plus_value_latente || 0)}
</div>
)}
</td>
<td className="text-muted">{v.notes || '—'}</td>
<td><button className="icon-btn" title="Supprimer" onClick={() => setValDeleteTarget(v)}>✕</button></td>
</tr>