This commit is contained in:
ocroguennec committed 2026-09-28 08:24:16 +02:00
1 parent 624d32ff7c
commit bce89d8112
18 files changed
+663 -28

No files matched your search

+23 -1
View File
@@ -64,6 +64,7 @@ function EnveloppeModal({ row, onClose, onSaved }) {
description: row?.description || '',
plafond_eur: row?.plafond_eur != null ? String(row.plafond_eur) : '',
regime_fiscal: row?.regime_fiscal || '',
eligible_compte: !!row?.eligible_compte,
categories_ids: (row?.categories || []).map(c => c.id),
supports_ids: (row?.supports || []).map(s => s.id),
});
@@ -87,6 +88,7 @@ function EnveloppeModal({ row, onClose, onSaved }) {
description: form.description.trim() || null,
plafond_eur: plafond,
regime_fiscal: form.regime_fiscal.trim() || null,
eligible_compte: !!form.eligible_compte,
categories_ids: form.categories_ids || [],
supports_ids: form.supports_ids || [],
};
@@ -128,6 +130,18 @@ function EnveloppeModal({ row, onClose, onSaved }) {
<input value={form.regime_fiscal} onChange={e => set('regime_fiscal', e.target.value)} placeholder="Ex : Exonéré IR/PS, Flat tax 30%…" />
</div>
</div>
<div>
<label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
<input type="checkbox" checked={!!form.eligible_compte} style={{ width: 'auto' }}
onChange={e => set('eligible_compte', e.target.checked)} />
Éligible comme type de compte (Paramètres &gt; Mes comptes courants)
</label>
<p style={{ margin: '2px 0 0', fontSize: 11, color: 'var(--text-muted)' }}>
À cocher pour les enveloppes qui ont le profil d'un vrai compte financier (banque,
courtier, assureur…) — pas pour les actifs détenus en direct (immobilier, crypto
auto-conservée, objets de collection…).
</p>
</div>
<div>
<label>Catégories</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
@@ -302,7 +316,15 @@ function EnveloppesSection() {
onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-2)'}
onMouseLeave={e => e.currentTarget.style.background = 'none'}>
<td style={{ padding: '10px 16px' }}>
<div style={{ fontWeight: 600 }}>{row.nom}</div>
<div style={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 6 }}>
{row.nom}
{!!row.eligible_compte && (
<span title="Éligible comme type de compte (Paramètres > Mes comptes courants)"
style={{ fontSize: 10, fontWeight: 600, color: 'var(--primary)', border: '1px solid var(--primary)', borderRadius: 4, padding: '1px 5px' }}>
Compte
</span>
)}
</div>
{row.description && <div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>{row.description}</div>}
</td>
<td style={{ padding: '10px 8px', color: 'var(--text-muted)', fontSize: 12 }}>{eur(row.plafond_eur)}</td>
+4
View File
@@ -5,6 +5,7 @@ import PlateformesSection from './settings/PlateformesSection.jsx';
import CategoriesInvSection from './settings/CategoriesInvSection.jsx';
import SecteursInvSection from './settings/SecteursInvSection.jsx';
import ComptesSection from './settings/ComptesSection.jsx';
import EmpruntsSection from './settings/EmpruntsSection.jsx';
import MaFiscaliteSection from './settings/MaFiscaliteSection.jsx';
import ObjectifsSection from './settings/ObjectifsSection.jsx';
import DataCleanupSection from './settings/DataCleanupSection.jsx';
@@ -16,6 +17,7 @@ function IconFamily() { return <svg width="15" height="15" viewBox="0 0 24 24"
function IconMonitor() { return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>; }
function IconServer() { return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>; }
function IconWallet() { return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="2" y="5" width="20" height="14" rx="2"/><path d="M16 12h.01M2 10h20"/></svg>; }
function IconLoan() { return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>; }
function IconTag() { return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>; }
function IconLayers() { return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>; }
function IconGrid() { return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>; }
@@ -44,6 +46,7 @@ const NAV = [
{ id: 'membres', label: 'Mes membres & entreprises', icon: <IconFamily /> },
{ id: 'plateformes', label: 'Mes plateformes', icon: <IconServer /> },
{ id: 'comptes', label: 'Mes comptes courants', icon: <IconWallet /> },
{ id: 'emprunts', label: 'Mes emprunts', icon: <IconLoan /> },
{ id: 'ma-fiscalite', label: 'Ma fiscalité', icon: <IconMyFiscal /> },
{ id: 'objectifs', label: 'Fixation des objectifs', icon: <IconTarget /> },
],
@@ -97,6 +100,7 @@ export default function Settings() {
{section === 'membres' && <FamilleEntreprises />}
{section === 'plateformes' && <PlateformesSection />}
{section === 'comptes' && <ComptesSection />}
{section === 'emprunts' && <EmpruntsSection />}
{section === 'ma-fiscalite' && <MaFiscaliteSection />}
{section === 'objectifs' && <ObjectifsSection />}
{section === 'categories-inv' && <CategoriesInvSection />}
+41 -10
View File
@@ -14,7 +14,7 @@ const TYPE_COMPTE_LABELS = {
pea_pme: 'PEA-PME',
};
const EMPTY_COMPTE = { nom: '', type: 'compte_courant', investisseur_id: null, institution_id: null, banque: '', exoneration_fiscale: 'aucune' };
const EMPTY_COMPTE = { nom: '', type: 'compte_courant', investisseur_id: null, institution_id: null, banque: '', enveloppe_id: null, exoneration_fiscale: 'aucune' };
// Référentiel institutions financières (Phase 2, 26/09/26) — logos servis au même endroit
// que ceux des plateformes (/api/logos/:filename), cf. institutionsReferentiel.js.
@@ -28,8 +28,15 @@ function compteInvestisseur(c) {
return { id: c.investisseur_id, nom: c.investisseur_nom, prenom: c.investisseur_prenom, type: c.investisseur_type, type_fiscal: c.investisseur_type_fiscal };
}
function CompteFormFields({ state, setter, investisseurs, institutions }) {
function CompteFormFields({ state, setter, investisseurs, institutions, enveloppes }) {
const isAutre = !state.institution_id;
// Regroupement par catégorie pour le <select> Enveloppe (cf. enveloppesReferentielPublic.js) —
// seules les enveloppes eligible_compte=1 sont proposées ici (cadrage Olivier, 28/09/26).
const enveloppesParCategorie = {};
for (const env of enveloppes) {
const cat = (env.categories && env.categories[0]) ? env.categories[0].nom : 'Autre';
(enveloppesParCategorie[cat] ??= []).push(env);
}
return (
<>
<div>
@@ -72,6 +79,21 @@ function CompteFormFields({ state, setter, investisseurs, institutions }) {
placeholder="ex. BNP Paribas — absente du référentiel ?" />
</div>
)}
<div>
<label>Enveloppe</label>
<select value={state.enveloppe_id ?? ''}
onChange={e => setter({ ...state, enveloppe_id: e.target.value ? Number(e.target.value) : null })}>
<option value="">— Non renseignée —</option>
{Object.entries(enveloppesParCategorie).map(([cat, envs]) => (
<optgroup key={cat} label={cat}>
{envs.map(env => <option key={env.id} value={env.id}>{env.nom}</option>)}
</optgroup>
))}
</select>
<p style={{ margin: '4px 0 0', fontSize: 11, color: 'var(--text-muted)' }}>
Rattachement au référentiel (Admin &gt; Enveloppes &amp; supports) — informatif, sans effet sur le champ Type ci-dessus.
</p>
</div>
<div>
<label>Détenteur</label>
<select value={state.investisseur_id ?? ''}
@@ -98,16 +120,21 @@ export default function ComptesSection() {
const [comptes, setComptes] = useState([]);
const [investisseurs, setInvestisseurs] = useState([]);
const [institutions, setInstitutions] = useState([]);
const [enveloppes, setEnveloppes] = useState([]);
const reload = useCallback(async () => {
const [cpts, invs, insts] = await Promise.all([
const [cpts, invs, insts, envs] = await Promise.all([
api.get('/comptes'),
api.get('/investisseurs'),
api.get('/institutions-referentiel-public'),
api.get('/enveloppes-referentiel-public'),
]);
setComptes(cpts);
setInvestisseurs(invs);
setInstitutions(insts);
// Sous-ensemble "compte financier" uniquement (cadrage Olivier, 28/09/26) — cf.
// enveloppesReferentielPublic.js et la migration eligible_compte dans db/index.js.
setEnveloppes(envs.filter(e => e.eligible_compte));
}, []);
useEffect(() => { reload(); }, [reload]);
@@ -129,6 +156,7 @@ export default function ComptesSection() {
type: newCompte.type,
banque: newCompte.banque || null,
institution_id: newCompte.institution_id ?? null,
enveloppe_id: newCompte.enveloppe_id ?? null,
investisseur_id: newCompte.investisseur_id ? Number(newCompte.investisseur_id) : null,
exoneration_fiscale: newCompte.exoneration_fiscale ?? 'aucune',
});
@@ -144,6 +172,7 @@ export default function ComptesSection() {
type: editCompte.type,
banque: editCompte.banque || null,
institution_id: editCompte.institution_id ?? null,
enveloppe_id: editCompte.enveloppe_id ?? null,
investisseur_id: editCompte.investisseur_id ? Number(editCompte.investisseur_id) : null,
exoneration_fiscale: editCompte.exoneration_fiscale ?? 'aucune',
});
@@ -153,7 +182,7 @@ export default function ComptesSection() {
const openEdit = (c) => {
setErr(null);
setEditCompte({ id: c.id, nom: c.nom, type: c.type, banque: c.banque || '', institution_id: c.institution_id ?? null, investisseur_id: c.investisseur_id ?? null, exoneration_fiscale: c.exoneration_fiscale ?? 'aucune' });
setEditCompte({ id: c.id, nom: c.nom, type: c.type, banque: c.banque || '', institution_id: c.institution_id ?? null, enveloppe_id: c.enveloppe_id ?? null, investisseur_id: c.investisseur_id ?? null, exoneration_fiscale: c.exoneration_fiscale ?? 'aucune' });
};
const del = (c) => {
@@ -190,16 +219,17 @@ export default function ComptesSection() {
<thead>
<tr>
<th>Nom</th>
<th style={{ width: '14%' }}>Type</th>
{multiDetenteur && <th style={{ width: '22%' }}>Détenteur</th>}
<th style={{ width: '16%' }}>Banque</th>
<th style={{ width: '13%' }}>Type</th>
{multiDetenteur && <th style={{ width: '18%' }}>Détenteur</th>}
<th style={{ width: '14%' }}>Banque</th>
<th style={{ width: '16%' }}>Enveloppe</th>
<th style={{ width: '10%' }}>Exonération</th>
<th style={{ width: 80 }} />
</tr>
</thead>
<tbody>
{comptes.length === 0 && (
<tr><td colSpan={multiDetenteur ? 6 : 5} className="text-muted" style={{ textAlign: 'center', fontStyle: 'italic' }}>
<tr><td colSpan={multiDetenteur ? 7 : 6} className="text-muted" style={{ textAlign: 'center', fontStyle: 'italic' }}>
Aucun compte défini.
</td></tr>
)}
@@ -221,6 +251,7 @@ export default function ComptesSection() {
</span>
) : (c.banque || <span className="text-muted">—</span>)}
</td>
<td>{c.enveloppe_nom || <span className="text-muted">—</span>}</td>
<td>
{c.exoneration_fiscale && c.exoneration_fiscale !== 'aucune' ? (
<span
@@ -259,7 +290,7 @@ export default function ComptesSection() {
>
<form id="form-new-compte" onSubmit={e => runAddCompte(addCompte, e)} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{err && <div className="error">{err}</div>}
<CompteFormFields state={newCompte} setter={setNewCompte} investisseurs={investisseurs} institutions={institutions} />
<CompteFormFields state={newCompte} setter={setNewCompte} investisseurs={investisseurs} institutions={institutions} enveloppes={enveloppes} />
</form>
</Modal>
@@ -283,7 +314,7 @@ export default function ComptesSection() {
>
<form id="form-edit-compte" onSubmit={e => runSaveEdit(saveEdit, e)} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{err && <div className="error">{err}</div>}
<CompteFormFields state={editCompte} setter={setEditCompte} investisseurs={investisseurs} institutions={institutions} />
<CompteFormFields state={editCompte} setter={setEditCompte} investisseurs={investisseurs} institutions={institutions} enveloppes={enveloppes} />
</form>
</Modal>
@@ -0,0 +1,305 @@
import { useState, useEffect, useCallback } from 'react';
import { api } from '../../api.js';
import { memberLabel } from '../../utils/format.js';
import ConfirmModal from '../../components/ConfirmModal.jsx';
import Modal from '../../components/Modal.jsx';
import { useAsyncAction } from '../../hooks/useAsyncAction.js';
// Paramètres > Mes emprunts (Phase 3c, 28/09/26, demande Olivier) — passif patrimonial,
// symétrique de Paramètres > Mes comptes courants, cf. routes/emprunts.js et la table dédiée
// dans db/index.js. Un emprunt est structurellement différent d'un compte/enveloppe (capital
// restant dû qui diminue, taux, échéancier) — objet séparé plutôt qu'un enrichissement de
// comptes.js.
const EMPTY_EMPRUNT = {
nom: '', investisseur_id: null, institution_id: null,
montant_emprunte: '', capital_restant_du: '', taux_nominal: '',
date_debut: '', duree_mois: '', mensualite: '', notes: '',
};
const API_BASE = (import.meta.env.VITE_API_URL || '/api').replace(/\/api$/, '');
function institutionLogoUrl(institutionIconeOrLogo) {
return institutionIconeOrLogo ? `${API_BASE}/api/logos/${institutionIconeOrLogo}` : null;
}
function empruntInvestisseur(e) {
if (!e.investisseur_id) return null;
return { id: e.investisseur_id, nom: e.investisseur_nom, prenom: e.investisseur_prenom, type: e.investisseur_type, type_fiscal: e.investisseur_type_fiscal };
}
function eur(n) {
return n == null || n === '' ? null : new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(n);
}
function EmpruntFormFields({ state, setter, investisseurs, institutions }) {
return (
<>
<div>
<label>Nom *</label>
<input required value={state.nom}
onChange={e => setter({ ...state, nom: e.target.value })}
placeholder="ex. Crédit immobilier résidence principale" />
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div>
<label>Montant emprunté (€) *</label>
<input required type="number" min="0" step="0.01" value={state.montant_emprunte}
onChange={e => setter({ ...state, montant_emprunte: e.target.value })} placeholder="ex. 250000" />
</div>
<div>
<label>Capital restant dû (€) *</label>
<input required type="number" min="0" step="0.01" value={state.capital_restant_du}
onChange={e => setter({ ...state, capital_restant_du: e.target.value })} placeholder="ex. 230000" />
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}>
<div>
<label>Taux nominal (%)</label>
<input type="number" min="0" step="0.01" value={state.taux_nominal}
onChange={e => setter({ ...state, taux_nominal: e.target.value })} placeholder="ex. 3.2" />
</div>
<div>
<label>Date de début</label>
<input type="date" value={state.date_debut || ''}
onChange={e => setter({ ...state, date_debut: e.target.value })} />
</div>
<div>
<label>Durée (mois)</label>
<input type="number" min="1" step="1" value={state.duree_mois}
onChange={e => setter({ ...state, duree_mois: e.target.value })} placeholder="ex. 240" />
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div>
<label>Mensualité (€)</label>
<input type="number" min="0" step="0.01" value={state.mensualite}
onChange={e => setter({ ...state, mensualite: e.target.value })} placeholder="ex. 1400" />
</div>
<div>
<label>Établissement prêteur</label>
<select value={state.institution_id ?? ''}
onChange={e => setter({ ...state, institution_id: e.target.value ? Number(e.target.value) : null })}>
<option value="">— Non renseigné —</option>
{institutions.map(inst => (
<option key={inst.id} value={inst.id}>{inst.nom}</option>
))}
</select>
</div>
</div>
<div>
<label>Détenteur</label>
<select value={state.investisseur_id ?? ''}
onChange={e => setter({ ...state, investisseur_id: e.target.value ? Number(e.target.value) : null })}>
<option value="">— Non renseigné —</option>
{investisseurs.map(inv => (
<option key={inv.id} value={inv.id}>{memberLabel(inv)}</option>
))}
</select>
</div>
<div>
<label>Notes</label>
<textarea value={state.notes || ''} onChange={e => setter({ ...state, notes: e.target.value })}
style={{ minHeight: 70, resize: 'vertical' }} placeholder="Informations complémentaires…" />
</div>
</>
);
}
export default function EmpruntsSection() {
const [emprunts, setEmprunts] = useState([]);
const [investisseurs, setInvestisseurs] = useState([]);
const [institutions, setInstitutions] = useState([]);
const reload = useCallback(async () => {
const [emps, invs, insts] = await Promise.all([
api.get('/emprunts'),
api.get('/investisseurs'),
api.get('/institutions-referentiel-public'),
]);
setEmprunts(emps);
setInvestisseurs(invs);
setInstitutions(insts);
}, []);
useEffect(() => { reload(); }, [reload]);
const [showNew, setShowNew] = useState(false);
const [newEmprunt, setNewEmprunt] = useState(EMPTY_EMPRUNT);
const [editEmprunt, setEditEmprunt] = useState(null);
const [err, setErr] = useState(null);
const [confirmDel, setConfirmDel] = useState(null);
const { run: runAddEmprunt, pending: addingEmprunt } = useAsyncAction();
const { run: runSaveEdit, pending: savingEdit } = useAsyncAction();
const openNew = () => { setNewEmprunt(EMPTY_EMPRUNT); setErr(null); setShowNew(true); };
const toPayload = (state) => ({
nom: state.nom,
investisseur_id: state.investisseur_id ? Number(state.investisseur_id) : null,
institution_id: state.institution_id ? Number(state.institution_id) : null,
montant_emprunte: Number(state.montant_emprunte),
capital_restant_du: Number(state.capital_restant_du),
taux_nominal: state.taux_nominal !== '' && state.taux_nominal != null ? Number(state.taux_nominal) : null,
date_debut: state.date_debut || null,
duree_mois: state.duree_mois !== '' && state.duree_mois != null ? Number(state.duree_mois) : null,
mensualite: state.mensualite !== '' && state.mensualite != null ? Number(state.mensualite) : null,
notes: state.notes || null,
});
const addEmprunt = async (e) => {
e.preventDefault(); setErr(null);
try {
await api.post('/emprunts', toPayload(newEmprunt));
setShowNew(false); setNewEmprunt(EMPTY_EMPRUNT); reload();
} catch (ex) { setErr(ex.message); }
};
const saveEdit = async (e) => {
e.preventDefault(); setErr(null);
try {
await api.put(`/emprunts/${editEmprunt.id}`, toPayload(editEmprunt));
setEditEmprunt(null); reload();
} catch (ex) { setErr(ex.message); }
};
const openEdit = (e) => {
setErr(null);
setEditEmprunt({
id: e.id, nom: e.nom, investisseur_id: e.investisseur_id ?? null, institution_id: e.institution_id ?? null,
montant_emprunte: e.montant_emprunte ?? '', capital_restant_du: e.capital_restant_du ?? '',
taux_nominal: e.taux_nominal ?? '', date_debut: e.date_debut || '', duree_mois: e.duree_mois ?? '',
mensualite: e.mensualite ?? '', notes: e.notes || '',
});
};
const del = (e) => {
setConfirmDel({
message: `Supprimer l'emprunt "${e.nom}" ?`,
onConfirm: async () => {
try { await api.del(`/emprunts/${e.id}`); reload(); }
catch (ex) { setErr(ex.message); }
finally { setConfirmDel(null); }
},
});
};
const multiDetenteur = investisseurs.length > 1;
return (
<>
<div className="card">
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 16 }}>
<div>
<h3 style={{ margin: 0 }}>Mes emprunts</h3>
<p className="text-muted" style={{ fontSize: 'var(--fs-sm)', margin: '4px 0 0' }}>
Passif patrimonial — crédits en cours, distincts des comptes et enveloppes d'actifs.
</p>
</div>
<button className="primary" style={{ whiteSpace: 'nowrap' }} onClick={openNew}>
+ Nouvel emprunt
</button>
</div>
{err && <div className="error" style={{ marginBottom: 10 }}>{err}</div>}
<table>
<thead>
<tr>
<th>Nom</th>
<th style={{ width: '14%' }}>Montant emprunté</th>
<th style={{ width: '14%' }}>Capital restant dû</th>
{multiDetenteur && <th style={{ width: '18%' }}>Détenteur</th>}
<th style={{ width: '16%' }}>Prêteur</th>
<th style={{ width: 80 }} />
</tr>
</thead>
<tbody>
{emprunts.length === 0 && (
<tr><td colSpan={multiDetenteur ? 6 : 5} className="text-muted" style={{ textAlign: 'center', fontStyle: 'italic' }}>
Aucun emprunt défini.
</td></tr>
)}
{emprunts.map(e => {
const inv = empruntInvestisseur(e);
return (
<tr key={e.id}>
<td style={{ fontWeight: 500 }}>{e.nom}</td>
<td>{eur(e.montant_emprunte)}</td>
<td>{eur(e.capital_restant_du)}</td>
{multiDetenteur && <td>{inv ? memberLabel(inv) : <span className="text-muted">—</span>}</td>}
<td>
{e.institution_id ? (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
{institutionLogoUrl(e.institution_icone_filename || e.institution_logo_filename) && (
<img src={institutionLogoUrl(e.institution_icone_filename || e.institution_logo_filename)} alt=""
style={{ width: 18, height: 18, objectFit: 'contain', borderRadius: 3, flexShrink: 0 }} />
)}
{e.institution_nom}
</span>
) : <span className="text-muted">—</span>}
</td>
<td>
<div style={{ display: 'flex', gap: 4 }}>
<button style={{ padding: '3px 10px', fontSize: 11 }} onClick={() => openEdit(e)}>Modifier</button>
<button className="danger" style={{ padding: '3px 10px', fontSize: 11 }} onClick={() => del(e)}>✕</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* ── Modal création ── */}
<Modal
open={showNew}
title="Nouvel emprunt"
onClose={() => { setShowNew(false); setNewEmprunt(EMPTY_EMPRUNT); setErr(null); }}
busy={addingEmprunt}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', width: '100%', gap: 8 }}>
<button type="button" disabled={addingEmprunt} onClick={() => { setShowNew(false); setNewEmprunt(EMPTY_EMPRUNT); setErr(null); }}>Annuler</button>
<button className="primary" form="form-new-emprunt" type="submit" disabled={addingEmprunt}>{addingEmprunt ? 'Création…' : 'Créer'}</button>
</div>
}
>
<form id="form-new-emprunt" onSubmit={e => runAddEmprunt(addEmprunt, e)} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{err && <div className="error">{err}</div>}
<EmpruntFormFields state={newEmprunt} setter={setNewEmprunt} investisseurs={investisseurs} institutions={institutions} />
</form>
</Modal>
{/* ── Modal édition ── */}
<Modal
open={!!editEmprunt}
title="Modifier l'emprunt"
onClose={() => { setEditEmprunt(null); setErr(null); }}
busy={savingEdit}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<button className="danger" type="button" disabled={savingEdit} onClick={() => { setEditEmprunt(null); del(emprunts.find(e => e.id === editEmprunt?.id) ?? editEmprunt); }}>
Supprimer
</button>
<div style={{ display: 'flex', gap: 8 }}>
<button type="button" disabled={savingEdit} onClick={() => { setEditEmprunt(null); setErr(null); }}>Annuler</button>
<button className="primary" form="form-edit-emprunt" type="submit" disabled={savingEdit}>{savingEdit ? 'Enregistrement…' : 'Enregistrer'}</button>
</div>
</div>
}
>
<form id="form-edit-emprunt" onSubmit={e => runSaveEdit(saveEdit, e)} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{err && <div className="error">{err}</div>}
<EmpruntFormFields state={editEmprunt} setter={setEditEmprunt} investisseurs={investisseurs} institutions={institutions} />
</form>
</Modal>
{/* ── ConfirmModal suppression ── */}
<ConfirmModal
open={!!confirmDel}
message={confirmDel?.message}
onConfirm={confirmDel?.onConfirm}
onCancel={() => setConfirmDel(null)}
/>
</>
);
}