This commit is contained in:
ocroguennec committed 2026-09-18 10:35:43 +02:00
1 parent 4cd4cbce71
commit 27c4c57a58
6 files changed
+243 -49

No files matched your search

+17
View File
@@ -3409,4 +3409,21 @@ if (!invPeCols.includes('capital_investissable')) {
}
db.exec('CREATE INDEX IF NOT EXISTS idx_remb_invpe ON remboursements(investissement_pe_id)');
// Migration : nature d'une distribution PE (18/09/26, demande Olivier — distinguer, au sein des
// distributions reçues sur un deal PE, ce qui restitue le capital déployé ('capital') de ce qui
// est un revenu d'intérêt versé sur le capital NON déployé, placé par le fonds sur un support
// obligataire en attendant d'être appelé ('interet_non_deploye') — reprend la ligne "Obligation"
// des captures Fundora, jusque-là non reconstituable faute de cette distinction. Nullable et
// sans CHECK (validé côté Zod, cf. remboursements.js) : une distribution non classée n'est ni
// l'un ni l'autre, elle reste comptée dans "Distributions" par défaut (cf.
// investissementsPe.js::GET / et GET /:id). Sans effet sur les remboursements crowdlending
// (type 'normal') ni sur les bonus (parrainage/plateforme), seul le type 'interets_plateforme'
// (rebaptisé "Distribution" côté PE) l'utilise.
{
const rembCols3 = db.prepare('PRAGMA table_info(remboursements)').all().map(c => c.name);
if (!rembCols3.includes('distribution_nature')) {
db.exec('ALTER TABLE remboursements ADD COLUMN distribution_nature TEXT');
}
}
export default db;
+16 -2
View File
@@ -110,7 +110,14 @@ router.get('/', (req, res) => {
WHERE v.investissement_pe_id = ipe.id
ORDER BY v.date_valorisation DESC, v.id DESC LIMIT 1) AS derniere_valorisation_date,
COALESCE((SELECT SUM(r.net_recu) FROM remboursements r
WHERE r.investissement_pe_id = ipe.id), 0) AS distributions_cumulees
WHERE r.investissement_pe_id = ipe.id), 0) AS distributions_cumulees,
-- Sous-ensemble des distributions ci-dessus classées "intérêt sur capital non déployé"
-- (18/09/26, demande Olivier — reconstitue la ligne "Obligation" des captures Fundora,
-- distincte de la restitution de capital déployé). Inclus dans distributions_cumulees ET
-- dans le TVPI (valeur réellement perçue), juste isolé ici pour l'affichage détaillé.
COALESCE((SELECT SUM(r.net_recu) FROM remboursements r
WHERE r.investissement_pe_id = ipe.id AND r.distribution_nature = 'interet_non_deploye'), 0)
AS obligation_cumulee
FROM investissements_pe ipe
JOIN plateformes p ON p.id = ipe.plateforme_id
JOIN investisseurs inv ON inv.id = ipe.investisseur_id
@@ -212,7 +219,14 @@ router.get('/:id', (req, res, next) => {
WHERE v.investissement_pe_id = ipe.id
ORDER BY v.date_valorisation DESC, v.id DESC LIMIT 1) AS derniere_valorisation_date,
COALESCE((SELECT SUM(r.net_recu) FROM remboursements r
WHERE r.investissement_pe_id = ipe.id), 0) AS distributions_cumulees
WHERE r.investissement_pe_id = ipe.id), 0) AS distributions_cumulees,
-- Sous-ensemble des distributions ci-dessus classées "intérêt sur capital non déployé"
-- (18/09/26, demande Olivier — reconstitue la ligne "Obligation" des captures Fundora,
-- distincte de la restitution de capital déployé). Inclus dans distributions_cumulees ET
-- dans le TVPI (valeur réellement perçue), juste isolé ici pour l'affichage détaillé.
COALESCE((SELECT SUM(r.net_recu) FROM remboursements r
WHERE r.investissement_pe_id = ipe.id AND r.distribution_nature = 'interet_non_deploye'), 0)
AS obligation_cumulee
FROM investissements_pe ipe
JOIN plateformes p ON p.id = ipe.plateforme_id
JOIN investisseurs inv ON inv.id = ipe.investisseur_id
+13 -5
View File
@@ -20,6 +20,12 @@ const Schema = z.object({
// plateforme (ex. Fundora) — cf. project_workspaces_transformation.md.
// Sans effet sur le type 'normal' (crowdlending).
investissement_pe_id: z.number().int().positive().nullable().optional(),
// Nature de la distribution pour un deal PE (18/09/26, demande Olivier) : 'capital' = restitution
// du capital déployé, 'interet_non_deploye' = intérêt versé sur le capital NON déployé (support
// obligataire du fonds) — cf. migration distribution_nature dans db/index.js. Optionnel : une
// distribution non classée reste comptée dans "Distributions" (pas "Obligation") côté PE. Sans
// effet hors type 'interets_plateforme'.
distribution_nature: z.enum(['capital', 'interet_non_deploye']).nullable().optional(),
date_remb: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
capital: z.number().nonnegative().default(0),
cashback: z.number().nonnegative().default(0),
@@ -337,14 +343,15 @@ router.post('/', (req, res, next) => {
INSERT INTO remboursements
(type, bonus_plateforme_id, bonus_investisseur_id, date_remb,
capital, cashback, interets_bruts_avant_local, taxe_locale, interets_bruts, frais_ttc,
prelev_sociaux, prelev_forfaitaire, interets_nets, net_recu, statut, source, notes, workspace_id, investissement_pe_id)
VALUES ('interets_plateforme',?,?,?, 0,0,?,?,?,?,?,?, ?,?, ?, 'manuel', ?, ?, ?)
prelev_sociaux, prelev_forfaitaire, interets_nets, net_recu, statut, source, notes, workspace_id,
investissement_pe_id, distribution_nature)
VALUES ('interets_plateforme',?,?,?, 0,0,?,?,?,?,?,?, ?,?, ?, 'manuel', ?, ?, ?, ?)
`).run(
body.bonus_plateforme_id, bonusInvestisseurId, body.date_remb,
body.interets_bruts_avant_local, body.taxe_locale, body.interets_bruts, body.frais_ttc,
body.prelev_sociaux, body.prelev_forfaitaire,
interets_nets, net_recu, body.statut, body.notes || null,
resolveActiveWorkspaceId(req), body.investissement_pe_id ?? null,
resolveActiveWorkspaceId(req), body.investissement_pe_id ?? null, body.distribution_nature ?? null,
);
return res.status(201).json({ id: r.lastInsertRowid, ...body, interets_nets, net_recu });
}
@@ -513,12 +520,13 @@ router.put('/:id', (req, res, next) => {
UPDATE remboursements
SET date_remb=?, interets_bruts_avant_local=?, taxe_locale=?, interets_bruts=?, frais_ttc=?,
prelev_sociaux=?, prelev_forfaitaire=?, interets_nets=?, net_recu=?, statut=?, notes=?,
investissement_pe_id=?
investissement_pe_id=?, distribution_nature=?
WHERE id=?
`).run(
body.date_remb, body.interets_bruts_avant_local, body.taxe_locale, body.interets_bruts, body.frais_ttc,
body.prelev_sociaux, body.prelev_forfaitaire,
interets_nets, net_recu, body.statut, body.notes || null, body.investissement_pe_id ?? null, id,
interets_nets, net_recu, body.statut, body.notes || null, body.investissement_pe_id ?? null,
body.distribution_nature ?? null, id,
);
} else {
const bonusInvestisseurId = body.bonus_investisseur_id ?? existing.bonus_investisseur_id;
+10
View File
@@ -207,6 +207,16 @@ export default function RembFormModal({
</select>
</div>
)}
{!locked && isInteretsPlateforme && dealOptions.length > 0 && (
<div>
<label>Nature de la distribution</label>
<select value={form.distribution_nature || ''} onChange={e => onFieldChange('distribution_nature', e.target.value)}>
<option value="">— Non précisé —</option>
<option value="capital">Restitution de capital déployé</option>
<option value="interet_non_deploye">Intérêt sur capital non déployé</option>
</select>
</div>
)}
{!locked && isBonus && (
<div>
<label>Date *</label>
+167 -41
View File
@@ -55,6 +55,34 @@ const FRAIS_MODE_LABELS = {
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 (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
<span style={{
width: 9, height: 9, borderRadius: '50%', background: color,
border: outline ? '1px solid var(--border)' : 'none', display: 'inline-block',
}} />
{label} <b>{fmtEUR(value)}</b>
</span>
);
}
function StatRow({ label, value, caption, valueColor }) {
return (
<div style={{ marginBottom: 14 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
<div style={{ fontWeight: 600, fontSize: 13 }}>{label}</div>
<div style={{ fontWeight: 700, fontSize: 14, color: valueColor, whiteSpace: 'nowrap' }}>{value}</div>
</div>
{caption && <div className="text-muted" style={{ fontSize: 11, marginTop: 2 }}>{caption}</div>}
</div>
);
}
export default function InvestissementPeDetail() {
const { id } = useParams();
const navigate = useNavigate();
@@ -431,6 +459,50 @@ export default function InvestissementPeDetail() {
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 (
<>
<div className="topbar">
@@ -446,30 +518,46 @@ export default function InvestissementPeDetail() {
<div style={{ padding: '0 24px' }}>
{/* ── Résumé ── */}
<div className="card" style={{ marginBottom: 16 }}>
<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>
{deal.capital_investissable != null && deal.capital_investissable !== deal.montant_investi && (
<div className="text-muted" style={{ fontSize: 11, marginTop: 2 }}>
dont {fmtEUR(deal.capital_investissable)} investissables (après frais à la source)
<div className="text-muted" style={{ fontSize: 13, marginBottom: 12 }}>
{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}` : ''}
</div>
{deal.notes && <div className="text-muted" style={{ marginBottom: 12, fontSize: 13 }}>{deal.notes}</div>}
{/* 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. */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 16 }}>
<div className="card">
<h4 style={{ margin: '0 0 2px' }}>1. Montant souscrit</h4>
<div className="text-muted" style={{ fontSize: 12, marginBottom: 10 }}>
Engagement total, décaissé en totalité à la souscription
</div>
<div style={{ fontSize: '1.5rem', fontWeight: 700, marginBottom: 10 }}>{fmtEUR(deal.montant_investi)}</div>
<div style={{ display: 'flex', height: 8, borderRadius: 4, overflow: 'hidden', background: 'var(--surface-2)' }}>
<div style={{ width: `${pctFraisEntreeSouscrit}%`, background: 'var(--text)' }} />
<div style={{ width: `${pctFraisGestionSouscrit}%`, background: 'var(--text)' }} />
<div style={{ width: `${pctDeployeSouscrit}%`, background: 'var(--primary)' }} />
</div>
<div style={{ display: 'flex', gap: 16, marginTop: 10, fontSize: 12, flexWrap: 'wrap' }}>
<LegendDot color="var(--text)" label="Frais d'entrée" value={fraisEntreeMontant} />
<LegendDot color="var(--text)" label="Frais de gestion" value={fraisGestionMontant} />
<LegendDot color="var(--primary)" label="Capital déployé" value={latestValCapitalDeploye} />
<LegendDot color="var(--surface-2)" outline label="Capital non déployé" value={latestValCapitalNonDeploye} />
</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>
<div style={{ fontSize: '1.2rem', fontWeight: 700, marginTop: 4 }}>{fmtEUR(deal.distributions_cumulees)}</div></div>
<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 }}>
<div className="card">
<h4 style={{ margin: '0 0 2px' }}>2. Valorisation nette</h4>
<div className="text-muted" style={{ fontSize: 12, marginBottom: 10 }}>
Capital déployé + plus-value latente + capital non déployé
</div>
<div style={{ fontSize: '1.5rem', fontWeight: 700, marginBottom: 10 }}>
{fmtEUR(deal.derniere_valorisation ?? deal.montant_investi)}
</div>
{latestValHasBreakdown ? (
@@ -478,19 +566,14 @@ export default function InvestissementPeDetail() {
<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 style={{ display: 'flex', gap: 16, marginTop: 10, fontSize: 12, flexWrap: 'wrap' }}>
<LegendDot color="var(--primary)" label="Capital déployé" value={latestValCapitalDeploye} />
<LegendDot
color={latestValPlusValue >= 0 ? 'var(--success)' : 'var(--danger)'}
label={latestValPlusValue >= 0 ? 'Plus-value latente' : 'Moins-value latente'}
value={latestValPlusValue}
/>
<LegendDot color="var(--surface-2)" outline label="Capital non déployé" value={latestValCapitalNonDeploye} />
</div>
</>
) : (
@@ -501,14 +584,57 @@ export default function InvestissementPeDetail() {
</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}` : ''}
{deal.objectif ? ` · Objectif : ${deal.objectif}` : ''}
{deal.reference ? ` · Réf. ${deal.reference}` : ''}
</div>
{deal.notes && <div className="text-muted" style={{ marginTop: 8, fontSize: 13 }}>{deal.notes}</div>}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16, marginBottom: 16 }}>
<div className="card">
<div style={{ fontSize: 'var(--fs-xs)', textTransform: 'uppercase', letterSpacing: '.06em', color: 'var(--text-muted)', marginBottom: 12 }}>Déploiement</div>
<StatRow
label="Pourcentage du capital au travail"
value={`${pctCapitalAuTravail.toFixed(1).replace('.', ',')} %`}
caption="Pourcentage appelé par le fonds"
/>
<StatRow
label="Capital déployé"
value={fmtEUR(latestValCapitalDeploye)}
caption="(Montant souscrit − frais) × % d'appels de fonds"
/>
<StatRow
label="Capital non déployé"
value={fmtEUR(latestValCapitalNonDeploye)}
caption="(Montant souscrit − frais) × (1 − % d'appels de fonds)"
/>
</div>
<div className="card">
<div style={{ fontSize: 'var(--fs-xs)', textTransform: 'uppercase', letterSpacing: '.06em', color: 'var(--text-muted)', marginBottom: 12 }}>Performance</div>
<StatRow
label="Valorisation nette"
value={fmtEUR(deal.derniere_valorisation ?? deal.montant_investi)}
valueColor="var(--success)"
caption={<>
Capital déployé + plus-value latente + capital non déployé
{tvpiSousJacent != null && <><br />TVPI du sous-jacent {fmtMultiple(tvpiSousJacent)}</>}
</>}
/>
<StatRow label="TVPI" value={fmtMultiple(deal.tvpi)} caption="Valorisation nette / montant souscrit" />
<StatRow label="Distributions" value={fmtEUR(distributionsCapital)} caption="Restitution de capital déployé, remboursements non classés inclus" />
<StatRow label="Obligation" value={fmtEUR(obligationCumulee)} caption="Intérêt versé sur le capital non déployé (remboursements classés ainsi)" />
<StatRow
label="Valeur totale cible"
value={valeurTotaleCible != null ? fmtEUR(valeurTotaleCible) : '—'}
caption="Montant souscrit × objectif de l'opportunité"
/>
</div>
<div className="card">
<div style={{ fontSize: 'var(--fs-xs)', textTransform: 'uppercase', letterSpacing: '.06em', color: 'var(--text-muted)', marginBottom: 12 }}>Frais</div>
<StatRow
label="Frais totaux"
value={fmtEUR(fraisEntreeMontant + fraisGestionMontant)}
caption={`Frais d'entrée ${fmtEUR(fraisEntreeMontant)} · Frais de gestion ${fmtEUR(fraisGestionMontant)}`}
/>
</div>
</div>
{/* ── Valorisations ── */}
+20 -1
View File
@@ -21,6 +21,12 @@ const TYPE_LABELS = {
interets_plateforme: 'Distribution',
};
// Nature de la distribution (18/09/26) — affichée en sous-texte sous le type dans le tableau.
const DISTRIBUTION_NATURE_LABELS = {
capital: 'Restitution de capital',
interet_non_deploye: 'Intérêt sur capital non déployé',
};
const MOIS_FR = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'];
const MOIS_ABBR = ['Jan','Fév','Mar','Avr','Mai','Juin','Juil','Août','Sep','Oct','Nov','Déc'];
@@ -164,6 +170,10 @@ const emptyRembForm = {
// rattacher une distribution à un investissement précis pour calculer son
// TVPI, quand plusieurs deals partagent une même plateforme (ex. Fundora).
investissement_pe_id: '',
// Nature de la distribution (18/09/26, demande Olivier) : distingue restitution de capital
// déployé vs. intérêt sur capital non déployé — cf. RembFormModal.jsx. Optionnel, uniquement
// pertinent pour le type "Distribution" (interets_plateforme).
distribution_nature: '',
date_remb: today(),
statut: 'paye',
notes: '',
@@ -338,6 +348,7 @@ export default function RemboursementsSimple() {
bonus_plateforme_id: String(r.bonus_plateforme_id || ''),
bonus_investisseur_id: String(r.bonus_investisseur_id || ''),
investissement_pe_id: r.investissement_pe_id ? String(r.investissement_pe_id) : '',
distribution_nature: r.distribution_nature || '',
date_remb: r.date_remb,
statut: r.statut,
notes: r.notes || '',
@@ -419,6 +430,7 @@ export default function RemboursementsSimple() {
bonus_plateforme_id: Number(form.bonus_plateforme_id),
bonus_investisseur_id: Number(form.bonus_investisseur_id) || undefined,
investissement_pe_id: Number(form.investissement_pe_id) || undefined,
distribution_nature: (!isBonusForm && form.distribution_nature) ? form.distribution_nature : undefined,
date_remb: form.date_remb,
statut: form.statut,
notes: form.notes || undefined,
@@ -840,7 +852,14 @@ export default function RemboursementsSimple() {
<td>{fmtDate(r.date_remb)}</td>
<td>{r.plateforme_nom}</td>
{multiDetenteur && <td className="text-muted">{r.plateforme_detenteur_nom || '—'}</td>}
<td>{TYPE_LABELS[r.type] || r.type}</td>
<td>
{TYPE_LABELS[r.type] || r.type}
{r.distribution_nature && (
<div className="text-muted" style={{ fontSize: 11 }}>
{DISTRIBUTION_NATURE_LABELS[r.distribution_nature] || r.distribution_nature}
</div>
)}
</td>
<td className="num">{fmtEUR(r.net_recu)}</td>
<td className="text-muted">{statutLabel(r.statut)}</td>
<td onClick={e => e.stopPropagation()}>