This commit is contained in:
2026-06-16 09:16:40 +02:00
parent 9058bc7d22
commit 6234c76507
2 changed files with 452 additions and 6 deletions
+9 -5
View File
@@ -219,7 +219,7 @@ function syncCategories(platId, catIds) {
router.get('/referentiel-list', (_req, res) => { router.get('/referentiel-list', (_req, res) => {
const rows = db.prepare(` const rows = db.prepare(`
SELECT pr.id, pr.nom, pr.domiciliation, pr.fiscalite, SELECT pr.id, pr.nom, pr.domiciliation, pr.fiscalite,
pr.taux_fiscalite_locale, pr.type_produit_fiscal, pr.logo_filename pr.taux_fiscalite_locale, pr.type_produit_fiscal, pr.logo_filename, pr.icone_filename
FROM plateformes_referentiel pr FROM plateformes_referentiel pr
ORDER BY pr.nom ORDER BY pr.nom
`).all(); `).all();
@@ -262,13 +262,15 @@ router.post('/', (req, res, next) => {
let taux = fiscalite === 'avec_fiscalite_locale' ? (body.taux_fiscalite_locale ?? null) : null; let taux = fiscalite === 'avec_fiscalite_locale' ? (body.taux_fiscalite_locale ?? null) : null;
let typeProduitFiscal = body.domiciliation === 'FR' ? (body.type_produit_fiscal ?? '2TT') : '2TT'; let typeProduitFiscal = body.domiciliation === 'FR' ? (body.type_produit_fiscal ?? '2TT') : '2TT';
let referentielId = body.referentiel_id ?? null; let referentielId = body.referentiel_id ?? null;
let logoFilename = null;
let iconeFilename = null;
// Si un référentiel est sélectionné, hériter ses valeurs (pas encore d'overrides) // Si un référentiel est sélectionné, hériter logo et icone
if (referentielId) { if (referentielId) {
const ref = db.prepare('SELECT * FROM plateformes_referentiel WHERE id = ?').get(referentielId); const ref = db.prepare('SELECT * FROM plateformes_referentiel WHERE id = ?').get(referentielId);
if (!ref) throw new HttpError(404, 'Référentiel introuvable'); if (!ref) throw new HttpError(404, 'Référentiel introuvable');
// Les champs du body priment (l'user peut déjà avoir modifié à la création) logoFilename = ref.logo_filename ?? null;
// On calcule les overrides par rapport au référentiel iconeFilename = ref.icone_filename ?? null;
} }
const r = db.prepare(` const r = db.prepare(`
@@ -276,6 +278,7 @@ router.post('/', (req, res, next) => {
(user_id, nom, url, notes, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal, (user_id, nom, url, notes, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal,
methode_remboursement, investisseur_id, date_ouverture, methode_remboursement, investisseur_id, date_ouverture,
type_pret_defaut, freq_interets_defaut, type_pret_defaut, freq_interets_defaut,
logo_filename, icone_filename,
referentiel_id, overridden_fields) referentiel_id, overridden_fields)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
`).run( `).run(
@@ -283,8 +286,9 @@ router.post('/', (req, res, next) => {
body.domiciliation, fiscalite, taux, typeProduitFiscal, body.domiciliation, fiscalite, taux, typeProduitFiscal,
body.methode_remboursement, body.investisseur_id ?? null, body.date_ouverture || null, body.methode_remboursement, body.investisseur_id ?? null, body.date_ouverture || null,
body.type_pret_defaut ?? null, body.freq_interets_defaut ?? null, body.type_pret_defaut ?? null, body.freq_interets_defaut ?? null,
logoFilename, iconeFilename,
referentielId, referentielId,
'[]' // toujours vide à la création — les overrides se calculent au premier PUT '[]'
); );
const id = r.lastInsertRowid; const id = r.lastInsertRowid;
syncCategories(id, body.categories); syncCategories(id, body.categories);
@@ -273,6 +273,361 @@ function PlatDetailPanel({ plat, onEdit }) {
/* ── Panneau de détail PFU ───────────────────────────────────── */ /* ── Panneau de détail PFU ───────────────────────────────────── */
/* ── Picker référentiel ──────────────────────────────────────── */
function PlatPickerModal({ open, onClose, referentiel, onSelect, onManual, err }) {
const [search, setSearch] = useState('');
const [domFilter, setDomFilter] = useState('all'); // 'all' | 'fr' | 'etr'
if (!open) return null;
const q = search.trim().toLowerCase();
const filtered = referentiel.filter(r => {
if (domFilter === 'fr' && r.domiciliation !== 'FR') return false;
if (domFilter === 'etr' && r.domiciliation === 'FR') return false;
if (q && !r.nom.toLowerCase().includes(q)) return false;
return true;
});
const imgFor = (r) => {
const f = r.icone_filename || r.logo_filename;
return f ? (LOGO_BASE + f) : null;
};
return (
<Modal
open={open}
title="Ajouter une plateforme"
onClose={onClose}
width={700}
footer={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<button type="button" className="ghost" onClick={() => { onClose(); onManual(); }}>
Définir manuellement
</button>
<button type="button" onClick={onClose}>Fermer</button>
</div>
}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{err && <div className="error">{err}</div>}
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: 'var(--fs-sm)', lineHeight: 1.5 }}>
Sélectionnez une plateforme depuis le référentiel pour l'ajouter à votre portefeuille.
Les informations (domiciliation, fiscalité…) seront pré-remplies automatiquement.
</p>
{/* Barre de recherche + chips filtre */}
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<input
type="search"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Rechercher une plateforme…"
autoFocus
style={{ flex: 1, boxSizing: 'border-box', padding: '9px 14px', borderRadius: 8,
border: '1.5px solid var(--border)', fontSize: 'var(--fs-sm)', background: 'var(--surface)',
color: 'var(--text)', outline: 'none' }}
/>
</div>
<div style={{ display: 'flex', gap: 6 }}>
{[
{ key: 'all', content: 'Toutes' },
{ key: 'fr', content: <span style={{ display:'inline-flex', alignItems:'center', gap:5 }}><FlagIcon code="FR" size={14} /> Françaises</span> },
{ key: 'etr', content: <span style={{ display:'inline-flex', alignItems:'center', gap:5 }}>🌍 Étrangères</span> },
].map(({ key, content }) => (
<button key={key} type="button" onClick={() => setDomFilter(key)} style={{
padding: '5px 14px', borderRadius: 20, fontSize: 'var(--fs-sm)', fontWeight: 500,
cursor: 'pointer', border: '1.5px solid',
borderColor: domFilter === key ? 'var(--primary)' : 'var(--border)',
background: domFilter === key ? 'var(--primary)' : 'var(--surface)',
color: domFilter === key ? '#fff' : 'var(--text-muted)',
transition: 'all .15s',
display: 'inline-flex', alignItems: 'center',
}}>{content}</button>
))}
</div>
{/* Grille des plateformes */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
gap: 10,
maxHeight: 380,
overflowY: 'auto',
padding: '2px 2px 4px',
}}>
{filtered.length === 0 && (
<div style={{ gridColumn: '1/-1', textAlign: 'center', color: 'var(--text-muted)',
padding: '32px 0', fontSize: 'var(--fs-sm)' }}>
Aucune plateforme trouvée pour « {search} »
</div>
)}
{filtered.map(r => {
const img = imgFor(r);
return (
<button
key={r.id}
type="button"
onClick={() => onSelect(r)}
style={{
display: 'flex', flexDirection: 'column', alignItems: 'center',
gap: 8, padding: '16px 10px 12px',
background: 'var(--surface)', border: '1.5px solid var(--border)',
borderRadius: 12, cursor: 'pointer', textAlign: 'center',
transition: 'border-color .15s, box-shadow .15s, transform .1s',
minHeight: 110,
}}
onMouseEnter={e => {
e.currentTarget.style.borderColor = 'var(--primary)';
e.currentTarget.style.boxShadow = '0 4px 16px rgba(0,0,0,0.10)';
e.currentTarget.style.transform = 'translateY(-2px)';
}}
onMouseLeave={e => {
e.currentTarget.style.borderColor = 'var(--border)';
e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.transform = 'none';
}}
>
{/* Logo / Placeholder */}
{img
? <img src={img} alt={r.nom}
style={{ width: 48, height: 48, objectFit: 'contain', borderRadius: 8, flexShrink: 0 }} />
: <div style={{
width: 48, height: 48, borderRadius: 8, flexShrink: 0,
background: 'var(--surface-2)', border: '1px solid var(--border)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 20, color: 'var(--text-muted)',
}}>🏦</div>
}
{/* Nom */}
<span style={{
fontSize: 'var(--fs-sm)', fontWeight: 600, color: 'var(--text)',
lineHeight: 1.2, wordBreak: 'break-word',
}}>
{r.nom}
</span>
</button>
);
})}
</div>
{referentiel.length > 0 && (
<p style={{ margin: 0, fontSize: 11, color: 'var(--text-muted)', textAlign: 'center' }}>
{referentiel.length} plateforme{referentiel.length > 1 ? 's' : ''} dans le référentiel
</p>
)}
</div>
</Modal>
);
}
/* ── Formulaire plateforme partagé (création + édition) ─────────── */
function PlatForm({ state, setter, logoFile, setLogoFile, logoPreview, setLogoPreview,
investisseurs, referentiel, categoriesInv, secteursInv, isEdit = false, onDelLogo }) {
const isFrance = state.domiciliation === 'FR';
const handleLogoChange = (e) => {
const file = e.target.files[0];
if (!file) return;
setLogoFile(file);
const reader = new FileReader();
reader.onload = ev => setLogoPreview(ev.target.result);
reader.readAsDataURL(file);
};
const row = (label, content, hint) => (
<div>
<label style={{ display: 'block', fontWeight: 600, marginBottom: 4, fontSize: 'var(--fs-sm)' }}>{label}</label>
{content}
{hint && <p style={{ margin: '3px 0 0', fontSize: 11, color: 'var(--text-muted)' }}>{hint}</p>}
</div>
);
return (
<>
{/* Nom */}
{row('Nom *', (
<input required value={state.nom} onChange={e => setter(s => ({ ...s, nom: e.target.value }))}
placeholder="Ex: Lendosphere" style={{ width: '100%', boxSizing: 'border-box' }} />
))}
{/* Référentiel */}
{referentiel.length > 0 && row('Lier au référentiel', (
<select value={state.referentiel_id ?? ''} onChange={e => {
const rid = e.target.value ? Number(e.target.value) : null;
const ref = referentiel.find(r => r.id === rid);
setter(s => ({ ...s, referentiel_id: rid, ...(ref && !s.nom ? { nom: ref.nom } : {}) }));
}} style={{ width: '100%' }}>
<option value="">— Aucun —</option>
{referentiel.map(r => <option key={r.id} value={r.id}>{r.nom}</option>)}
</select>
))}
{/* URL */}
{row('URL', (
<input type="url" value={state.url || ''} onChange={e => setter(s => ({ ...s, url: e.target.value }))}
placeholder="https://..." style={{ width: '100%', boxSizing: 'border-box' }} />
))}
{/* Domiciliation */}
{row('Domiciliation', (
<CountrySelect
value={state.domiciliation || ''}
onChange={code => setter(s => applyDomiciliationChange(s, code))}
placeholder="Pays de la plateforme"
/>
))}
{/* Fiscalité */}
{row('Fiscalité', (
<select value={state.fiscalite || 'flat_tax'} disabled={isFrance}
onChange={e => setter(s => applyFiscaliteChange(s, e.target.value))}
style={{ width: '100%' }}>
{isFrance
? <option value="flat_tax">Flat Tax (France)</option>
: <>
<option value="sans_fiscalite_locale">Sans fiscalité locale</option>
<option value="avec_fiscalite_locale">Avec fiscalité locale</option>
</>
}
</select>
), isFrance ? 'Forcée à Flat Tax pour les plateformes françaises.' : null)}
{/* Taux fiscalité locale */}
{state.fiscalite === 'avec_fiscalite_locale' && row('Taux de fiscalité locale (%)', (
<input type="number" min="0" max="100" step="0.1"
value={state.taux_fiscalite_locale || ''}
onChange={e => setter(s => ({ ...s, taux_fiscalite_locale: e.target.value }))}
placeholder="Ex: 15" style={{ width: '100%', boxSizing: 'border-box' }} />
))}
{/* Type produit fiscal */}
{row('Case fiscale (déclaration)', (
<select value={state.type_produit_fiscal || '2TT'} onChange={e => setter(s => ({ ...s, type_produit_fiscal: e.target.value }))}
style={{ width: '100%' }}>
<option value="2TT">2TT — Intérêts soumis PFL/PFU</option>
<option value="2TR">2TR — Intérêts soumis au barème</option>
<option value="2BH">2BH — Intérêts bruts avant PFL</option>
<option value="2CK">2CK — PFL déjà prélevé</option>
<option value="2TY">2TY — Revenus de capitaux (autre)</option>
</select>
))}
{/* Méthode de remboursement */}
{row('Versement des remboursements', (
<select value={state.methode_remboursement || 'portefeuille'} onChange={e => setter(s => ({ ...s, methode_remboursement: e.target.value }))}
style={{ width: '100%' }}>
<option value="portefeuille">Porte-monnaie de la plateforme</option>
<option value="compte_courant">Compte courant de l'investisseur</option>
<option value="choix_investisseur">Au choix de l'investisseur</option>
</select>
))}
{/* Type prêt par défaut */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
{row('Type de prêt par défaut', (
<select value={state.type_pret_defaut || ''} onChange={e => setter(s => ({ ...s, type_pret_defaut: e.target.value }))}
style={{ width: '100%' }}>
<option value="">— Non défini —</option>
<option value="in_fine">In fine</option>
<option value="amortissable">Amortissable</option>
<option value="differe">Différé</option>
</select>
))}
{row('Fréquence intérêts par défaut', (
<select value={state.freq_interets_defaut || ''} onChange={e => setter(s => ({ ...s, freq_interets_defaut: e.target.value }))}
style={{ width: '100%' }}>
<option value="">— Non défini —</option>
<option value="mensuel">Mensuel</option>
<option value="trimestriel">Trimestriel</option>
<option value="in_fine">In fine</option>
</select>
))}
</div>
{/* Date d'ouverture */}
{row("Date d'ouverture du compte", (
<input type="date" value={state.date_ouverture || ''} onChange={e => setter(s => ({ ...s, date_ouverture: e.target.value }))}
style={{ width: '100%', boxSizing: 'border-box' }} />
))}
{/* Détenteur */}
{investisseurs.length > 1 && row('Détenteur', (
<InvSelect
value={state.investisseur_id}
onChange={id => setter(s => ({ ...s, investisseur_id: id }))}
investisseurs={investisseurs}
placeholder="— Sélectionner —"
/>
))}
{/* Catégories d'investissement (édition seulement) */}
{isEdit && categoriesInv.length > 0 && row('Catégories', (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{categoriesInv.map(c => {
const inherited = (state.inherited_cat_ids || []).includes(c.id);
const checked = (state.categories_inv_ids || []).includes(c.id);
return (
<label key={c.id} style={{ display: 'flex', alignItems: 'center', gap: 4, cursor: inherited ? 'default' : 'pointer',
opacity: inherited ? 0.6 : 1 }}>
<input type="checkbox" checked={checked} disabled={inherited} style={{ width: 'auto' }}
onChange={e => setter(s => ({
...s,
categories_inv_ids: e.target.checked
? [...(s.categories_inv_ids || []), c.id]
: (s.categories_inv_ids || []).filter(id => id !== c.id),
}))} />
<span className="chip-cat" style={{ fontSize: 11 }}>{c.nom}{inherited ? ' ↗' : ''}</span>
</label>
);
})}
</div>
))}
{/* Secteurs (édition seulement) */}
{isEdit && secteursInv.length > 0 && row('Secteurs', (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{secteursInv.map(s => {
const inherited = (state.inherited_sect_ids || []).includes(s.id);
const checked = (state.secteurs_inv_ids || []).includes(s.id);
return (
<label key={s.id} style={{ display: 'flex', alignItems: 'center', gap: 4, cursor: inherited ? 'default' : 'pointer',
opacity: inherited ? 0.6 : 1 }}>
<input type="checkbox" checked={checked} disabled={inherited} style={{ width: 'auto' }}
onChange={e => setter(prev => ({
...prev,
secteurs_inv_ids: e.target.checked
? [...(prev.secteurs_inv_ids || []), s.id]
: (prev.secteurs_inv_ids || []).filter(id => id !== s.id),
}))} />
<span className="chip-sect" style={{ fontSize: 11 }}>{s.nom}{inherited ? ' ↗' : ''}</span>
</label>
);
})}
</div>
))}
{/* Logo */}
{row('Logo', (
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
{(logoPreview || (isEdit && state.logo_filename)) && (
<img src={logoPreview || (LOGO_BASE + state.logo_filename)} alt="logo"
style={{ width: 40, height: 40, objectFit: 'contain', borderRadius: 4, border: '1px solid var(--border)' }} />
)}
<input type="file" accept="image/*" onChange={handleLogoChange} style={{ flex: 1 }} />
{isEdit && state.logo_filename && !logoFile && (
<button type="button" className="danger" style={{ padding: '4px 10px', fontSize: 12 }} onClick={onDelLogo}>Supprimer</button>
)}
</div>
))}
</>
);
}
export default function PlateformesSection() { export default function PlateformesSection() {
// ── State ────────────────────────────────────────────────────── // ── State ──────────────────────────────────────────────────────
const [plats, setPlats] = useState([]); const [plats, setPlats] = useState([]);
@@ -285,6 +640,7 @@ export default function PlateformesSection() {
const [editPlatLogoFile, setEditPlatLogoFile] = useState(null); const [editPlatLogoFile, setEditPlatLogoFile] = useState(null);
const [editPlatLogoPreview, setEditPlatLogoPreview] = useState(null); const [editPlatLogoPreview, setEditPlatLogoPreview] = useState(null);
const [selectedPlat, setSelectedPlat] = useState(null); const [selectedPlat, setSelectedPlat] = useState(null);
const [showAddPicker, setShowAddPicker] = useState(false);
const [showNewPlat, setShowNewPlat] = useState(false); const [showNewPlat, setShowNewPlat] = useState(false);
const [platOpenMenu, setPlatOpenMenu] = useState(null); // { plat, x, y } const [platOpenMenu, setPlatOpenMenu] = useState(null); // { plat, x, y }
const [platExporting, setPlatExporting] = useState(false); const [platExporting, setPlatExporting] = useState(false);
@@ -405,6 +761,23 @@ export default function PlateformesSection() {
return api.upload(`/plateformes/${platId}/logo`, fd); return api.upload(`/plateformes/${platId}/logo`, fd);
}; };
/* ── Ajouter depuis le référentiel ──────────────────────────── */
const addPlatFromRef = async (ref) => {
try {
await api.post('/plateformes', {
nom: ref.nom,
domiciliation: ref.domiciliation || 'FR',
fiscalite: ref.fiscalite || 'flat_tax',
taux_fiscalite_locale: ref.taux_fiscalite_locale ?? null,
type_produit_fiscal: ref.type_produit_fiscal || '2TT',
methode_remboursement: 'portefeuille',
referentiel_id: ref.id,
});
setShowAddPicker(false);
await load();
} catch (e) { setErr(e.message); }
};
/* ── Plateformes ─────────────────────────────────────────────── */ /* ── Plateformes ─────────────────────────────────────────────── */
const addPlat = async (e) => { const addPlat = async (e) => {
e.preventDefault(); setErr(null); setMsg(null); e.preventDefault(); setErr(null); setMsg(null);
@@ -590,7 +963,7 @@ export default function PlateformesSection() {
<input ref={platImportRef} type="file" accept=".zip" style={{ display: 'none' }} <input ref={platImportRef} type="file" accept=".zip" style={{ display: 'none' }}
onChange={e => { const f = e.target.files[0]; e.target.value = ''; if (f) handlePlatImportZip(f); }} /> onChange={e => { const f = e.target.files[0]; e.target.value = ''; if (f) handlePlatImportZip(f); }} />
<button className="primary" type="button" <button className="primary" type="button"
onClick={() => { setNewPlat(EMPTY_PLAT); setErr(null); setShowNewPlat(true); }}> onClick={() => { setErr(null); setShowAddPicker(true); }}>
+ Ajouter + Ajouter
</button> </button>
</div> </div>
@@ -724,6 +1097,75 @@ export default function PlateformesSection() {
</div> </div>
</> </>
)} )}
{/* ── Picker : choisir depuis le référentiel ou manuellement ── */}
<PlatPickerModal
open={showAddPicker}
onClose={() => setShowAddPicker(false)}
referentiel={referentiel}
onSelect={addPlatFromRef}
onManual={() => { setNewPlat(EMPTY_PLAT); setErr(null); setShowNewPlat(true); }}
err={err}
/>
{/* ── Modal : Ajouter une plateforme ── */}
<Modal
open={showNewPlat}
title="Nouvelle plateforme"
onClose={() => { setShowNewPlat(false); setNewPlat(EMPTY_PLAT); setNewPlatLogoFile(null); setNewPlatLogoPreview(null); setErr(null); }}
width={600}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', width: '100%', gap: 8 }}>
<button type="button" onClick={() => { setShowNewPlat(false); setNewPlat(EMPTY_PLAT); setNewPlatLogoFile(null); setNewPlatLogoPreview(null); setErr(null); }}>Annuler</button>
<button className="primary" form="form-new-plat" type="submit">Créer</button>
</div>
}
>
<form id="form-new-plat" onSubmit={addPlat} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
{err && <div className="error">{err}</div>}
<PlatForm
state={newPlat} setter={setNewPlat}
logoFile={newPlatLogoFile} setLogoFile={setNewPlatLogoFile}
logoPreview={newPlatLogoPreview} setLogoPreview={setNewPlatLogoPreview}
investisseurs={investisseurs} referentiel={referentiel}
categoriesInv={[]} secteursInv={[]}
/>
</form>
</Modal>
{/* ── Modal : Modifier une plateforme ── */}
<Modal
open={!!editPlat}
title="Modifier la plateforme"
onClose={() => { setEditPlat(null); setEditPlatLogoFile(null); setEditPlatLogoPreview(null); setErr(null); }}
width={600}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<button className="danger" type="button" onClick={() => { const p = editPlat; setEditPlat(null); delPlat(p.id); }}>Supprimer</button>
<div style={{ display: 'flex', gap: 8 }}>
<button type="button" onClick={() => { setEditPlat(null); setEditPlatLogoFile(null); setEditPlatLogoPreview(null); setErr(null); }}>Annuler</button>
<button className="primary" form="form-edit-plat" type="submit">Enregistrer</button>
</div>
</div>
}
>
{editPlat && (
<form id="form-edit-plat" onSubmit={saveEditPlat} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
{err && <div className="error">{err}</div>}
<PlatForm
state={editPlat} setter={setEditPlat}
logoFile={editPlatLogoFile} setLogoFile={setEditPlatLogoFile}
logoPreview={editPlatLogoPreview} setLogoPreview={setEditPlatLogoPreview}
investisseurs={investisseurs} referentiel={referentiel}
categoriesInv={categoriesInv} secteursInv={secteursInv}
isEdit
onDelLogo={() => delLogoPlat(editPlat.id)}
/>
</form>
)}
</Modal>
<ConfirmModal <ConfirmModal
open={!!confirmDelete} open={!!confirmDelete}
title="Supprimer la plateforme" title="Supprimer la plateforme"