import { useState, useEffect, useRef } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { api } from '../../api.js'; import InvSelect from '../../components/InvSelect.jsx'; import Modal from '../../components/Modal.jsx'; import ConfirmModal from '../../components/ConfirmModal.jsx'; import CountrySelect, { COUNTRIES, FlagIcon } from '../../components/CountrySelect.jsx'; import ResultBanner from '../../components/ResultBanner.jsx'; import { useInvestisseur } from '../../context/InvestisseurContext.jsx'; import { memberLabel, fmtDate } from '../../utils/format.js'; function dlBlob(content, filename, type) { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } const countryLabel = code => COUNTRIES.find(c => c.code === code)?.name ?? code ?? '—'; const FISCALITE_LABELS = { flat_tax: 'Flat Tax', sans_fiscalite_locale: 'Sans fiscalité locale', avec_fiscalite_locale: 'Avec fiscalité locale', }; const METHODE_REMB_LABELS = { portefeuille: 'Porte-monnaie de la plateforme', compte_courant: "Compte courant de l'investisseur", choix_investisseur: "Au choix de l'investisseur (sur la plateforme)", }; /** Reconstruit un objet investisseur minimal depuis les colonnes dénormalisées de la plateforme */ function platInvestisseur(p) { if (!p.investisseur_id) return null; return { id: p.investisseur_id, nom: p.investisseur_nom, prenom: p.investisseur_prenom, type: p.investisseur_type, type_fiscal: p.investisseur_type_fiscal }; } function fmtFiscalite(p) { if (!p.fiscalite) return '—'; const base = FISCALITE_LABELS[p.fiscalite] ?? p.fiscalite; if (p.fiscalite === 'avec_fiscalite_locale' && p.taux_fiscalite_locale != null) { return `${base} (${p.taux_fiscalite_locale} %)`; } return base; } const EMPTY_PLAT = { nom: '', url: '', domiciliation: 'france', fiscalite: 'flat_tax', taux_fiscalite_locale: '', type_produit_fiscal: '2TT', methode_remboursement: 'portefeuille', investisseur_id: null, date_ouverture: '', logo_filename: null, type_pret_defaut: '', freq_interets_defaut: '', referentiel_id: null }; const LOGO_BASE = (import.meta.env.VITE_API_URL || '/api').replace(/\/api$/, '') + '/api/logos/'; const logoUrl = (filename) => filename ? LOGO_BASE + filename : null; function applyDomiciliationChange(state, newDomicil) { const next = { ...state, domiciliation: newDomicil }; if (newDomicil === 'FR') { next.fiscalite = 'flat_tax'; next.taux_fiscalite_locale = ''; } else if (state.fiscalite === 'flat_tax') { next.fiscalite = 'sans_fiscalite_locale'; next.taux_fiscalite_locale = ''; } return next; } function applyFiscaliteChange(state, newFiscalite) { const next = { ...state, fiscalite: newFiscalite }; if (newFiscalite !== 'avec_fiscalite_locale') next.taux_fiscalite_locale = ''; return next; } function platsToCSV(plats) { const BOM = ''; const sep = ';'; const q = v => `"${String(v ?? '').replace(/"/g, '""')}"`; const headers = ['ID', 'Nom', 'URL', 'Domiciliation', 'Fiscalité', 'Taux fiscal local (%)', 'Créé le']; const rows = plats.map(p => [ p.id, p.nom, p.url || '', countryLabel(p.domiciliation), FISCALITE_LABELS[p.fiscalite] ?? p.fiscalite ?? '', p.taux_fiscalite_locale ?? '', p.created_at ? new Date(p.created_at).toLocaleDateString('fr-FR') : '', ]); return BOM + [headers, ...rows].map(r => r.map(q).join(sep)).join('\r\n'); } function platsToXLS(plats) { const esc = v => String(v ?? '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); const cell = (v, t = 'String') => `${esc(v)}`; const mkRow = cells => ` ${cells.join('')}`; const header = mkRow(['ID','Nom','URL','Domiciliation','Fiscalité','Taux fiscal local (%)','Créé le'].map(h => cell(h))); const dataRows = plats.map(p => mkRow([ cell(p.id, 'Number'), cell(p.nom), cell(p.url || ''), cell(countryLabel(p.domiciliation)), cell(FISCALITE_LABELS[p.fiscalite] ?? p.fiscalite ?? ''), cell(p.taux_fiscalite_locale ?? ''), cell(p.created_at ? new Date(p.created_at).toLocaleDateString('fr-FR') : ''), ])).join('\n'); return ` ${header} ${dataRows}
`; } /* ── Icônes utilitaires ──────────────────────────────────────── */ function IconExport() { return ; } function IconCSV() { return ; } function IconXLS() { return ; } function IconJSON() { return ; } function IconImport() { return ; } /* ── ExportDropdown ──────────────────────────────────────────── */ function ExportDropdown({ disabled, onCSV, onXLS, onJSON, title = 'Exporter' }) { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { if (!open) return; const handler = (e) => { if (!ref.current?.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [open]); const choose = (fn) => { setOpen(false); fn(); }; return (
{open && (
{onJSON && ( )}
)}
); } /* ── Helpers PFU ─────────────────────────────────────────────── */ function PlatDetailPanel({ plat, onEdit }) { const navigate = useNavigate(); const [platCatsInv, setPlatCatsInv] = useState([]); const [platSectsInv, setPlatSectsInv] = useState([]); useEffect(() => { if (!plat) { setPlatCatsInv([]); setPlatSectsInv([]); return; } Promise.all([ api.get(`/plateformes/${plat.id}/categories-inv`).catch(() => []), api.get(`/plateformes/${plat.id}/secteurs-inv`).catch(() => []), ]).then(([cats, sects]) => { setPlatCatsInv(cats); setPlatSectsInv(sects); }); }, [plat?.id]); if (!plat) return (
Sélectionnez une plateforme
); const inv = platInvestisseur(plat); const fields = [ { label: 'Plateforme', value: plat.nom }, plat.url && { label: 'Site web', value: {plat.url}, }, { label: 'Détenteur', value: inv ? memberLabel(inv) : '—' }, plat.date_ouverture && { label: "Date d'ouverture", value: fmtDate(plat.date_ouverture) }, { label: 'Domiciliation', value: plat.domiciliation ? {countryLabel(plat.domiciliation)} : '—' }, { label: 'Fiscalité', value: fmtFiscalite(plat) }, plat.domiciliation === 'FR' && { label: 'Déclaration 2561', value: (plat.type_produit_fiscal ?? '2TT') === '2TR' ? 'Case 2TR — Produits de placement à revenu fixe' : 'Case 2TT — Produits des minibons et prêts participatifs', }, { label: 'Méthode de remboursement', value: METHODE_REMB_LABELS[plat.methode_remboursement] ?? '—' }, platCatsInv.length > 0 && { label: "Catégories d'investissement", value:
{platCatsInv.map(c => {c.nom})}
, }, platSectsInv.length > 0 && { label: "Secteurs d'investissement", value:
{platSectsInv.map(s => {s.nom})}
, }, plat.type_pret_defaut && { label: 'Type de prêt (défaut)', value: { in_fine: 'In fine', amortissable: 'Amortissable', differe: 'Différé' }[plat.type_pret_defaut] ?? plat.type_pret_defaut }, plat.freq_interets_defaut && { label: 'Périodicité (défaut)', value: { mensuel: 'Mensuelle', trimestriel: 'Trimestrielle', in_fine: 'In fine' }[plat.freq_interets_defaut] ?? plat.freq_interets_defaut }, { label: 'Investissements', value: plat.nb_investissements != null ? `${plat.nb_investissements} investissement${plat.nb_investissements !== 1 ? 's' : ''}` : '—', }, plat.notes && { label: 'Notes', value: plat.notes }, ].filter(Boolean); const logo = logoUrl(plat.logo_filename); return (
{logo && (
{`Logo { e.currentTarget.style.display = 'none'; }} />
)}
Détail de la plateforme {plat.referentiel_id && ( )}
{fields.map(f => (
{f.label} {f.value}
))}
); } /* ── 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 ( } >
{err &&
{err}
}

Sélectionnez une plateforme depuis le référentiel pour l'ajouter à votre portefeuille. Les informations (domiciliation, fiscalité…) seront pré-remplies automatiquement.

{/* Barre de recherche + chips filtre */}
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' }} />
{[ { key: 'all', content: 'Toutes' }, { key: 'fr', content: Françaises }, { key: 'etr', content: 🌍 Étrangères }, ].map(({ key, content }) => ( ))}
{/* Grille des plateformes */}
{filtered.length === 0 && (
Aucune plateforme trouvée pour « {search} »
)} {filtered.map(r => { const img = imgFor(r); return ( ); })}
{referentiel.length > 0 && (

{referentiel.length} plateforme{referentiel.length > 1 ? 's' : ''} dans le référentiel

)}
); } /* ── 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) => (
{content} {hint &&

{hint}

}
); return ( <> {/* Nom */} {row('Nom *', ( 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', ( ))} {/* URL */} {row('URL', ( setter(s => ({ ...s, url: e.target.value }))} placeholder="https://..." style={{ width: '100%', boxSizing: 'border-box' }} /> ))} {/* Domiciliation */} {row('Domiciliation', ( setter(s => applyDomiciliationChange(s, code))} placeholder="Pays de la plateforme" /> ))} {/* Fiscalité */} {row('Fiscalité', ( ), isFrance ? 'Forcée à Flat Tax pour les plateformes françaises.' : null)} {/* Taux fiscalité locale */} {state.fiscalite === 'avec_fiscalite_locale' && row('Taux de fiscalité locale (%)', ( 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)', ( ))} {/* Méthode de remboursement */} {row('Versement des remboursements', ( ))} {/* Type prêt par défaut */}
{row('Type de prêt par défaut', ( ))} {row('Fréquence intérêts par défaut', ( ))}
{/* Date d'ouverture */} {row("Date d'ouverture du compte", ( setter(s => ({ ...s, date_ouverture: e.target.value }))} style={{ width: '100%', boxSizing: 'border-box' }} /> ))} {/* Détenteur */} {investisseurs.length > 1 && row('Détenteur', ( setter(s => ({ ...s, investisseur_id: id }))} investisseurs={investisseurs} placeholder="— Sélectionner —" /> ))} {/* Catégories d'investissement (édition seulement) */} {isEdit && categoriesInv.length > 0 && row('Catégories', (
{categoriesInv.map(c => { const inherited = (state.inherited_cat_ids || []).includes(c.id); const checked = (state.categories_inv_ids || []).includes(c.id); return ( ); })}
))} {/* Secteurs (édition seulement) */} {isEdit && secteursInv.length > 0 && row('Secteurs', (
{secteursInv.map(s => { const inherited = (state.inherited_sect_ids || []).includes(s.id); const checked = (state.secteurs_inv_ids || []).includes(s.id); return ( ); })}
))} {/* Logo */} {row('Logo', (
{(logoPreview || (isEdit && state.logo_filename)) && ( logo )} {isEdit && state.logo_filename && !logoFile && ( )}
))} ); } export default function PlateformesSection() { const [searchParams, setSearchParams] = useSearchParams(); // ── State ────────────────────────────────────────────────────── const [plats, setPlats] = useState([]); const [investisseurs, setInvestisseurs] = useState([]); const [referentiel, setReferentiel] = useState([]); const [newPlat, setNewPlat] = useState(EMPTY_PLAT); const [newPlatLogoFile, setNewPlatLogoFile] = useState(null); const [newPlatLogoPreview, setNewPlatLogoPreview] = useState(null); const [editPlat, setEditPlat] = useState(null); const [editPlatLogoFile, setEditPlatLogoFile] = useState(null); const [editPlatLogoPreview, setEditPlatLogoPreview] = useState(null); const [selectedPlat, setSelectedPlat] = useState(null); const [showAddPicker, setShowAddPicker] = useState(false); const [showNewPlat, setShowNewPlat] = useState(false); const [platOpenMenu, setPlatOpenMenu] = useState(null); // { plat, x, y } const [platExporting, setPlatExporting] = useState(false); const [platImportResult, setPlatImportResult] = useState(null); // Catégories d'investissement (globales + privées) const [categoriesInv, setCategoriesInv] = useState([]); const [selectedCatInv, setSelectedCatInv] = useState(null); const [editingCatInv, setEditingCatInv] = useState(null); // id en cours d'édition const [editingNomCatInv, setEditingNomCatInv] = useState(''); const [newCatInvNom, setNewCatInvNom] = useState(''); const [showNewCatInv, setShowNewCatInv] = useState(false); // Secteurs d'investissement (globaux + privés) const [secteursInv, setSecteursInv] = useState([]); const [selectedSectInv, setSelectedSectInv] = useState(null); const [editingSectInv, setEditingSectInv] = useState(null); const [editingNomSectInv, setEditingNomSectInv] = useState(''); const [newSectInvNom, setNewSectInvNom] = useState(''); const [showNewSectInv, setShowNewSectInv] = useState(false); // PFU const [showPfoDetail, setShowPfoDetail] = useState(false); const [err, setErr] = useState(null); const [msg, setMsg] = useState(null); const [confirmDelete, setConfirmDelete] = useState(null); const platImportRef = useRef(null); const load = async () => { const [p, invs, ref, catInv, sectInv] = await Promise.all([ api.get('/plateformes'), api.get('/investisseurs'), api.get('/plateformes/referentiel-list'), api.get('/categories-inv'), api.get('/secteurs-inv'), ]); setPlats(p); setInvestisseurs(invs); setReferentiel(ref); setCategoriesInv(catInv); setSecteursInv(sectInv); setSelectedPlat(prev => prev ? (p.find(x => x.id === prev.id) ?? null) : null); }; useEffect(() => { load(); }, []); // eslint-disable-line useEffect(() => { if (plats.length === 0) return; setSelectedPlat(prev => prev ? prev : plats[0]); }, [plats]); // eslint-disable-line // Ouverture automatique du picker d'ajout (venant de l'état vide "Aucune plateforme configurée") useEffect(() => { if (searchParams.get('openAdd') === '1') { setErr(null); setShowAddPicker(true); const next = new URLSearchParams(searchParams); next.delete('openAdd'); setSearchParams(next, { replace: true }); } }, [searchParams]); // eslint-disable-line const handleLogoFile = (file, setFile, setPreview) => { if (!file) { setFile(null); setPreview(null); return; } setFile(file); const reader = new FileReader(); reader.onload = (ev) => setPreview(ev.target.result); reader.readAsDataURL(file); }; /* ── Catégories d'investissement (privées) ──────────────────────── */ const saveCatInv = async (nom) => { if (!nom.trim()) return; try { const row = await api.post('/categories-inv', { nom: nom.trim() }); setCategoriesInv(prev => [...prev, row].sort((a, b) => b.is_global - a.is_global || a.nom.localeCompare(b.nom))); setShowNewCatInv(false); setNewCatInvNom(''); setErr(null); } catch (e) { setErr(e.message || 'Erreur'); } }; const renameCatInv = async (id, nom) => { if (!nom.trim()) return; try { await api.put(`/categories-inv/${id}`, { nom: nom.trim() }); setCategoriesInv(prev => prev.map(c => c.id === id ? { ...c, nom: nom.trim() } : c)); setEditingCatInv(null); } catch (e) { setErr(e.message || 'Erreur'); } }; const delCatInv = async (id) => { try { await api.del(`/categories-inv/${id}`); setCategoriesInv(prev => prev.filter(c => c.id !== id)); if (selectedCatInv?.id === id) setSelectedCatInv(null); } catch (e) { setErr(e.message || 'Erreur'); } }; /* ── Secteurs d'investissement (privés) ──────────────────────── */ const saveSectInv = async (nom) => { if (!nom.trim()) return; try { const row = await api.post('/secteurs-inv', { nom: nom.trim() }); setSecteursInv(prev => [...prev, row].sort((a, b) => b.is_global - a.is_global || a.nom.localeCompare(b.nom))); setShowNewSectInv(false); setNewSectInvNom(''); setErr(null); } catch (e) { setErr(e.message || 'Erreur'); } }; const renameSectInv = async (id, nom) => { if (!nom.trim()) return; try { await api.put(`/secteurs-inv/${id}`, { nom: nom.trim() }); setSecteursInv(prev => prev.map(s => s.id === id ? { ...s, nom: nom.trim() } : s)); setEditingSectInv(null); } catch (e) { setErr(e.message || 'Erreur'); } }; const delSectInv = async (id) => { try { await api.del(`/secteurs-inv/${id}`); setSecteursInv(prev => prev.filter(s => s.id !== id)); if (selectedSectInv?.id === id) setSelectedSectInv(null); } catch (e) { setErr(e.message || 'Erreur'); } }; /** Upload le logo vers le serveur après que la plateforme a été créée/sauvée */ const uploadLogo = async (platId, file) => { if (!file) return null; const fd = new FormData(); fd.append('logo', file); 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 ─────────────────────────────────────────────── */ const addPlat = async (e) => { e.preventDefault(); setErr(null); setMsg(null); try { const created = await api.post('/plateformes', { ...newPlat, taux_fiscalite_locale: newPlat.fiscalite === 'avec_fiscalite_locale' && newPlat.taux_fiscalite_locale !== '' ? Number(newPlat.taux_fiscalite_locale) : null, methode_remboursement: newPlat.methode_remboursement || 'portefeuille', investisseur_id: newPlat.investisseur_id ? Number(newPlat.investisseur_id) : null, date_ouverture: newPlat.date_ouverture || null, type_pret_defaut: newPlat.type_pret_defaut || null, freq_interets_defaut: newPlat.freq_interets_defaut || null, referentiel_id: newPlat.referentiel_id ? Number(newPlat.referentiel_id) : null, }); if (newPlatLogoFile) await uploadLogo(created.id, newPlatLogoFile); setNewPlat(EMPTY_PLAT); setNewPlatLogoFile(null); setNewPlatLogoPreview(null); setShowNewPlat(false); await load(); } catch (e) { setErr(e.message); } }; const delPlat = (id) => { setConfirmDelete({ message: 'Supprimer cette plateforme ?', onConfirm: async () => { try { await api.del(`/plateformes/${id}`); setSelectedPlat(null); await load(); } catch (e) { setErr(e.message); } finally { setConfirmDelete(null); } }, }); }; // ── Export/Import ZIP plateformes ───────────────────────────────────── const handlePlatExportAll = async () => { try { setPlatExporting(true); const blob = await api.blob('/plateformes/export'); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `plateformes-${new Date().toISOString().slice(0, 10)}.zip`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (e) { setPlatImportResult({ ok: false, msg: e.message }); } finally { setPlatExporting(false); } }; const handlePlatExportOne = async (plat) => { try { const blob = await api.blob(`/plateformes/${plat.id}/export`); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${plat.nom.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${new Date().toISOString().slice(0, 10)}.zip`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (e) { setPlatImportResult({ ok: false, msg: e.message }); } }; const handlePlatImportZip = async (file) => { try { const fd = new FormData(); fd.append('file', file); const r = await api.upload('/plateformes/import-zip', fd); setPlatImportResult({ ok: true, msg: `Import terminé : ${r.created} créée(s), ${r.updated} mise(s) à jour sur ${r.total} entrée(s).` }); load(); } catch (e) { setPlatImportResult({ ok: false, msg: e.message }); } }; const openEditPlat = async (p) => { setEditPlatLogoFile(null); setEditPlatLogoPreview(null); // Charger les associations catégories/secteurs d'investissement de la plateforme const [platCatsInv, platSectsInv] = await Promise.all([ api.get(`/plateformes/${p.id}/categories-inv`).catch(() => []), api.get(`/plateformes/${p.id}/secteurs-inv`).catch(() => []), ]); setEditPlat({ id: p.id, nom: p.nom, url: p.url || '', categories_inv_ids: platCatsInv.map(c => c.id), secteurs_inv_ids: platSectsInv.map(s => s.id), inherited_cat_ids: platCatsInv.filter(c => c.is_inherited).map(c => c.id), inherited_sect_ids: platSectsInv.filter(s => s.is_inherited).map(s => s.id), domiciliation: p.domiciliation || 'france', fiscalite: p.fiscalite || 'flat_tax', taux_fiscalite_locale: p.taux_fiscalite_locale ?? '', type_produit_fiscal: p.type_produit_fiscal || '2TT', methode_remboursement: p.methode_remboursement || 'portefeuille', investisseur_id: p.investisseur_id ?? null, date_ouverture: p.date_ouverture || '', logo_filename: p.logo_filename || null, type_pret_defaut: p.type_pret_defaut || '', freq_interets_defaut: p.freq_interets_defaut || '', referentiel_id: p.referentiel_id ?? null, referentiel_nom: p.referentiel_nom ?? null, overridden_fields: p.overridden_fields ?? [], }); }; const saveEditPlat = async (e) => { e.preventDefault(); setErr(null); setMsg(null); try { await api.put(`/plateformes/${editPlat.id}`, { nom: editPlat.nom, url: editPlat.url || '', domiciliation: editPlat.domiciliation, fiscalite: editPlat.fiscalite, taux_fiscalite_locale: editPlat.fiscalite === 'avec_fiscalite_locale' && editPlat.taux_fiscalite_locale !== '' ? Number(editPlat.taux_fiscalite_locale) : null, type_produit_fiscal: editPlat.type_produit_fiscal || '2TT', methode_remboursement: editPlat.methode_remboursement || 'portefeuille', investisseur_id: editPlat.investisseur_id ? Number(editPlat.investisseur_id) : null, date_ouverture: editPlat.date_ouverture || null, type_pret_defaut: editPlat.type_pret_defaut || null, freq_interets_defaut: editPlat.freq_interets_defaut || null, }); if (editPlatLogoFile) await uploadLogo(editPlat.id, editPlatLogoFile); // Sauvegarder les associations catégories/secteurs d'investissement await Promise.all([ api.put(`/plateformes/${editPlat.id}/categories-inv`, { ids: editPlat.categories_inv_ids || [] }), api.put(`/plateformes/${editPlat.id}/secteurs-inv`, { ids: editPlat.secteurs_inv_ids || [] }), ]); setMsg('Plateforme mise à jour.'); setTimeout(() => setMsg(null), 3000); setEditPlatLogoFile(null); setEditPlatLogoPreview(null); setEditPlat(null); await load(); } catch (e) { setErr(e.message); } }; const delLogoPlat = async (id) => { try { await api.del(`/plateformes/${id}/logo`); setEditPlat(ep => ep ? { ...ep, logo_filename: null } : ep); await load(); } catch (e) { setErr(e.message); } }; /* ── PFU CRUD ────────────────────────────────────────────────── */ return ( <> {err &&
{err}
} {msg &&
{msg}
}
{/* Colonne gauche — liste */}

Mes plateformes

Plateformes de crowdlending que vous utilisez.

dlBlob(platsToCSV(plats), 'plateformes.csv', 'text/csv;charset=utf-8')} onXLS={() => dlBlob(platsToXLS(plats), 'plateformes.xls', 'application/vnd.ms-excel')} /> { const f = e.target.files[0]; e.target.value = ''; if (f) handlePlatImportZip(f); }} />
{platImportResult && setPlatImportResult(null)} style={{ marginBottom: 12 }} />}
{plats.length === 0 && ( )} {plats.map((p, i) => { const imgSrc = p.icone_filename ? logoUrl(p.icone_filename) : p.logo_filename ? logoUrl(p.logo_filename) : null; const inv = platInvestisseur(p); const cats = p.categories_inv || []; const sects = p.secteurs_inv || []; return ( setSelectedPlat(selectedPlat?.id === p.id ? null : p)} style={{ borderBottom: i < plats.length - 1 ? '1px solid var(--border)' : 'none', cursor: 'pointer', background: selectedPlat?.id === p.id ? 'var(--surface-2)' : 'none', }} onMouseEnter={e => { if (selectedPlat?.id !== p.id) e.currentTarget.style.background = 'var(--surface-2)'; }} onMouseLeave={e => { if (selectedPlat?.id !== p.id) e.currentTarget.style.background = 'none'; }}> ); })}
Nom Domiciliation Catégories Secteurs Détenteur Invest.
Aucune plateforme
{imgSrc ? :
×
}
{p.nom}
{p.domiciliation ? {countryLabel(p.domiciliation)} : '—'}
{cats.slice(0, 2).map(c => {c.nom})} {cats.length > 2 && +{cats.length - 2}} {cats.length === 0 && }
{sects.slice(0, 2).map(s => {s.nom})} {sects.length > 2 && +{sects.length - 2}} {sects.length === 0 && }
{inv ? memberLabel(inv) : '—'} 0 ? 'var(--text)' : 'var(--text-muted)' }}> {p.nb_investissements ?? 0}
{/* Colonne droite — détail */}
{/* Menu ⋮ plateformes */} {platOpenMenu && ( <>
setPlatOpenMenu(null)} />
{[ { icon: , label: 'Modifier', onClick: () => { const p = platOpenMenu.plat; setPlatOpenMenu(null); openEditPlat(p); } }, { icon: , label: 'Exporter', onClick: () => { const p = platOpenMenu.plat; setPlatOpenMenu(null); handlePlatExportOne(p); } }, { icon: , label: 'Supprimer', onClick: () => { const p = platOpenMenu.plat; setPlatOpenMenu(null); delPlat(p.id); }, color: 'var(--danger)' }, ].map(({ icon, label, onClick, color }) => ( ))}
)} {/* ── Picker : choisir depuis le référentiel ou manuellement ── */} setShowAddPicker(false)} referentiel={referentiel} onSelect={addPlatFromRef} onManual={() => { setNewPlat(EMPTY_PLAT); setErr(null); setShowNewPlat(true); }} err={err} /> {/* ── Modal : Ajouter une plateforme ── */} { setShowNewPlat(false); setNewPlat(EMPTY_PLAT); setNewPlatLogoFile(null); setNewPlatLogoPreview(null); setErr(null); }} width={600} footer={
} >
{err &&
{err}
}
{/* ── Modal : Modifier une plateforme ── */} { setEditPlat(null); setEditPlatLogoFile(null); setEditPlatLogoPreview(null); setErr(null); }} width={600} footer={
} > {editPlat && (
{err &&
{err}
} delLogoPlat(editPlat.id)} /> )}
setConfirmDelete(null)} /> ); }