This commit is contained in:
ocroguennec committed 2026-09-18 07:48:55 +02:00
1 parent 56d52d538e
commit 0df335803e
2 files changed
+176 -286

No files matched your search

+152 -284
View File
@@ -3,7 +3,7 @@ import { useLocation, useNavigate } 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 RembFormModal from '../components/RembFormModal.jsx';
import ConfirmModal from '../components/ConfirmModal.jsx';
import EmptyState from '../components/EmptyState.jsx';
import PageIcon from '../components/PageIcon.jsx';
@@ -132,8 +132,32 @@ function ExportDropdown({ disabled, onCSV, onXLS, onJSON }) {
);
}
// Sélecteur du haut de RembFormModal (17/09/26, cf. InvestissementSelect.jsx) : mêmes
// 3 sentinelles que Remboursements.jsx crowdlending, en lieu et place d'un champ `type`
// dédié — permet de réutiliser RembFormModal tel quel (même formulaire, même gestion de
// la fiscalité) plutôt que de dupliquer sa logique, à la demande d'Olivier ("la même
// chose que pour le crowdlending").
const BONUS_VALUES = ['BONUS_PARRAINAGE', 'BONUS_PLATEFORME'];
const INTERETS_PLATEFORME_VALUE = 'INTERETS_PLATEFORME';
const BONUS_TYPE_MAP = { BONUS_PARRAINAGE: 'bonus_parrainage', BONUS_PLATEFORME: 'bonus_plateforme' };
const TYPE_TO_SENTINEL = {
bonus_parrainage: 'BONUS_PARRAINAGE',
bonus_plateforme: 'BONUS_PLATEFORME',
interets_plateforme: INTERETS_PLATEFORME_VALUE,
};
// Helpers purs pour le calcul fiscal (mêmes formules que RembFormModal.jsx / Remboursements.jsx
// crowdlending), dupliqués volontairement (pas d'état) : servent au setField() de cette page.
const round2 = (n) => Math.round(n * 100) / 100;
const getRatesForYearPe = (dateStr, pfuRates) => {
if (!dateStr || !pfuRates?.length) return null;
const year = parseInt(dateStr.slice(0, 4), 10);
return pfuRates.find(r => r.annee === year)
?? pfuRates.reduce((best, r) => r.annee > best.annee ? r : best, pfuRates[0]);
};
const emptyRembForm = {
type: 'bonus_plateforme',
investissement_id: 'BONUS_PLATEFORME',
bonus_plateforme_id: '',
bonus_investisseur_id: '',
// Lien optionnel vers un deal PE (investissements_pe, 17/09/26) : permet de
@@ -175,7 +199,7 @@ const emptyRembForm = {
* X-Workspace-Id (cf. workspaceScope.js, Étape 2a/2c du 17/09/26).
*/
export default function RemboursementsSimple() {
const { activeId: investisseurId, investisseurs } = useInvestisseur();
const { activeId: investisseurId } = useInvestisseur();
const { active: activeWorkspace } = useWorkspace() || {};
const location = useLocation();
const navigate = useNavigate();
@@ -185,7 +209,8 @@ export default function RemboursementsSimple() {
const [deals, setDeals] = useState([]);
// Taux PFU par année (17/09/26, cf. project ask Olivier : aligner la gestion de la
// fiscalité PE sur celle de Remboursements.jsx crowdlending) — sert au calcul
// automatique des prélèvements sociaux/forfaitaire, cf. RemboursementForm ci-dessous.
// automatique des prélèvements sociaux/forfaitaire dans setField() ci-dessous, passé
// à RembFormModal (composant partagé avec Remboursements.jsx crowdlending).
const [pfuRates, setPfuRates] = useState([]);
const [loading, setLoading] = useState(true);
const [err, setErr] = useState(null);
@@ -194,6 +219,10 @@ export default function RemboursementsSimple() {
const [editingId, setEditingId] = useState(null);
const [form, setForm] = useState(emptyRembForm);
const [deleteTarget, setDeleteTarget] = useState(null);
// Confirmation de suppression inline dans RembFormModal (bouton "Supprimer" du
// formulaire lui-même) — distinct de deleteTarget/ConfirmModal ci-dessus, qui reste
// le flux de suppression depuis l'icône ✕ d'une ligne du tableau.
const [confirmingDelete, setConfirmingDelete] = useState(false);
const { run: runSubmit, pending: submitting } = useAsyncAction();
// Plateformes éligibles au workspace actif uniquement (GET /plateformes
@@ -258,12 +287,13 @@ export default function RemboursementsSimple() {
const firstId = deal ? deal.plateforme_id : (plats[0]?.id || '');
setForm({
...emptyRembForm,
...(deal ? { type: 'interets_plateforme' } : {}),
investissement_id: deal ? INTERETS_PLATEFORME_VALUE : emptyRembForm.investissement_id,
bonus_plateforme_id: firstId,
bonus_investisseur_id: investisseurForPlat(firstId),
investissement_pe_id: deal ? String(deal.id) : '',
});
setErr(null);
setConfirmingDelete(false);
setModalOpen(true);
};
@@ -304,7 +334,7 @@ export default function RemboursementsSimple() {
const openEdit = (r) => {
setEditingId(r.id);
setForm({
type: r.type,
investissement_id: TYPE_TO_SENTINEL[r.type] || INTERETS_PLATEFORME_VALUE,
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) : '',
@@ -320,15 +350,72 @@ export default function RemboursementsSimple() {
prelev_forfaitaire: String(r.prelev_forfaitaire ?? ''),
});
setErr(null);
setConfirmingDelete(false);
setModalOpen(true);
};
const close = () => { setModalOpen(false); setEditingId(null); setForm(emptyRembForm); setErr(null); };
const close = () => {
setModalOpen(false); setEditingId(null); setForm(emptyRembForm); setErr(null); setConfirmingDelete(false);
};
// Recalcule automatiquement taxe locale / prélèvements PFU à chaque changement d'un
// champ "moteur" — même logique que setField() dans Remboursements.jsx crowdlending
// (cf. RembFormModal.jsx, qui appelle onFieldChange pour ces mêmes clés). L'utilisateur
// garde la main pour corriger prelev_sociaux/prelev_forfaitaire ensuite : ces deux champs
// ne redéclenchent pas ce recalcul (passent par onFormPatch, pas par ici).
const setField = (key, value) => {
// Le sélecteur "Type *" (InvestissementSelect, partagé avec Remboursements.jsx
// crowdlending) inclut toujours une option vide "—" — pertinente côté crowdlending
// (état initial "rien choisi") mais jamais côté PE, qui n'a que 3 types valides et
// aucun état "aucun type" à gérer. On ignore ce choix plutôt que de laisser le
// formulaire basculer sur la branche "remboursement normal" de RembFormModal
// (Capital/Méthode de remboursement...), sans rapport avec un deal PE.
if (key === 'investissement_id' && value === '') return;
const next = { ...form, [key]: value };
if (key === 'interets_bruts_avant_local') {
const plat = plats.find(p => String(p.id) === String(next.bonus_plateforme_id));
const taux = plat?.fiscalite === 'avec_fiscalite_locale' ? (plat?.taux_fiscalite_locale || 0) : 0;
next.taxe_locale = round2(Number(value) * taux / 100);
next.interets_bruts = round2(Number(value) - next.taxe_locale);
}
if (key === 'taxe_locale') {
next.interets_bruts = round2(Number(next.interets_bruts_avant_local || 0) - Number(value));
}
if (['date_remb', 'interets_bruts', 'interets_bruts_avant_local', 'taxe_locale', 'frais_ttc'].includes(key)) {
const dateStr = key === 'date_remb' ? value : next.date_remb;
const base = round2((Number(next.interets_bruts) || 0) - (Number(next.frais_ttc) || 0));
const rates = getRatesForYearPe(dateStr, pfuRates);
if (rates) {
next.prelev_sociaux = round2(base * rates.prelev_sociaux / 100);
next.prelev_forfaitaire = round2(base * rates.impot_revenu / 100);
}
}
setForm(next);
};
// Changement de plateforme associée : réinitialise le deal PE lié (peut ne plus être
// éligible à la nouvelle plateforme) — passe par onFormPatch (pas setField), comme le
// <select> Plateforme de RembFormModal qui construit lui-même son patch complet.
const onFormPatch = (patch) => {
setForm(f => {
const next = { ...f, ...patch };
if ('bonus_plateforme_id' in patch) next.investissement_pe_id = '';
return next;
});
};
const isBonusForm = BONUS_VALUES.includes(form.investissement_id);
// Deals PE de la plateforme sélectionnée dans le formulaire (RembFormModal,
// dealOptions) — un remboursement bonus/intérêts peut être rattaché à un deal
// précis, pour le calcul de son TVPI (cf. pages/InvestissementsPe.jsx).
const dealsForSelectedPlat = deals.filter(d => String(d.plateforme_id) === String(form.bonus_plateforme_id));
const submit = async (e) => {
e?.preventDefault?.(); setErr(null);
const base = {
type: form.type,
type: isBonusForm ? BONUS_TYPE_MAP[form.investissement_id] : 'interets_plateforme',
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,
@@ -336,8 +423,12 @@ export default function RemboursementsSimple() {
statut: form.statut,
notes: form.notes || undefined,
};
const payload = form.type === 'interets_plateforme'
const payload = isBonusForm
? {
...base,
cashback: Number(form.cashback) || 0,
}
: {
...base,
interets_bruts_avant_local: Number(form.interets_bruts_avant_local) || 0,
taxe_locale: Number(form.taxe_locale) || 0,
@@ -345,10 +436,6 @@ export default function RemboursementsSimple() {
frais_ttc: Number(form.frais_ttc) || 0,
prelev_sociaux: Number(form.prelev_sociaux) || 0,
prelev_forfaitaire: Number(form.prelev_forfaitaire) || 0,
}
: {
...base,
cashback: Number(form.cashback) || 0,
};
try {
if (editingId) await api.put(`/remboursements/${editingId}`, payload);
@@ -358,6 +445,7 @@ export default function RemboursementsSimple() {
} catch (e) { setErr(e.message); }
};
// Suppression depuis l'icône ✕ d'une ligne du tableau (flux ConfirmModal existant).
const onDelete = async () => {
if (!deleteTarget) return;
try {
@@ -367,6 +455,15 @@ export default function RemboursementsSimple() {
} catch (e) { setErr(e.message); }
};
// Suppression depuis le bouton "Supprimer" du formulaire lui-même (confirmation inline
// de RembFormModal — même pattern que Remboursements.jsx crowdlending).
const onDeleteEditing = async () => {
if (!editingId) return;
await api.del(`/remboursements/${editingId}`);
close();
await load();
};
const sorted = useMemo(
() => [...rows].sort((a, b) => b.date_remb.localeCompare(a.date_remb) || b.id - a.id),
[rows],
@@ -489,24 +586,29 @@ export default function RemboursementsSimple() {
cta="Ajouter un remboursement"
to="/remboursements?new=1"
/>
<Modal
<RembFormModal
open={modalOpen}
title={editingId ? 'Modifier le remboursement' : 'Nouveau remboursement'}
onClose={close}
busy={submitting}
footer={
<div style={{ display: 'flex', gap: 8 }}>
<button disabled={submitting} onClick={close}>Annuler</button>
<button className="primary" disabled={submitting} onClick={() => runSubmit(submit)}>
{submitting ? 'Création…' : 'Créer'}
</button>
</div>
}
>
<RemboursementForm form={form} setForm={setForm} plats={plats} investisseurs={investisseurs} deals={deals}
multiDetenteur={multiDetenteur} err={err} runSubmit={runSubmit} submit={submit}
investisseurForPlat={investisseurForPlat} editingId={editingId} pfuRates={pfuRates} />
</Modal>
editingId={editingId}
form={form}
onFieldChange={setField}
onFormPatch={onFormPatch}
err={err}
onSubmit={e => runSubmit(submit, e)}
onDelete={() => runSubmit(onDeleteEditing)}
submitting={submitting}
confirmingDelete={confirmingDelete}
setConfirmingDelete={setConfirmingDelete}
pfuRates={pfuRates}
comptesInvestisseur={[]}
investissements={[]}
plateformes={plats}
investissementOptions={[]}
multiDetenteur={multiDetenteur}
investissementLabel="Type *"
dealOptions={dealsForSelectedPlat}
forceShowFrais
/>
</>
);
@@ -754,34 +856,29 @@ export default function RemboursementsSimple() {
</div>
)}
<Modal
<RembFormModal
open={modalOpen}
title={editingId ? 'Modifier le remboursement' : 'Nouveau remboursement'}
onClose={close}
busy={submitting}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<div>
{editingId && (
<button className="danger" disabled={submitting}
onClick={() => { const target = { id: editingId }; close(); setDeleteTarget(target); }}>
Supprimer
</button>
)}
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button disabled={submitting} onClick={close}>Annuler</button>
<button className="primary" disabled={submitting} onClick={() => runSubmit(submit)}>
{submitting ? (editingId ? 'Enregistrement…' : 'Création…') : (editingId ? 'Enregistrer' : 'Créer')}
</button>
</div>
</div>
}
>
<RemboursementForm form={form} setForm={setForm} plats={plats} investisseurs={investisseurs} deals={deals}
multiDetenteur={multiDetenteur} err={err} runSubmit={runSubmit} submit={submit}
investisseurForPlat={investisseurForPlat} editingId={editingId} pfuRates={pfuRates} />
</Modal>
editingId={editingId}
form={form}
onFieldChange={setField}
onFormPatch={onFormPatch}
err={err}
onSubmit={e => runSubmit(submit, e)}
onDelete={() => runSubmit(onDeleteEditing)}
submitting={submitting}
confirmingDelete={confirmingDelete}
setConfirmingDelete={setConfirmingDelete}
pfuRates={pfuRates}
comptesInvestisseur={[]}
investissements={[]}
plateformes={plats}
investissementOptions={[]}
multiDetenteur={multiDetenteur}
investissementLabel="Type *"
dealOptions={dealsForSelectedPlat}
forceShowFrais
/>
<ConfirmModal
open={!!deleteTarget}
@@ -794,232 +891,3 @@ export default function RemboursementsSimple() {
</>
);
}
/* ── Fiscalité (17/09/26, cf. Remboursements.jsx / RembFormModal.jsx crowdlending,
* à la demande d'Olivier : la saisie d'une distribution PE doit s'appuyer sur les
* mêmes taux PFU et la même logique indicatif/retenue-à-la-source, pas sur des champs
* prélèvements en saisie libre). Helpers purs, dupliqués volontairement (pas d'état) —
* même formules que RembFormModal.jsx. ── */
const round2 = (n) => Math.round(n * 100) / 100;
const fmtRatePfu = (n) => n != null ? n.toFixed(1).replace('.', ',') + ' %' : '…';
const getRatesForYearPe = (dateStr, pfuRates) => {
if (!dateStr || !pfuRates?.length) return null;
const year = parseInt(dateStr.slice(0, 4), 10);
return pfuRates.find(r => r.annee === year)
?? pfuRates.reduce((best, r) => r.annee > best.annee ? r : best, pfuRates[0]);
};
/** Formulaire d'ajout/édition — factorisé pour être identique dans les deux
* points d'entrée (page avec mouvements existants, et état vide). Champs
* strictement alignés sur le Schema de backend/src/routes/remboursements.js,
* limités aux types bonus_parrainage / bonus_plateforme / interets_plateforme
* (le type "normal", lié à un investissement, reste crowdlending uniquement). */
function RemboursementForm({ form, setForm, plats, investisseurs, deals, multiDetenteur, err, runSubmit, submit, investisseurForPlat, editingId, pfuRates }) {
const isInterets = form.type === 'interets_plateforme';
// Deals PE de la plateforme sélectionnée (un remboursement bonus/intérêts
// peut être rattaché à un deal précis, pour le calcul de son TVPI — cf.
// pages/InvestissementsPe.jsx). Optionnel : plusieurs plateformes n'ont
// qu'un seul deal, ou aucun deal PE suivi pour l'instant.
const dealsForPlat = deals.filter(d => String(d.plateforme_id) === String(form.bonus_plateforme_id));
// ── Fiscalité (même logique que RembFormModal.jsx crowdlending) ────────
const currentPlat = plats.find(p => String(p.id) === String(form.bonus_plateforme_id));
const hasLocalTax = isInterets && currentPlat?.fiscalite === 'avec_fiscalite_locale' && currentPlat?.taux_fiscalite_locale;
// Pas d'équivalent fiscalite_override côté investissements_pe (pas de notion
// d'exonération par deal) : seule la fiscalité de la plateforme détermine le
// caractère indicatif, contrairement à crowdlending qui croise aussi
// investissement.fiscalite_override — cf. table investissements_pe (db/index.js).
const isIndicatif = isInterets && Boolean(currentPlat && currentPlat.fiscalite !== 'flat_tax');
const rates = isInterets ? getRatesForYearPe(form.date_remb, pfuRates) : null;
const labelPS = `Prélèvements sociaux${rates ? ` (${fmtRatePfu(rates.prelev_sociaux)})` : ''} (€)`;
const labelIR = `Impôt sur le revenu${rates ? ` (${fmtRatePfu(rates.impot_revenu)})` : ''} (€)`;
// Recalcule automatiquement taxe locale / prélèvements PFU quand un champ
// "moteur" change — l'utilisateur garde la main pour corriger prelev_sociaux/
// prelev_forfaitaire ensuite (ces deux champs ne redéclenchent pas ce recalcul,
// exactement comme setField()/onFormPatch() dans Remboursements.jsx).
const setFiscalField = (key, value) => {
const next = { ...form, [key]: value };
if (key === 'interets_bruts_avant_local') {
const taux = hasLocalTax ? (currentPlat.taux_fiscalite_locale || 0) : 0;
next.taxe_locale = round2(Number(value) * taux / 100);
next.interets_bruts = round2(Number(value) - next.taxe_locale);
}
if (key === 'taxe_locale') {
next.interets_bruts = round2(Number(next.interets_bruts_avant_local || 0) - Number(value));
}
if (['date_remb', 'interets_bruts', 'interets_bruts_avant_local', 'taxe_locale', 'frais_ttc'].includes(key)) {
const dateStr = key === 'date_remb' ? value : next.date_remb;
const base = round2((Number(next.interets_bruts) || 0) - (Number(next.frais_ttc) || 0));
const r = getRatesForYearPe(dateStr, pfuRates);
if (r) {
next.prelev_sociaux = round2(base * r.prelev_sociaux / 100);
next.prelev_forfaitaire = round2(base * r.impot_revenu / 100);
}
}
setForm(next);
};
const interetsNets = round2(
(Number(form.interets_bruts) || 0) - (Number(form.frais_ttc) || 0)
- (Number(form.prelev_sociaux) || 0) - (Number(form.prelev_forfaitaire) || 0)
);
const previewNet = () => {
if (isInterets) {
return isIndicatif
? round2((Number(form.interets_bruts) || 0) - (Number(form.frais_ttc) || 0))
: interetsNets;
}
return Number(form.cashback) || 0;
};
return (
<form onSubmit={e => runSubmit(submit, e)}>
{err && <div className="error">{err}</div>}
<div className="form-grid">
<div>
<label>Type *</label>
<select required value={form.type} disabled={!!editingId}
title={editingId ? 'Le type ne peut pas être modifié après création' : undefined}
onChange={e => setForm({ ...form, type: e.target.value })}>
<option value="bonus_plateforme">Bonus plateforme</option>
<option value="bonus_parrainage">Bonus parrainage</option>
<option value="interets_plateforme">Intérêts plateforme</option>
</select>
</div>
<div>
<label>Plateforme *</label>
<select required value={form.bonus_plateforme_id} onChange={e => {
const platId = e.target.value;
setForm(f => ({ ...f, bonus_plateforme_id: platId, bonus_investisseur_id: investisseurForPlat(platId), investissement_pe_id: '' }));
}}>
<option value="">—</option>
{plats.map(p => <option key={p.id} value={p.id}>{p.nom}{multiDetenteur && p.investisseur_nom ? ` — ${p.investisseur_nom}` : ''}</option>)}
</select>
</div>
{dealsForPlat.length > 0 && (
<div>
<label>Investissement (optionnel)</label>
<select value={form.investissement_pe_id} onChange={e => setForm({ ...form, investissement_pe_id: e.target.value })}>
<option value="">— Aucun (non rattaché à un deal) —</option>
{dealsForPlat.map(d => <option key={d.id} value={d.id}>{d.nom_deal}</option>)}
</select>
</div>
)}
<div>
<label>Date *</label>
<input type="date" required value={form.date_remb} onChange={e => setForm({ ...form, date_remb: e.target.value })} />
</div>
{!isInterets && (
<div>
<label>Montant reçu (€) *</label>
<input type="number" step="0.01" min="0" required value={form.cashback} onChange={e => setForm({ ...form, cashback: e.target.value })} />
</div>
)}
{isInterets && (
<>
{hasLocalTax && (
<div>
<label>Intérêts bruts avant retenue locale ({currentPlat.taux_fiscalite_locale} %) (€)</label>
<input type="number" step="0.01" min="0" value={form.interets_bruts_avant_local}
onChange={e => setFiscalField('interets_bruts_avant_local', e.target.value)} />
</div>
)}
{hasLocalTax && (
<div>
<label>Retenue à la source locale (€)</label>
<input type="number" step="0.01" min="0" value={form.taxe_locale}
onChange={e => setFiscalField('taxe_locale', e.target.value)} />
</div>
)}
<div>
<label>{hasLocalTax ? 'Intérêts bruts après retenue locale (€)' : 'Intérêts bruts (€) *'}</label>
<input type="number" step="0.01" min="0" required={!hasLocalTax} value={form.interets_bruts}
readOnly={!!hasLocalTax}
style={hasLocalTax ? { background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' } : undefined}
onChange={e => !hasLocalTax && setFiscalField('interets_bruts', e.target.value)} />
</div>
<div>
<label>Frais TTC (€)</label>
<input type="number" step="0.01" min="0" value={form.frais_ttc}
onChange={e => setFiscalField('frais_ttc', e.target.value)} />
</div>
<div style={{ gridColumn: '1 / -1', borderTop: '1px solid var(--border)', paddingTop: 4, marginTop: 4 }}>
<span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--text-muted)' }}>
Imposition{isIndicatif ? ' — indicatif' : ''}
</span>
</div>
<div>
<label>{labelPS}</label>
<input type="number" step="0.01" value={form.prelev_sociaux}
readOnly={isIndicatif}
style={isIndicatif ? { background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' } : undefined}
onChange={e => !isIndicatif && setForm({ ...form, prelev_sociaux: e.target.value })} />
</div>
<div>
<label>{labelIR}</label>
<input type="number" step="0.01" value={form.prelev_forfaitaire}
readOnly={isIndicatif}
style={isIndicatif ? { background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' } : undefined}
onChange={e => !isIndicatif && setForm({ ...form, prelev_forfaitaire: e.target.value })} />
</div>
<div>
<label>Montant net perçu (€)</label>
<input type="number" step="0.01" readOnly value={previewNet()}
style={{ background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' }} />
</div>
<div style={{ gridColumn: '1 / -1', color: 'var(--text-muted, #6b7280)', fontSize: 12 }}>
{isIndicatif
? "L'imposition n'est pas déduite du montant net perçu (fiscalité appliquée à titre indicatif — plateforme hors flat tax)."
: "L'imposition est déduite du montant net perçu (intérêts bruts − frais − prélèvements)."}
</div>
</>
)}
<div>
<label>Statut</label>
<select value={form.statut} onChange={e => setForm({ ...form, statut: e.target.value })}>
<option value="paye">Payé</option>
<option value="retard">En retard</option>
<option value="partiel">Partiel</option>
<option value="impaye">Impayé</option>
</select>
</div>
<div style={{ gridColumn: '1 / -1' }}>
<label>Détenteur *</label>
<select required value={form.bonus_investisseur_id} onChange={e => setForm({ ...form, bonus_investisseur_id: e.target.value })}>
<option value="">—</option>
{investisseurs.filter(i => i.type !== 'entreprise').length > 0 && (
<optgroup label="Famille">
{investisseurs.filter(i => i.type !== 'entreprise').map(i => (
<option key={i.id} value={i.id}>{i.nom}{i.is_principal ? ' (principal)' : ''}</option>
))}
</optgroup>
)}
{investisseurs.filter(i => i.type === 'entreprise').length > 0 && (
<optgroup label="Entreprises">
{investisseurs.filter(i => i.type === 'entreprise').map(i => (
<option key={i.id} value={i.id}>{i.nom}</option>
))}
</optgroup>
)}
</select>
</div>
<div style={{ gridColumn: '1 / -1' }}>
<label>Notes</label>
<input value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} />
</div>
{!isInterets && (
<div style={{ gridColumn: '1 / -1', color: 'var(--text-muted, #6b7280)', fontSize: 13 }}>
Net perçu estimé : {fmtEUR(previewNet())}
</div>
)}
</div>
</form>
);
}