From 6234c76507fb9271df778a80a5b42b651a8c10f4 Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 16 Jun 2026 09:16:40 +0200 Subject: [PATCH] MAJ --- backend/src/routes/plateformes.js | 14 +- .../src/pages/settings/PlateformesSection.jsx | 444 +++++++++++++++++- 2 files changed, 452 insertions(+), 6 deletions(-) diff --git a/backend/src/routes/plateformes.js b/backend/src/routes/plateformes.js index 018b80d..c79699b 100644 --- a/backend/src/routes/plateformes.js +++ b/backend/src/routes/plateformes.js @@ -219,7 +219,7 @@ function syncCategories(platId, catIds) { router.get('/referentiel-list', (_req, res) => { const rows = db.prepare(` 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 ORDER BY pr.nom `).all(); @@ -262,13 +262,15 @@ router.post('/', (req, res, next) => { let taux = fiscalite === 'avec_fiscalite_locale' ? (body.taux_fiscalite_locale ?? null) : null; let typeProduitFiscal = body.domiciliation === 'FR' ? (body.type_produit_fiscal ?? '2TT') : '2TT'; 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) { const ref = db.prepare('SELECT * FROM plateformes_referentiel WHERE id = ?').get(referentielId); 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) - // On calcule les overrides par rapport au référentiel + logoFilename = ref.logo_filename ?? null; + iconeFilename = ref.icone_filename ?? null; } 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, methode_remboursement, investisseur_id, date_ouverture, type_pret_defaut, freq_interets_defaut, + logo_filename, icone_filename, referentiel_id, overridden_fields) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) `).run( @@ -283,8 +286,9 @@ router.post('/', (req, res, next) => { body.domiciliation, fiscalite, taux, typeProduitFiscal, body.methode_remboursement, body.investisseur_id ?? null, body.date_ouverture || null, body.type_pret_defaut ?? null, body.freq_interets_defaut ?? null, + logoFilename, iconeFilename, referentielId, - '[]' // toujours vide à la création — les overrides se calculent au premier PUT + '[]' ); const id = r.lastInsertRowid; syncCategories(id, body.categories); diff --git a/frontend/src/pages/settings/PlateformesSection.jsx b/frontend/src/pages/settings/PlateformesSection.jsx index 7f950c2..a811f71 100644 --- a/frontend/src/pages/settings/PlateformesSection.jsx +++ b/frontend/src/pages/settings/PlateformesSection.jsx @@ -273,6 +273,361 @@ function PlatDetailPanel({ plat, onEdit }) { /* ── 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() { // ── State ────────────────────────────────────────────────────── const [plats, setPlats] = useState([]); @@ -285,6 +640,7 @@ export default function PlateformesSection() { 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); @@ -405,6 +761,23 @@ export default function PlateformesSection() { 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); @@ -590,7 +963,7 @@ export default function PlateformesSection() { { const f = e.target.files[0]; e.target.value = ''; if (f) handlePlatImportZip(f); }} /> @@ -724,6 +1097,75 @@ export default function PlateformesSection() { )} + + + {/* ── 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)} + /> + + )} +
+