1202 lines
72 KiB
React
1202 lines
72 KiB
React
import { useEffect, useState } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { api } from '../api.js';
|
|
import InvSelect from '../components/InvSelect.jsx';
|
|
import { useAuth } from '../context/AuthContext.jsx';
|
|
import ResultBanner from '../components/ResultBanner.jsx';
|
|
import CountrySelect, { COUNTRIES, FlagIcon } from '../components/CountrySelect.jsx';
|
|
|
|
const LOGO_BASE = (import.meta.env.VITE_API_URL || '/api').replace(/\/api$/, '') + '/api/logos/';
|
|
|
|
const TYPES_INV = {
|
|
p2p: 'Prêt P2P',
|
|
dette: 'Dette',
|
|
equity: 'Capitaux propres',
|
|
tokenise: 'Tokenisé',
|
|
achat_louer: 'Achat à louer',
|
|
mini_obligations:'Mini-obligations',
|
|
mixte: 'Mixte',
|
|
};
|
|
const SECTEURS = {
|
|
immobilier: 'Immobilier',
|
|
pme: 'PME',
|
|
startups: 'Startups',
|
|
litige: 'Litige',
|
|
energie: 'Énergie verte',
|
|
sante_science: 'Santé & Science',
|
|
logistique: 'Logistique',
|
|
personnel: 'Prêts personnels',
|
|
art: 'Art',
|
|
autre: 'Autre',
|
|
};
|
|
const INV_TYPES = {
|
|
particulier: 'Particulier',
|
|
entreprise: 'Entreprise',
|
|
les_deux: 'Particulier & Entreprise',
|
|
};
|
|
const countryLabel = code => COUNTRIES.find(c => c.code === code)?.name ?? code ?? '—';
|
|
|
|
// ── Composants helpers ────────────────────────────────────────────────────────
|
|
|
|
function SectionTitle({ children }) {
|
|
return (
|
|
<div style={{
|
|
fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '.07em',
|
|
color: 'var(--text-muted)', marginBottom: 14, paddingBottom: 8,
|
|
borderBottom: '1px solid var(--border)',
|
|
}}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function KpiCell({ label, value, editing, input }) {
|
|
return (
|
|
<div style={{ background: 'var(--surface-2)', borderRadius: 10, padding: '14px 16px' }}>
|
|
<div style={{ fontSize: 11, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '.05em', marginBottom: 6 }}>{label}</div>
|
|
{editing
|
|
? input
|
|
: <div style={{ fontSize: 20, fontWeight: 700, color: value != null ? 'var(--text)' : 'var(--text-muted)' }}>{value ?? 'N/A'}</div>
|
|
}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function InfoRow({ label, value, editing, input }) {
|
|
return (
|
|
<div style={{
|
|
display: 'flex', flexDirection: editing ? 'column' : 'row',
|
|
justifyContent: 'space-between', gap: editing ? 4 : 8,
|
|
padding: '7px 0', borderBottom: '1px solid var(--border)',
|
|
}}>
|
|
<span style={{ fontSize: 13, color: 'var(--text-muted)', flexShrink: 0 }}>{label}</span>
|
|
{editing
|
|
? input
|
|
: <span style={{ fontSize: 13, fontWeight: 500, color: value ? 'var(--text)' : 'var(--text-muted)', textAlign: 'right', maxWidth: '60%' }}>{value ?? '—'}</span>
|
|
}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function FeatureRow({ label, field, data, editing, onChange }) {
|
|
const val = !!(data?.[field]);
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '10px 0', borderBottom: '1px solid var(--border)' }}>
|
|
{editing ? (
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer', fontSize: 14, width: '100%' }}>
|
|
<input type="checkbox" checked={val} onChange={e => onChange(field, e.target.checked)} style={{ width: 'auto' }} />
|
|
{label}
|
|
</label>
|
|
) : (
|
|
<>
|
|
<div style={{
|
|
flexShrink: 0, width: 32, height: 32, borderRadius: 8,
|
|
background: val ? '#10b981' : 'var(--surface-2)',
|
|
border: `1.5px solid ${val ? '#10b981' : 'var(--border)'}`,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
}}>
|
|
{val
|
|
? <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
}
|
|
</div>
|
|
<span style={{ fontSize: 14, fontWeight: val ? 500 : 400, color: val ? 'var(--text)' : 'var(--text-muted)' }}>{label}</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Page principale ────────────────────────────────────────────────────────────
|
|
|
|
export default function PlatformeProfile() {
|
|
const { id } = useParams();
|
|
const navigate = useNavigate();
|
|
const { isAdmin } = useAuth();
|
|
|
|
const [data, setData] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [err, setErr] = useState(null);
|
|
const [editing, setEditing] = useState(false);
|
|
const [form, setForm] = useState(null);
|
|
const [saving, setSaving] = useState(false);
|
|
const [saveErr, setSaveErr] = useState(null);
|
|
const [catsInv, setCatsInv] = useState([]); // categories_inv globales
|
|
const [secteursInv, setSecteursInv] = useState([]); // secteurs_inv globaux
|
|
|
|
useEffect(() => {
|
|
load();
|
|
if (isAdmin) {
|
|
api.get('/ref-categories').then(setCatsInv).catch(() => {});
|
|
api.get('/ref-secteurs').then(setSecteursInv).catch(() => {});
|
|
}
|
|
}, [id]);
|
|
|
|
async function load() {
|
|
try {
|
|
setLoading(true);
|
|
const endpoint = isAdmin ? `/referentiel/${id}` : `/referentiel-public/${id}`;
|
|
const d = await api.get(endpoint);
|
|
setData(d);
|
|
} catch (e) { setErr(e.message); }
|
|
finally { setLoading(false); }
|
|
}
|
|
|
|
function startEdit() {
|
|
const paysOp = Array.isArray(data.pays_operation)
|
|
? data.pays_operation.join(', ')
|
|
: (data.pays_operation || '');
|
|
setForm({
|
|
...data,
|
|
pays_operation_str: paysOp,
|
|
categories_inv_ids: (data.categories_inv || []).map(c => c.id),
|
|
secteurs_inv_ids: (data.secteurs_inv || []).map(s => s.id),
|
|
});
|
|
setEditing(true);
|
|
setSaveErr(null);
|
|
}
|
|
|
|
function cancelEdit() { setEditing(false); setForm(null); }
|
|
|
|
const setField = (key, val) => setForm(f => ({ ...f, [key]: val }));
|
|
const setInput = key => e => setField(key, e.target.type === 'checkbox' ? e.target.checked : e.target.value);
|
|
|
|
async function save() {
|
|
setSaving(true); setSaveErr(null);
|
|
try {
|
|
const paysArr = form.pays_operation_str
|
|
? form.pays_operation_str.split(',').map(s => s.trim()).filter(Boolean)
|
|
: [];
|
|
const payload = {
|
|
nom: form.nom,
|
|
url: form.url || null,
|
|
domiciliation: form.domiciliation,
|
|
fiscalite: form.fiscalite,
|
|
taux_fiscalite_locale: form.taux_fiscalite_locale !== '' && form.taux_fiscalite_locale != null ? Number(form.taux_fiscalite_locale) : null,
|
|
type_produit_fiscal: form.type_produit_fiscal || '2TT',
|
|
logo_filename: data.logo_filename ?? null,
|
|
description: form.description || null,
|
|
categories: data.categories || [],
|
|
notation: data.notation || [],
|
|
categories_inv_ids: form.categories_inv_ids || [],
|
|
secteurs_inv_ids: form.secteurs_inv_ids || [],
|
|
// Profil enrichi
|
|
annee_creation: form.annee_creation ? Number(form.annee_creation) : null,
|
|
investisseurs_types: form.investisseurs_types || null,
|
|
regulateur: form.regulateur || null,
|
|
numero_licence: form.numero_licence || null,
|
|
is_regule: form.is_regule ? 1 : 0,
|
|
pays_inscription: form.pays_inscription || null,
|
|
pays_siege: form.pays_siege || null,
|
|
pays_operation: paysArr,
|
|
investissement_minimum: form.investissement_minimum !== '' && form.investissement_minimum != null ? Number(form.investissement_minimum) : null,
|
|
rendement_annonce: form.rendement_annonce !== '' && form.rendement_annonce != null ? Number(form.rendement_annonce) : null,
|
|
nb_investisseurs: form.nb_investisseurs !== '' && form.nb_investisseurs != null ? Number(form.nb_investisseurs) : null,
|
|
volume_total_finance: form.volume_total_finance !== '' && form.volume_total_finance != null ? Number(form.volume_total_finance) : null,
|
|
duree_moyenne_pret: form.duree_moyenne_pret !== '' && form.duree_moyenne_pret != null ? Number(form.duree_moyenne_pret) : null,
|
|
garantie_rachat: form.garantie_rachat ? 1 : 0,
|
|
statistiques_publiques: form.statistiques_publiques ? 1 : 0,
|
|
bonus_inscription: form.bonus_inscription ? 1 : 0,
|
|
marche_secondaire: form.marche_secondaire ? 1 : 0,
|
|
investissement_auto: form.investissement_auto ? 1 : 0,
|
|
url_trustpilot: form.url_trustpilot || null,
|
|
url_linkedin: form.url_linkedin || null,
|
|
};
|
|
const updated = await api.put(`/referentiel/${id}`, payload);
|
|
setData(updated);
|
|
setCatsInv(await api.get('/ref-categories').catch(() => catsInv));
|
|
setEditing(false);
|
|
setForm(null);
|
|
} catch (e) { setSaveErr(e.message); }
|
|
finally { setSaving(false); }
|
|
}
|
|
|
|
if (loading) return <div style={{ padding: 40, textAlign: 'center', color: 'var(--text-muted)' }}>Chargement…</div>;
|
|
if (err) return <div className="error" style={{ margin: 24 }}>{err}</div>;
|
|
if (!data) return null;
|
|
|
|
const d = editing ? form : data;
|
|
const paysOp = Array.isArray(data.pays_operation) ? data.pays_operation : [];
|
|
|
|
return (
|
|
<div style={{ maxWidth: 1080, margin: '0 auto', padding: '24px 24px 60px' }}>
|
|
|
|
{/* ── Breadcrumb ─────────────────────────────────────────────────────── */}
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 20, fontSize: 13, color: 'var(--text-muted)' }}>
|
|
<button
|
|
onClick={() => navigate(-1)}
|
|
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px 6px', borderRadius: 4, color: 'var(--text-muted)', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}
|
|
onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-2)'}
|
|
onMouseLeave={e => e.currentTarget.style.background = 'none'}
|
|
>
|
|
← Retour
|
|
</button>
|
|
<span>/</span>
|
|
<span>Référentiel</span>
|
|
<span>/</span>
|
|
<span style={{ color: 'var(--text)', fontWeight: 500 }}>{data.nom}</span>
|
|
</div>
|
|
|
|
{/* ── Hero ───────────────────────────────────────────────────────────── */}
|
|
<div className="card" style={{ padding: '24px 28px', marginBottom: 20 }}>
|
|
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 20 }}>
|
|
|
|
{/* Logo */}
|
|
<div style={{
|
|
flexShrink: 0, width: 80, height: 80, borderRadius: 14,
|
|
border: '1px solid var(--border)', background: 'var(--surface-2)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden',
|
|
}}>
|
|
{data.logo_filename
|
|
? <img src={LOGO_BASE + data.logo_filename} alt={data.nom}
|
|
style={{ width: 68, height: 68, objectFit: 'contain' }}
|
|
onError={e => { e.currentTarget.style.display = 'none'; }} />
|
|
: <span style={{ fontSize: 26, fontWeight: 800, color: 'var(--primary)', userSelect: 'none' }}>
|
|
{data.nom[0].toUpperCase()}
|
|
</span>
|
|
}
|
|
</div>
|
|
|
|
{/* Titre + badges */}
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 6 }}>
|
|
{editing
|
|
? <input value={form.nom} onChange={setInput('nom')}
|
|
style={{ fontSize: 22, fontWeight: 700, border: '1px solid var(--border)', borderRadius: 6, padding: '4px 10px', background: 'var(--surface-2)' }} />
|
|
: <h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>{data.nom}</h1>
|
|
}
|
|
{data.is_regule
|
|
? <span style={{ padding: '3px 10px', borderRadius: 20, background: 'rgba(16,185,129,.12)', color: '#059669', fontWeight: 600, fontSize: 12, flexShrink: 0 }}>✓ Réglementé</span>
|
|
: isAdmin && <span style={{ padding: '3px 10px', borderRadius: 20, background: 'var(--surface-2)', color: 'var(--text-muted)', fontSize: 12, flexShrink: 0 }}>Non réglementé</span>
|
|
}
|
|
{data.domiciliation && (
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '3px 10px', borderRadius: 20, background: 'var(--surface-2)', color: 'var(--text-muted)', fontSize: 12, flexShrink: 0 }}>
|
|
<FlagIcon code={data.domiciliation} size={16} />
|
|
{countryLabel(data.domiciliation)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{editing
|
|
? <textarea value={form.description ?? ''} onChange={setInput('description')}
|
|
placeholder="Description courte de la plateforme…"
|
|
style={{ width: '100%', minHeight: 60, resize: 'vertical', fontSize: 13, marginTop: 4 }} />
|
|
: data.description && (
|
|
<p style={{ margin: 0, color: 'var(--text-muted)', lineHeight: 1.6, fontSize: 14 }}>{data.description}</p>
|
|
)
|
|
}
|
|
</div>
|
|
|
|
{/* Actions admin */}
|
|
{isAdmin && (
|
|
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
|
|
{editing ? (
|
|
<>
|
|
<button className="primary" onClick={save} disabled={saving} style={{ fontSize: 13 }}>
|
|
{saving ? 'Enregistrement…' : 'Enregistrer'}
|
|
</button>
|
|
<button className="ghost" onClick={cancelEdit} style={{ fontSize: 13 }}>Annuler</button>
|
|
</>
|
|
) : (
|
|
<button className="ghost" onClick={startEdit} style={{ fontSize: 13 }}>
|
|
✎ Modifier
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{saveErr && <div className="error" style={{ marginTop: 12 }}>{saveErr}</div>}
|
|
</div>
|
|
|
|
{/* ── Corps : 2 colonnes ─────────────────────────────────────────────── */}
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 320px', gap: 20, alignItems: 'start' }}>
|
|
|
|
{/* ── Colonne gauche ──────────────────────────────────────────────── */}
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
|
|
|
{/* KPIs */}
|
|
<div className="card" style={{ padding: '20px 24px' }}>
|
|
<SectionTitle>Aperçu</SectionTitle>
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(130px, 1fr))', gap: 12 }}>
|
|
<KpiCell label="Invest. minimum"
|
|
value={data.investissement_minimum != null ? `${data.investissement_minimum.toLocaleString('fr-FR')} €` : null}
|
|
editing={editing}
|
|
input={<input type="number" value={form?.investissement_minimum ?? ''} onChange={setInput('investissement_minimum')} placeholder="€" style={{ width: '100%' }} />}
|
|
/>
|
|
<KpiCell label="Rendement annoncé"
|
|
value={data.rendement_annonce != null ? `${data.rendement_annonce} %` : null}
|
|
editing={editing}
|
|
input={<input type="number" step="0.01" value={form?.rendement_annonce ?? ''} onChange={setInput('rendement_annonce')} placeholder="%" style={{ width: '100%' }} />}
|
|
/>
|
|
<KpiCell label="Investisseurs"
|
|
value={data.nb_investisseurs != null ? Number(data.nb_investisseurs).toLocaleString('fr-FR') : null}
|
|
editing={editing}
|
|
input={<input type="number" value={form?.nb_investisseurs ?? ''} onChange={setInput('nb_investisseurs')} style={{ width: '100%' }} />}
|
|
/>
|
|
<KpiCell label="Volume financé"
|
|
value={data.volume_total_finance != null
|
|
? data.volume_total_finance >= 1e6
|
|
? `${(data.volume_total_finance / 1e6).toFixed(1)} M€`
|
|
: `${(data.volume_total_finance / 1e3).toFixed(0)} k€`
|
|
: null}
|
|
editing={editing}
|
|
input={<input type="number" value={form?.volume_total_finance ?? ''} onChange={setInput('volume_total_finance')} placeholder="€" style={{ width: '100%' }} />}
|
|
/>
|
|
<KpiCell label="Durée moy."
|
|
value={data.duree_moyenne_pret != null ? `${data.duree_moyenne_pret} mois` : null}
|
|
editing={editing}
|
|
input={<input type="number" step="0.1" value={form?.duree_moyenne_pret ?? ''} onChange={setInput('duree_moyenne_pret')} placeholder="mois" style={{ width: '100%' }} />}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Features */}
|
|
<div className="card" style={{ padding: '20px 24px' }}>
|
|
<SectionTitle>Fonctionnalités</SectionTitle>
|
|
<div>
|
|
<FeatureRow label="Garantie de rachat" field="garantie_rachat" data={d} editing={editing} onChange={setField} />
|
|
<FeatureRow label="Statistiques publiques" field="statistiques_publiques" data={d} editing={editing} onChange={setField} />
|
|
<FeatureRow label="Bonus d'inscription" field="bonus_inscription" data={d} editing={editing} onChange={setField} />
|
|
<FeatureRow label="Marché secondaire" field="marche_secondaire" data={d} editing={editing} onChange={setField} />
|
|
<FeatureRow label="Investissement automatique" field="investissement_auto" data={d} editing={editing} onChange={setField} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Catégories */}
|
|
{(data.categories?.length > 0) && (
|
|
<div className="card" style={{ padding: '20px 24px' }}>
|
|
<SectionTitle>Catégories</SectionTitle>
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
|
{data.categories.map(c => <span key={c} className="cat-badge">{c}</span>)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
|
|
{/* ── Colonne droite ───────────────────────────────────────────────── */}
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
|
|
|
{/* Identité */}
|
|
<div className="card" style={{ padding: '20px 24px' }}>
|
|
<SectionTitle>Identité</SectionTitle>
|
|
<InfoRow label="Année de création"
|
|
value={data.annee_creation}
|
|
editing={editing}
|
|
input={<input type="number" value={form?.annee_creation ?? ''} onChange={setInput('annee_creation')} placeholder="ex: 2017" style={{ width: '100%' }} />}
|
|
/>
|
|
{/* Catégories d'investissement (multi-select global) */}
|
|
<div style={{ padding: '7px 0', borderBottom: '1px solid var(--border)' }}>
|
|
<span style={{ fontSize: 13, color: 'var(--text-muted)', display: 'block', marginBottom: 6 }}>
|
|
Catégories d'investissement
|
|
</span>
|
|
{editing ? (
|
|
<InvSelect
|
|
items={catsInv}
|
|
selected={form?.categories_inv_ids || []}
|
|
onChange={ids => setField('categories_inv_ids', ids)}
|
|
addApiPath="/categories-inv"
|
|
onItemAdded={cat => setCatsInv(prev => [...prev, cat].sort((a,b) => a.nom.localeCompare(b.nom)))}
|
|
emptyLabel="Aucune catégorie d'investissement"
|
|
addLabel="Ajouter une catégorie d'investissement"
|
|
inputPlaceholder="Nom de la catégorie…"
|
|
inheritedIds={(data.categories_inv || []).filter(c => c.is_inherited).map(c => c.id)}
|
|
/>
|
|
) : (
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
|
{(data.categories_inv || []).length === 0
|
|
? <span style={{ fontSize: 13, color: 'var(--text-muted)' }}>—</span>
|
|
: (data.categories_inv || []).map(c => (
|
|
<span key={c.id} className="chip-cat" title={c.is_inherited ? 'Hérité du référentiel' : undefined}>
|
|
{c.nom}
|
|
{c.is_inherited && <span style={{ fontSize: 9, fontWeight: 600, marginLeft: 4, padding: '1px 4px', borderRadius: 3, background: 'var(--accent)', color: '#fff', opacity: .8, verticalAlign: 'middle' }}>Réf</span>}
|
|
</span>
|
|
))
|
|
}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{/* Secteurs d'investissement (multi-select global) */}
|
|
<div style={{ padding: '7px 0', borderBottom: '1px solid var(--border)' }}>
|
|
<span style={{ fontSize: 13, color: 'var(--text-muted)', display: 'block', marginBottom: 6 }}>
|
|
Secteurs d'investissement
|
|
</span>
|
|
{editing ? (
|
|
<InvSelect
|
|
items={secteursInv}
|
|
selected={form?.secteurs_inv_ids || []}
|
|
onChange={ids => setField('secteurs_inv_ids', ids)}
|
|
addApiPath="/secteurs-inv"
|
|
onItemAdded={sect => setSecteursInv(prev => [...prev, sect].sort((a,b) => a.nom.localeCompare(b.nom)))}
|
|
emptyLabel="Aucun secteur d'investissement"
|
|
addLabel="Ajouter un secteur d'investissement"
|
|
inputPlaceholder="Nom du secteur…"
|
|
inheritedIds={(data.secteurs_inv || []).filter(s => s.is_inherited).map(s => s.id)}
|
|
/>
|
|
) : (
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
|
{(data.secteurs_inv || []).length === 0
|
|
? <span style={{ fontSize: 13, color: 'var(--text-muted)' }}>—</span>
|
|
: (data.secteurs_inv || []).map(s => (
|
|
<span key={s.id} className="chip-sect" title={s.is_inherited ? 'Hérité du référentiel' : undefined}>
|
|
{s.nom}
|
|
{s.is_inherited && <span style={{ fontSize: 9, fontWeight: 600, marginLeft: 4, padding: '1px 4px', borderRadius: 3, background: 'var(--accent)', color: '#fff', opacity: .8, verticalAlign: 'middle' }}>Réf</span>}
|
|
</span>
|
|
))
|
|
}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<InfoRow label="Investisseurs acceptés"
|
|
value={INV_TYPES[data.investisseurs_types] || data.investisseurs_types}
|
|
editing={editing}
|
|
input={
|
|
<select value={form?.investisseurs_types ?? ''} onChange={setInput('investisseurs_types')} style={{ width: '100%' }}>
|
|
<option value="">—</option>
|
|
<option value="particulier">Particulier</option>
|
|
<option value="entreprise">Entreprise</option>
|
|
<option value="les_deux">Particulier & Entreprise</option>
|
|
</select>
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
{/* Régulation */}
|
|
<div className="card" style={{ padding: '20px 24px' }}>
|
|
<SectionTitle>Régulation</SectionTitle>
|
|
{editing ? (
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, cursor: 'pointer', padding: '7px 0', borderBottom: '1px solid var(--border)', marginBottom: 2 }}>
|
|
<input type="checkbox" checked={!!form?.is_regule} onChange={setInput('is_regule')} style={{ width: 'auto' }} />
|
|
Plateforme réglementée
|
|
</label>
|
|
) : (
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '7px 0', borderBottom: '1px solid var(--border)' }}>
|
|
<span style={{ fontSize: 13, color: 'var(--text-muted)' }}>Statut</span>
|
|
<span style={{ fontSize: 13, fontWeight: 600, color: data.is_regule ? '#059669' : 'var(--text-muted)' }}>
|
|
{data.is_regule ? '✓ Réglementé' : '—'}
|
|
</span>
|
|
</div>
|
|
)}
|
|
<InfoRow label="Régulateur"
|
|
value={data.regulateur}
|
|
editing={editing}
|
|
input={<input type="text" value={form?.regulateur ?? ''} onChange={setInput('regulateur')} placeholder="ex: AMF, ACPR…" style={{ width: '100%' }} />}
|
|
/>
|
|
<InfoRow label="N° de licence"
|
|
value={data.numero_licence}
|
|
editing={editing}
|
|
input={<input type="text" value={form?.numero_licence ?? ''} onChange={setInput('numero_licence')} style={{ width: '100%' }} />}
|
|
/>
|
|
</div>
|
|
|
|
{/* Géographie */}
|
|
<div className="card" style={{ padding: '20px 24px' }}>
|
|
<SectionTitle>Géographie</SectionTitle>
|
|
<InfoRow label="Inscription"
|
|
value={data.pays_inscription}
|
|
editing={editing}
|
|
input={<input type="text" value={form?.pays_inscription ?? ''} onChange={setInput('pays_inscription')} style={{ width: '100%' }} />}
|
|
/>
|
|
<InfoRow label="Domiciliation"
|
|
value={data.domiciliation ? <span style={{ display:"inline-flex", alignItems:"center", gap:5 }}><FlagIcon code={data.domiciliation} size={16} />{countryLabel(data.domiciliation)}</span> : '—'}
|
|
editing={editing}
|
|
input={
|
|
<CountrySelect
|
|
value={form?.domiciliation ?? ''}
|
|
onChange={code => setField('domiciliation', code)}
|
|
showCode
|
|
/>
|
|
}
|
|
/>
|
|
<div style={{ padding: '7px 0' }}>
|
|
<span style={{ fontSize: 13, color: 'var(--text-muted)', display: 'block', marginBottom: 4 }}>Pays d'opération</span>
|
|
{editing
|
|
? <input type="text" value={form?.pays_operation_str ?? ''} onChange={setInput('pays_operation_str')}
|
|
placeholder="France, Allemagne, Espagne…"
|
|
style={{ width: '100%', fontSize: 13 }} />
|
|
: paysOp.length > 0
|
|
? <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 4 }}>
|
|
{paysOp.map(p => (
|
|
<span key={p} style={{ padding: '2px 8px', borderRadius: 10, background: 'var(--surface-2)', fontSize: 12, color: 'var(--text-muted)' }}>{p}</span>
|
|
))}
|
|
</div>
|
|
: <span style={{ color: 'var(--text-muted)', fontSize: 13 }}>—</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Liens */}
|
|
<div className="card" style={{ padding: '20px 24px' }}>
|
|
<SectionTitle>Liens</SectionTitle>
|
|
{editing ? (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
<div>
|
|
<label style={{ fontSize: 12, color: 'var(--text-muted)', display: 'block', marginBottom: 3 }}>Site web</label>
|
|
<input type="url" value={form?.url ?? ''} onChange={setInput('url')} style={{ width: '100%' }} placeholder="https://…" />
|
|
</div>
|
|
<div>
|
|
<label style={{ fontSize: 12, color: 'var(--text-muted)', display: 'block', marginBottom: 3 }}>Trustpilot</label>
|
|
<input type="url" value={form?.url_trustpilot ?? ''} onChange={setInput('url_trustpilot')} style={{ width: '100%' }} placeholder="https://…" />
|
|
</div>
|
|
<div>
|
|
<label style={{ fontSize: 12, color: 'var(--text-muted)', display: 'block', marginBottom: 3 }}>LinkedIn</label>
|
|
<input type="url" value={form?.url_linkedin ?? ''} onChange={setInput('url_linkedin')} style={{ width: '100%' }} placeholder="https://…" />
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
{/* Bouton site web */}
|
|
{data.url && (
|
|
<a href={data.url} target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }}>
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
|
|
background: '#10b981', borderRadius: 10, padding: '12px 16px',
|
|
color: 'white', fontWeight: 600, fontSize: 14, cursor: 'pointer',
|
|
}}>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/>
|
|
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
|
</svg>
|
|
Visiter le site web
|
|
</div>
|
|
</a>
|
|
)}
|
|
|
|
{/* Trustpilot */}
|
|
{data.url_trustpilot && (
|
|
<a href={data.url_trustpilot} target="_blank" rel="noopener noreferrer"
|
|
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
textDecoration: 'none', padding: '10px 14px', borderRadius: 8,
|
|
border: '1px solid var(--border)', background: 'var(--surface-2)' }}>
|
|
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)' }}>Avis</span>
|
|
<svg viewBox="0 0 85 21" fill="none" xmlns="http://www.w3.org/2000/svg" height="18">
|
|
<g clipPath="url(#tp_clip)">
|
|
<path d="M22.3053 7.44287H30.8992V9.05826H27.5201V18.1391H25.662V9.05826H22.2979V7.44287H22.3053ZM30.5321 10.3943H32.1205V11.889H32.1505C32.2029 11.6776 32.3003 11.4738 32.4427 11.2775C32.5851 11.0813 32.7574 10.8925 32.9597 10.734C33.162 10.568 33.3867 10.4396 33.634 10.334C33.8813 10.2358 34.136 10.183 34.3907 10.183C34.5855 10.183 34.7279 10.1905 34.8028 10.1981C34.8778 10.2056 34.9527 10.2207 35.0351 10.2283V11.8739C34.9152 11.8512 34.7953 11.8361 34.668 11.821C34.5406 11.8059 34.4207 11.7984 34.3008 11.7984C34.0161 11.7984 33.7464 11.8588 33.4916 11.972C33.2369 12.0852 33.0196 12.2588 32.8323 12.4777C32.645 12.7042 32.4951 12.9759 32.3828 13.3081C32.2704 13.6402 32.2179 14.0176 32.2179 14.4479V18.1316H30.5246V10.3943H30.5321ZM42.8198 18.1391H41.1565V17.0597H41.1265C40.9167 17.4522 40.6095 17.7617 40.1974 17.9957C39.7853 18.2297 39.3658 18.3505 38.9387 18.3505C37.9272 18.3505 37.1929 18.1014 36.7434 17.5956C36.2938 17.0899 36.0691 16.3275 36.0691 15.3084V10.3943H37.7624V15.1424C37.7624 15.8217 37.8897 16.3048 38.152 16.5841C38.4067 16.8634 38.7739 17.0069 39.2384 17.0069C39.598 17.0069 39.8902 16.954 40.13 16.8408C40.3698 16.7276 40.5646 16.5841 40.7069 16.3954C40.8568 16.2143 40.9617 15.9878 41.0291 15.7312C41.0965 15.4745 41.1265 15.1952 41.1265 14.8933V10.4019H42.8198V18.1391ZM45.7044 15.6557C45.7569 16.1539 45.9442 16.5011 46.2664 16.7049C46.596 16.9012 46.9856 17.0069 47.4427 17.0069C47.6 17.0069 47.7798 16.9918 47.9821 16.9691C48.1844 16.9465 48.3792 16.8936 48.5516 16.8257C48.7314 16.7577 48.8737 16.6521 48.9936 16.5162C49.106 16.3803 49.1585 16.2067 49.151 15.9878C49.1435 15.7689 49.0611 15.5877 48.9112 15.4519C48.7614 15.3084 48.574 15.2028 48.3418 15.1122C48.1095 15.0291 47.8473 14.9537 47.5476 14.8933C47.2479 14.8329 46.9482 14.7649 46.641 14.697C46.3263 14.6291 46.0191 14.5385 45.7269 14.4404C45.4347 14.3422 45.1724 14.2063 44.9402 14.0327C44.7079 13.8667 44.5206 13.6478 44.3857 13.3836C44.2434 13.1194 44.1759 12.7948 44.1759 12.4023C44.1759 11.9795 44.2808 11.6323 44.4831 11.3455C44.6854 11.0586 44.9477 10.8322 45.2549 10.6585C45.5696 10.4849 45.9142 10.3642 46.2963 10.2887C46.6784 10.2207 47.0456 10.183 47.3902 10.183C47.7873 10.183 48.1695 10.2283 48.5291 10.3113C48.8887 10.3943 49.2184 10.5302 49.5106 10.7265C49.8028 10.9152 50.0426 11.1643 50.2374 11.4662C50.4322 11.7682 50.5521 12.1381 50.6045 12.5683H48.8363C48.7539 12.1607 48.574 11.8814 48.2818 11.7455C47.9896 11.6021 47.6525 11.5342 47.2778 11.5342C47.158 11.5342 47.0156 11.5417 46.8508 11.5644C46.6859 11.587 46.5361 11.6248 46.3862 11.6776C46.2439 11.7304 46.124 11.8135 46.0191 11.9191C45.9217 12.0248 45.8693 12.1607 45.8693 12.3343C45.8693 12.5457 45.9442 12.7117 46.0865 12.8401C46.2289 12.9684 46.4162 13.0741 46.6485 13.1647C46.8807 13.2477 47.143 13.3232 47.4427 13.3836C47.7424 13.4439 48.0496 13.5119 48.3643 13.5798C48.6714 13.6478 48.9711 13.7383 49.2708 13.8365C49.5705 13.9346 49.8328 14.0705 50.0651 14.2441C50.2973 14.4177 50.4846 14.6291 50.627 14.8857C50.7693 15.1424 50.8443 15.467 50.8443 15.8444C50.8443 16.3048 50.7394 16.6898 50.5296 17.0144C50.3198 17.3314 50.0501 17.5956 49.7204 17.7919C49.3907 17.9882 49.0161 18.1391 48.6115 18.2297C48.2069 18.3203 47.8023 18.3656 47.4052 18.3656C46.9182 18.3656 46.4687 18.3127 46.0566 18.1995C45.6445 18.0863 45.2848 17.9202 44.9851 17.7013C44.6854 17.4749 44.4457 17.1956 44.2734 16.8634C44.101 16.5313 44.0111 16.1312 43.9961 15.6708H45.7044V15.6557ZM51.2938 10.3943H52.575V8.0694H54.2683V10.3943H55.7968V11.67H54.2683V15.8066C54.2683 15.9878 54.2758 16.1388 54.2908 16.2746C54.3058 16.403 54.3433 16.5162 54.3957 16.6068C54.4482 16.6974 54.5306 16.7653 54.643 16.8106C54.7554 16.8559 54.8977 16.8785 55.0925 16.8785C55.2124 16.8785 55.3323 16.8785 55.4522 16.871C55.572 16.8634 55.6919 16.8483 55.8118 16.8181V18.1391C55.6245 18.1618 55.4372 18.1769 55.2648 18.1995C55.085 18.2222 54.9052 18.2297 54.7179 18.2297C54.2683 18.2297 53.9087 18.1844 53.639 18.1014C53.3692 18.0184 53.152 17.89 53.0021 17.724C52.8448 17.5579 52.7474 17.3541 52.6874 17.105C52.635 16.8559 52.5975 16.569 52.59 16.252V11.6851H51.3088V10.3943H51.2938ZM56.9956 10.3943H58.599V11.4436H58.629C58.8687 10.9907 59.1984 10.6736 59.6255 10.4774C60.0526 10.2811 60.5096 10.183 61.0116 10.183C61.6185 10.183 62.143 10.2887 62.5925 10.5076C63.0421 10.7189 63.4167 11.0133 63.7164 11.3907C64.0161 11.7682 64.2334 12.206 64.3832 12.7042C64.5331 13.2024 64.608 13.7383 64.608 14.3045C64.608 14.8253 64.5406 15.3311 64.4057 15.8142C64.2708 16.3048 64.0685 16.7351 63.7988 17.1125C63.5291 17.49 63.1844 17.7843 62.7648 18.0108C62.3453 18.2373 61.8582 18.3505 61.2888 18.3505C61.0416 18.3505 60.7943 18.3278 60.5471 18.2826C60.2998 18.2373 60.06 18.1618 59.8353 18.0636C59.6105 17.9655 59.3932 17.8372 59.2059 17.6787C59.0111 17.5202 58.8538 17.339 58.7189 17.1352H58.6889V21H56.9956V10.3943ZM62.9147 14.2743C62.9147 13.9271 62.8697 13.5874 62.7798 13.2552C62.6899 12.9231 62.555 12.6363 62.3752 12.3796C62.1954 12.123 61.9706 11.9191 61.7084 11.7682C61.4387 11.6172 61.1315 11.5342 60.7868 11.5342C60.075 11.5342 59.5356 11.7833 59.1759 12.2815C58.8163 12.7797 58.6365 13.4439 58.6365 14.2743C58.6365 14.6668 58.6814 15.0291 58.7788 15.3613C58.8762 15.6934 59.0111 15.9803 59.2059 16.2218C59.3932 16.4634 59.618 16.6521 59.8802 16.7879C60.1425 16.9314 60.4497 16.9993 60.7943 16.9993C61.1839 16.9993 61.5061 16.9163 61.7758 16.7578C62.0456 16.5992 62.2628 16.3879 62.4352 16.1388C62.6075 15.8821 62.7349 15.5953 62.8098 15.2707C62.8772 14.9461 62.9147 14.614 62.9147 14.2743ZM65.9042 7.44287H67.5975V9.05826H65.9042V7.44287ZM65.9042 10.3943H67.5975V18.1391H65.9042V10.3943ZM69.111 7.44287H70.8043V18.1391H69.111V7.44287ZM75.9966 18.3505C75.3822 18.3505 74.8353 18.2448 74.3557 18.041C73.8762 17.8372 73.4716 17.5503 73.1345 17.1956C72.8048 16.8332 72.55 16.403 72.3777 15.9048C72.2054 15.4066 72.1155 14.8555 72.1155 14.2592C72.1155 13.6704 72.2054 13.1269 72.3777 12.6287C72.55 12.1305 72.8048 11.7002 73.1345 11.3379C73.4641 10.9756 73.8762 10.6963 74.3557 10.4925C74.8353 10.2887 75.3822 10.183 75.9966 10.183C76.611 10.183 77.1579 10.2887 77.6374 10.4925C78.117 10.6963 78.5216 10.9831 78.8587 11.3379C79.1884 11.7002 79.4431 12.1305 79.6155 12.6287C79.7878 13.1269 79.8777 13.6704 79.8777 14.2592C79.8777 14.8555 79.7878 15.4066 79.6155 15.9048C79.4431 16.403 79.1884 16.8332 78.8587 17.1956C78.5291 17.5579 78.117 17.8372 77.6374 18.041C77.1579 18.2448 76.611 18.3505 75.9966 18.3505ZM75.9966 16.9993C76.3712 16.9993 76.7009 16.9163 76.9781 16.7578C77.2553 16.5992 77.4801 16.3879 77.6599 16.1312C77.8397 15.8746 77.9671 15.5802 78.057 15.2556C78.1395 14.931 78.1844 14.5989 78.1844 14.2592C78.1844 13.9271 78.1395 13.6025 78.057 13.2703C77.9746 12.9382 77.8397 12.6514 77.6599 12.3947C77.4801 12.1381 77.2553 11.9342 76.9781 11.7757C76.7009 11.6172 76.3712 11.5342 75.9966 11.5342C75.622 11.5342 75.2923 11.6172 75.0151 11.7757C74.7379 11.9342 74.5131 12.1456 74.3333 12.3947C74.1534 12.6514 74.0261 12.9382 73.9361 13.2703C73.8537 13.6025 73.8088 13.9271 73.8088 14.2592C73.8088 14.5989 73.8537 14.931 73.9361 15.2556C74.0186 15.5802 74.1534 15.8746 74.3333 16.1312C74.5131 16.3879 74.7379 16.5992 75.0151 16.7578C75.2923 16.9238 75.622 16.9993 75.9966 16.9993ZM80.3722 10.3943H81.6534V8.0694H83.3467V10.3943H84.8752V11.67H83.3467V15.8066C83.3467 15.9878 83.3542 16.1388 83.3692 16.2746C83.3842 16.403 83.4217 16.5162 83.4741 16.6068C83.5266 16.6974 83.609 16.7653 83.7214 16.8106C83.8338 16.8559 83.9761 16.8785 84.1709 16.8785C84.2908 16.8785 84.4107 16.8785 84.5305 16.871C84.6504 16.8634 84.7703 16.8483 84.8902 16.8181V18.1391C84.7029 18.1618 84.5156 18.1769 84.3432 18.1995C84.1634 18.2222 83.9836 18.2297 83.7963 18.2297C83.3467 18.2297 82.9871 18.1844 82.7174 18.1014C82.4476 18.0184 82.2304 17.89 82.0805 17.724C81.9232 17.5579 81.8258 17.3541 81.7658 17.105C81.7134 16.8559 81.6759 16.569 81.6684 16.252V11.6851H80.3872V10.3943H80.3722Z" fill="#191919"/>
|
|
<path d="M20.3271 7.44285H12.5649L10.1673 0L7.76223 7.44285L0 7.4353L6.2862 12.0399L3.88111 19.4752L10.1673 14.8781L16.446 19.4752L14.0484 12.0399L20.3271 7.44285Z" fill="#00B67A"/>
|
|
<path d="M14.5881 13.7234L14.0486 12.04L10.1675 14.8783L14.5881 13.7234Z" fill="#005128"/>
|
|
</g>
|
|
<defs><clipPath id="tp_clip"><rect width="84.875" height="21" fill="white"/></clipPath></defs>
|
|
</svg>
|
|
</a>
|
|
)}
|
|
|
|
{/* Icônes réseaux sociaux */}
|
|
{data.url_linkedin && (
|
|
<div style={{ display: 'flex', gap: 10, justifyContent: 'center' }}>
|
|
<a href={data.url_linkedin} target="_blank" rel="noopener noreferrer"
|
|
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
width: 44, height: 44, borderRadius: 10, border: '1px solid var(--border)',
|
|
background: 'var(--surface-2)', color: '#0a66c2', textDecoration: 'none' }}>
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"/>
|
|
<rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/>
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
)}
|
|
|
|
{!data.url && !data.url_trustpilot && !data.url_linkedin && (
|
|
<span style={{ color: 'var(--text-muted)', fontSize: 13 }}>Aucun lien renseigné</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── Outils admin IA ─────────────────────────────────────────────────── */}
|
|
{isAdmin && (
|
|
<div style={{ marginTop: 32, borderTop: '1px solid var(--border)', paddingTop: 24, padding: '24px 24px 0' }}>
|
|
<div style={{ fontSize: 12, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '.07em', color: 'var(--text-muted)', marginBottom: 12 }}>
|
|
Outils administrateur
|
|
</div>
|
|
<ProfilImportBlock data={data} id={id} onApplied={load} catsInv={catsInv} secteursInv={secteursInv} />
|
|
<ProfilPromptBlock data={data} catsInv={catsInv} secteursInv={secteursInv} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|
|
// ── Champs profil lisibles ────────────────────────────────────────────────────
|
|
const PROFILE_FIELD_LABELS = {
|
|
description: 'Description',
|
|
annee_creation: 'Année de création',
|
|
categories_inv: "Catégories d'investissement",
|
|
secteurs_inv: 'Secteurs d\'investissement',
|
|
investisseurs_types: 'Investisseurs acceptés',
|
|
is_regule: 'Réglementé',
|
|
regulateur: 'Régulateur',
|
|
numero_licence: 'N° de licence',
|
|
pays_inscription: 'Pays inscription',
|
|
domiciliation: 'Domiciliation',
|
|
pays_operation: "Pays d'opération",
|
|
investissement_minimum: 'Invest. minimum (€)',
|
|
rendement_annonce: 'Rendement annoncé (%)',
|
|
nb_investisseurs: 'Nb investisseurs',
|
|
volume_total_finance: 'Volume total financé (€)',
|
|
duree_moyenne_pret: 'Durée moy. prêt (mois)',
|
|
garantie_rachat: 'Garantie de rachat',
|
|
statistiques_publiques: 'Stats publiques',
|
|
bonus_inscription: "Bonus d'inscription",
|
|
marche_secondaire: 'Marché secondaire',
|
|
investissement_auto: 'Invest. automatique',
|
|
url: 'Site web',
|
|
url_trustpilot: 'Trustpilot',
|
|
url_linkedin: 'LinkedIn',
|
|
};
|
|
|
|
// Champs du profil enrichi (pas les champs fiscaux/admin)
|
|
const PROFILE_FIELDS = Object.keys(PROFILE_FIELD_LABELS);
|
|
|
|
function fmtProfileVal(val) {
|
|
if (val === null || val === undefined) return '—';
|
|
if (typeof val === 'boolean') return val ? 'Oui' : 'Non';
|
|
if (typeof val === 'number' && (val === 0 || val === 1) && typeof val === 'number') return val ? 'Oui' : 'Non';
|
|
if (Array.isArray(val)) return val.join(', ') || '—';
|
|
return String(val);
|
|
}
|
|
|
|
// ── Bloc import JSON ──────────────────────────────────────────────────────────
|
|
function ProfilImportBlock({ data, id, onApplied, catsInv = [], secteursInv = [] }) {
|
|
const [open, setOpen] = useState(false);
|
|
const [json, setJson] = useState('');
|
|
const [parsed, setParsed] = useState(null);
|
|
const [parseErr, setParseErr] = useState(null);
|
|
const [selected, setSelected] = useState({});
|
|
const [applying, setApplying] = useState(false);
|
|
const [result, setResult] = useState(null);
|
|
const [toCreate, setToCreate] = useState({}); // { nom: bool } pour items inconnus
|
|
const [step, setStep] = useState('json'); // 'json' | 'creation' | 'diff'
|
|
|
|
// Helper : résout les noms IA → IDs + détecte les inconnus
|
|
const resolveTags = (aiNames, knownList, currentItems) => {
|
|
const norm = s => s.toLowerCase().trim().normalize('NFD').replace(/[̀-ͯ]/g, '');
|
|
const lev = (a, b) => {
|
|
const m = a.length, n = b.length;
|
|
const dp = Array.from({length: m+1}, (_, i) => Array.from({length: n+1}, (_, j) => j===0?i:0));
|
|
for (let j=1;j<=n;j++) dp[0][j]=j;
|
|
for (let i=1;i<=m;i++) for (let j=1;j<=n;j++)
|
|
dp[i][j] = a[i-1]===b[j-1] ? dp[i-1][j-1] : 1+Math.min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1]);
|
|
return dp[m][n];
|
|
};
|
|
const isSim = (a, b) => { const na=norm(a),nb=norm(b); return na===nb||na.includes(nb)||nb.includes(na)||lev(na,nb)<=2; };
|
|
|
|
const resolvedIds = [], resolvedNames = [], unknownNames = [];
|
|
for (const aiName of (aiNames || [])) {
|
|
const match = knownList.find(k => isSim(aiName, k.nom));
|
|
if (match) { resolvedIds.push(match.id); resolvedNames.push(match.nom); }
|
|
else unknownNames.push(aiName);
|
|
}
|
|
const currentNames = (currentItems || []).map(c => c.nom);
|
|
const currentIds = (currentItems || []).map(c => c.id).sort().join(',');
|
|
const newIds = [...resolvedIds].sort().join(',');
|
|
const hasChange = newIds !== currentIds || unknownNames.length > 0;
|
|
return { resolvedIds, resolvedNames, unknownNames, currentNames, hasChange };
|
|
};
|
|
|
|
const parse = () => {
|
|
setParseErr(null); setParsed(null); setSelected({}); setResult(null); setToCreate({}); setStep('json');
|
|
let obj;
|
|
try {
|
|
const cleaned = json.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '').trim();
|
|
obj = JSON.parse(cleaned);
|
|
} catch (e) { setParseErr('JSON invalide : ' + e.message); return; }
|
|
if (typeof obj !== 'object' || Array.isArray(obj)) {
|
|
setParseErr('Le JSON doit être un objet (pas un tableau).'); return;
|
|
}
|
|
|
|
// Champs normaux (hors cats/secteurs qui ont un traitement spécial)
|
|
const SKIP = new Set(['categories_inv', 'secteurs_inv']);
|
|
const changes = [];
|
|
for (const field of PROFILE_FIELDS) {
|
|
if (SKIP.has(field) || !(field in obj)) continue;
|
|
const incoming = obj[field];
|
|
const current = data[field];
|
|
const normVal = v => {
|
|
if (Array.isArray(v)) return v.join(',');
|
|
if (v === true || v === 1) return '1';
|
|
if (v === false || v === 0) return '0';
|
|
return String(v ?? '');
|
|
};
|
|
if (normVal(incoming) !== normVal(current)) {
|
|
changes.push({ field, before: current, after: incoming });
|
|
}
|
|
}
|
|
|
|
// Résolution catégories / secteurs
|
|
const catAiNames = Array.isArray(obj.categories_inv) ? obj.categories_inv : [];
|
|
const sectAiNames = Array.isArray(obj.secteurs_inv) ? obj.secteurs_inv : [];
|
|
const catRes = resolveTags(catAiNames, catsInv, data.categories_inv || []);
|
|
const sectRes = resolveTags(sectAiNames, secteursInv, data.secteurs_inv || []);
|
|
|
|
if (catRes.hasChange && catAiNames.length > 0) {
|
|
changes.push({
|
|
field: 'categories_inv',
|
|
before: catRes.currentNames,
|
|
after: [...catRes.resolvedNames, ...catRes.unknownNames.map(n => n + ' ✦')],
|
|
_resolution: catRes,
|
|
});
|
|
}
|
|
if (sectRes.hasChange && sectAiNames.length > 0) {
|
|
changes.push({
|
|
field: 'secteurs_inv',
|
|
before: sectRes.currentNames,
|
|
after: [...sectRes.resolvedNames, ...sectRes.unknownNames.map(n => n + ' ✦')],
|
|
_resolution: sectRes,
|
|
});
|
|
}
|
|
|
|
if (changes.length === 0) {
|
|
setParseErr('Aucune différence détectée avec les données actuelles.');
|
|
return;
|
|
}
|
|
|
|
const allUnknowns = [...catRes.unknownNames, ...sectRes.unknownNames];
|
|
const initCreate = {};
|
|
allUnknowns.forEach(n => { initCreate[n] = true; }); // tous cochés par défaut
|
|
setToCreate(initCreate);
|
|
|
|
setParsed({ obj, changes, catRes, sectRes });
|
|
const sel = {};
|
|
changes.forEach(c => { sel[c.field] = true; });
|
|
setSelected(sel);
|
|
|
|
// Si inconnues → étape création, sinon directement diff
|
|
setStep(allUnknowns.length > 0 ? 'creation' : 'diff');
|
|
};
|
|
|
|
const apply = async () => {
|
|
setApplying(true); setResult(null);
|
|
const obj = parsed.obj;
|
|
const selectedChanges = parsed.changes.filter(c => selected[c.field]);
|
|
if (selectedChanges.length === 0) { setApplying(false); return; }
|
|
|
|
// Construire le payload complet (base = data actuel, on écrase les champs sélectionnés)
|
|
const payload = {
|
|
nom: data.nom,
|
|
url: data.url || null,
|
|
domiciliation: data.domiciliation,
|
|
fiscalite: data.fiscalite,
|
|
taux_fiscalite_locale: data.taux_fiscalite_locale ?? null,
|
|
type_produit_fiscal: data.type_produit_fiscal || '2TT',
|
|
logo_filename: data.logo_filename ?? null,
|
|
description: data.description || null,
|
|
categories: data.categories || [],
|
|
notation: data.notation || [],
|
|
annee_creation: data.annee_creation ?? null,
|
|
investisseurs_types: data.investisseurs_types ?? null,
|
|
regulateur: data.regulateur ?? null,
|
|
numero_licence: data.numero_licence ?? null,
|
|
is_regule: data.is_regule ? 1 : 0,
|
|
pays_inscription: data.pays_inscription ?? null,
|
|
pays_siege: data.pays_siege ?? null,
|
|
pays_operation: Array.isArray(data.pays_operation) ? data.pays_operation : [],
|
|
investissement_minimum: data.investissement_minimum ?? null,
|
|
rendement_annonce: data.rendement_annonce ?? null,
|
|
nb_investisseurs: data.nb_investisseurs ?? null,
|
|
volume_total_finance: data.volume_total_finance ?? null,
|
|
duree_moyenne_pret: data.duree_moyenne_pret ?? null,
|
|
garantie_rachat: data.garantie_rachat ? 1 : 0,
|
|
statistiques_publiques: data.statistiques_publiques ? 1 : 0,
|
|
bonus_inscription: data.bonus_inscription ? 1 : 0,
|
|
marche_secondaire: data.marche_secondaire ? 1 : 0,
|
|
investissement_auto: data.investissement_auto ? 1 : 0,
|
|
url_trustpilot: data.url_trustpilot ?? null,
|
|
url_linkedin: data.url_linkedin ?? null,
|
|
};
|
|
|
|
// Écraser avec les champs sélectionnés
|
|
for (const c of selectedChanges) {
|
|
const v = c.after;
|
|
if (c.field === 'pays_operation') {
|
|
payload.pays_operation = Array.isArray(v) ? v : [];
|
|
} else if (c.field === 'is_regule' || c.field === 'garantie_rachat' ||
|
|
c.field === 'statistiques_publiques' || c.field === 'bonus_inscription' ||
|
|
c.field === 'marche_secondaire' || c.field === 'investissement_auto') {
|
|
payload[c.field] = v ? 1 : 0;
|
|
} else {
|
|
payload[c.field] = v ?? null;
|
|
}
|
|
}
|
|
|
|
// Résoudre categories_inv_ids et secteurs_inv_ids
|
|
let catIds = (data.categories_inv || []).map(c => c.id);
|
|
let sectIds = (data.secteurs_inv || []).map(s => s.id);
|
|
|
|
const catChange = selectedChanges.find(c => c.field === 'categories_inv');
|
|
const sectChange = selectedChanges.find(c => c.field === 'secteurs_inv');
|
|
|
|
try {
|
|
if (catChange) {
|
|
catIds = [...catChange._resolution.resolvedIds];
|
|
for (const nom of catChange._resolution.unknownNames) {
|
|
if (toCreate[nom]) {
|
|
const created = await api.post('/ref-categories', { nom });
|
|
catIds.push(created.id);
|
|
}
|
|
}
|
|
}
|
|
if (sectChange) {
|
|
sectIds = [...sectChange._resolution.resolvedIds];
|
|
for (const nom of sectChange._resolution.unknownNames) {
|
|
if (toCreate[nom]) {
|
|
const created = await api.post('/ref-secteurs', { nom });
|
|
sectIds.push(created.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
payload.categories_inv_ids = catIds;
|
|
payload.secteurs_inv_ids = sectIds;
|
|
|
|
await api.put('/referentiel/' + id, payload);
|
|
setResult({ ok: true, msg: `✔ ${selectedChanges.length} champ${selectedChanges.length > 1 ? 's' : ''} mis à jour.` });
|
|
onApplied();
|
|
setOpen(false); setJson(''); setParsed(null); setSelected({}); setStep('json');
|
|
} catch (e) {
|
|
setResult({ ok: false, msg: `✗ ${e.message}` });
|
|
} finally { setApplying(false); }
|
|
};
|
|
|
|
const nbSelected = Object.values(selected).filter(Boolean).length;
|
|
|
|
return (
|
|
<div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden', marginBottom: 12 }}>
|
|
<button onClick={() => setOpen(o => !o)} style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
width: '100%', padding: '12px 16px', background: 'var(--surface-2)',
|
|
border: 'none', cursor: 'pointer', textAlign: 'left',
|
|
borderBottom: open ? '1px solid var(--border)' : 'none',
|
|
}}>
|
|
<span style={{ display: 'flex', alignItems: 'center', gap: 8, fontWeight: 600, fontSize: 13 }}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
<polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
|
|
</svg>
|
|
Importer les données IA
|
|
{parsed && <span style={{ marginLeft: 6, fontSize: 11, fontWeight: 400, color: 'var(--text-muted)' }}>— {parsed.changes.length} champ{parsed.changes.length > 1 ? 's' : ''} modifié{parsed.changes.length > 1 ? 's' : ''}</span>}
|
|
</span>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
|
style={{ transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .2s' }}>
|
|
<polyline points="6 9 12 15 18 9"/>
|
|
</svg>
|
|
</button>
|
|
|
|
{open && (
|
|
<div style={{ padding: 16 }}>
|
|
<ResultBanner result={result} onDismiss={() => setResult(null)} style={{ marginBottom: 12 }} />
|
|
{step === 'json' && (
|
|
<>
|
|
<p style={{ margin: '0 0 10px', fontSize: 12, color: 'var(--text-muted)' }}>
|
|
Collez ici le JSON retourné par l'IA (avec ou sans bloc <code>```json```</code>).
|
|
</p>
|
|
<textarea value={json} onChange={e => { setJson(e.target.value); setParseErr(null); }}
|
|
placeholder={'\{\n "annee_creation": 2017,\n "is_regule": true,\n ...\n\}'}
|
|
style={{ width: '100%', minHeight: 160, fontFamily: 'monospace', fontSize: 12,
|
|
resize: 'vertical', background: 'var(--surface-2)', border: '1px solid var(--border)',
|
|
borderRadius: 6, padding: 10, color: 'var(--text)', boxSizing: 'border-box' }} />
|
|
{parseErr && <div className="error" style={{ marginTop: 8 }}>{parseErr}</div>}
|
|
<button className="primary" onClick={parse} disabled={!json.trim()} style={{ marginTop: 10 }}>
|
|
Analyser les changements →
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
{step === 'creation' && parsed && (() => {
|
|
const unknownCats = parsed.catRes?.unknownNames || [];
|
|
const unknownSects = parsed.sectRes?.unknownNames || [];
|
|
const allUnknowns = [
|
|
...unknownCats.map(n => ({ nom: n, type: 'cat' })),
|
|
...unknownSects.map(n => ({ nom: n, type: 'sect' })),
|
|
];
|
|
return (
|
|
<div>
|
|
<div style={{ marginBottom: 14, padding: '12px 16px', borderRadius: 8,
|
|
background: 'rgba(245,158,11,.08)', border: '1px solid rgba(245,158,11,.3)' }}>
|
|
<div style={{ fontWeight: 600, fontSize: 13, color: '#b45309', marginBottom: 6 }}>
|
|
✦ Nouveaux tags suggérés par l'IA
|
|
</div>
|
|
<p style={{ margin: '0 0 10px', fontSize: 12, color: 'var(--text-muted)' }}>
|
|
Ces tags n'existent pas encore dans le référentiel. Cochez ceux que vous souhaitez créer avant d'importer.
|
|
</p>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
{allUnknowns.map(({ nom, type }) => (
|
|
<label key={nom} style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13 }}>
|
|
<input type="checkbox" checked={!!toCreate[nom]}
|
|
onChange={() => setToCreate(t => ({ ...t, [nom]: !t[nom] }))}
|
|
style={{ width: 'auto', cursor: 'pointer' }} />
|
|
<span className={type === 'cat' ? 'chip-cat' : 'chip-sect'}>{nom}</span>
|
|
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>
|
|
{type === 'cat' ? "catégorie d'investissement" : "secteur d'investissement"}
|
|
</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<button style={{ fontSize: 12, padding: '4px 10px' }}
|
|
onClick={() => { setParsed(null); setJson(''); setSelected({}); setResult(null); setStep('json'); }}>
|
|
← Recommencer
|
|
</button>
|
|
<button className="primary" style={{ fontSize: 12, padding: '6px 14px' }}
|
|
onClick={() => setStep('diff')}>
|
|
Continuer vers le diff →
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})()}
|
|
|
|
{step === 'diff' && parsed && (
|
|
<>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14, flexWrap: 'wrap' }}>
|
|
<span style={{ fontSize: 13 }}><strong>{nbSelected}</strong> / {parsed.changes.length} champ{parsed.changes.length > 1 ? 's' : ''} sélectionné{nbSelected > 1 ? 's' : ''}</span>
|
|
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
|
|
<button style={{ fontSize: 12, padding: '4px 10px' }}
|
|
onClick={() => { setParsed(null); setJson(''); setSelected({}); setResult(null); setStep('json'); }}>
|
|
← Recommencer
|
|
</button>
|
|
<button className="primary" style={{ fontSize: 12, padding: '6px 14px' }} disabled={nbSelected === 0 || applying} onClick={apply}>
|
|
{applying ? 'Application…' : `Appliquer (${nbSelected})`}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{parsed.changes.some(c => c._resolution?.unknownNames?.some(n => toCreate[n])) && (
|
|
<div style={{ marginBottom: 10, padding: '8px 12px', borderRadius: 6,
|
|
background: 'rgba(99,102,241,.08)', border: '1px solid rgba(99,102,241,.2)', fontSize: 12 }}>
|
|
✦ Les tags marqués seront créés lors de l'importation.
|
|
</div>
|
|
)}
|
|
|
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
|
|
<thead>
|
|
<tr style={{ background: 'var(--surface-2)' }}>
|
|
<th style={{ width: 28, padding: '6px 8px' }}></th>
|
|
<th style={{ padding: '6px 12px', textAlign: 'left', color: 'var(--text-muted)', fontWeight: 500 }}>Champ</th>
|
|
<th style={{ padding: '6px 12px', textAlign: 'left', color: 'var(--danger)', fontWeight: 600 }}>Avant</th>
|
|
<th style={{ padding: '6px 12px', textAlign: 'left', color: '#2563eb', fontWeight: 600 }}>Après</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{parsed.changes.map(c => (
|
|
<tr key={c.field} style={{ borderTop: '1px solid var(--border)', background: selected[c.field] ? 'rgba(37,99,235,.04)' : 'none' }}>
|
|
<td style={{ padding: '6px 8px', textAlign: 'center' }}>
|
|
<input type="checkbox" checked={!!selected[c.field]}
|
|
onChange={() => setSelected(s => ({ ...s, [c.field]: !s[c.field] }))}
|
|
style={{ cursor: 'pointer', width: 'auto' }} />
|
|
</td>
|
|
<td style={{ padding: '6px 12px', color: 'var(--text-muted)', fontWeight: 500 }}>
|
|
{PROFILE_FIELD_LABELS[c.field] || c.field}
|
|
</td>
|
|
<td style={{ padding: '6px 12px', color: 'var(--danger)', fontFamily: 'monospace' }}>
|
|
{fmtProfileVal(c.before)}
|
|
</td>
|
|
<td style={{ padding: '6px 12px', color: '#2563eb', fontWeight: 600, fontFamily: 'monospace' }}>
|
|
{fmtProfileVal(c.after)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Prompt par défaut ─────────────────────────────────────────────────────────
|
|
const DEFAULT_PROFIL_PROMPT = `Tu es un assistant spécialisé dans l'analyse de plateformes de crowdfunding. Analyse la page indiquée et extrais les informations de profil de la plateforme.
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
PLATEFORME À ANALYSER
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
Nom : {{PLATFORM_NAME}}
|
|
URL à analyser : {{PLATFORM_URL}}
|
|
|
|
Tu peux aussi chercher le profil CrowdSpace :
|
|
https://thecrowdspace.com/fr/platform/[slug-de-la-plateforme]/
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
DONNÉES ACTUELLEMENT EN BASE
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
{{CURRENT_DATA}}
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
CHAMPS À EXTRAIRE
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
Retourne un objet JSON avec les champs suivants (null si information absente ou incertaine) :
|
|
|
|
- description : description courte en français (2-3 phrases max)
|
|
- annee_creation : année de fondation (entier, ex: 2017)
|
|
- categories_inv : tableau de noms de catégories. Privilégie les valeurs disponibles : {{CATEGORIES_INV_LIST}}. Si la plateforme appartient clairement à une catégorie absente de cette liste (et non une reformulation d'une existante), indique-la quand même. Null si inconnu.
|
|
- secteurs_inv : tableau de noms de secteurs. Privilégie les valeurs disponibles : {{SECTEURS_INV_LIST}}. Si la plateforme opère dans un secteur absent de cette liste (et non une reformulation d'un existant), indique-le quand même. Null si inconnu.
|
|
- investisseurs_types : "particulier" | "entreprise" | "les_deux"
|
|
- is_regule : true | false
|
|
- regulateur : nom de l'autorité (ex: "AMF", "ACPR", "FCA")
|
|
- numero_licence : numéro de licence ou d'agrément
|
|
- pays_inscription : pays d'immatriculation légale
|
|
- pays_siege : pays du siège social
|
|
- pays_operation : tableau de pays (ex: ["France", "Allemagne"])
|
|
- investissement_minimum : montant minimum en EUR (ex: 20)
|
|
- rendement_annonce : taux annoncé en % (ex: 8.5)
|
|
- nb_investisseurs : nombre d'investisseurs enregistrés
|
|
- volume_total_finance : volume total en EUR (ex: 50000000)
|
|
- duree_moyenne_pret : durée moyenne en mois (ex: 18)
|
|
- garantie_rachat : true | false
|
|
- statistiques_publiques : true | false
|
|
- bonus_inscription : true | false
|
|
- marche_secondaire : true | false
|
|
- investissement_auto : true | false
|
|
- url : URL officielle du site
|
|
- url_trustpilot : URL Trustpilot complète
|
|
- url_linkedin : URL LinkedIn complète
|
|
- source : URL de la page consultée
|
|
- date_collecte : date d'aujourd'hui (ISO 8601)
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
INSTRUCTIONS
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1. Consulte l'URL fournie et/ou la page CrowdSpace.
|
|
2. N'inclus que les informations vérifiables sur les pages.
|
|
3. Si une information est incertaine, mets null.
|
|
4. Pour les booléens, base-toi uniquement sur ce qui est explicitement mentionné.
|
|
5. Retourne UNIQUEMENT le JSON, sans commentaires ni explication.`;
|
|
|
|
// ── Bloc prompt IA ────────────────────────────────────────────────────────────
|
|
function ProfilPromptBlock({ data, catsInv = [], secteursInv = [] }) {
|
|
const LS_KEY = 'cl_profil_prompt';
|
|
const [open, setOpen] = useState(false);
|
|
const [editing, setEditing] = useState(false);
|
|
const [prompt, setPrompt] = useState(() => localStorage.getItem(LS_KEY) || DEFAULT_PROFIL_PROMPT);
|
|
const [draft, setDraft] = useState('');
|
|
const [url, setUrl] = useState('');
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const currentDataLines = () => {
|
|
const fields = [
|
|
['description', data.description],
|
|
['annee_creation', data.annee_creation],
|
|
['categories_inv', (data.categories_inv || []).map(c => c.nom).join(', ') || null],
|
|
['secteurs_inv', (data.secteurs_inv || []).map(s => s.nom).join(', ') || null],
|
|
['investisseurs_types', data.investisseurs_types],
|
|
['is_regule', data.is_regule ? 'oui' : 'non'],
|
|
['regulateur', data.regulateur],
|
|
['numero_licence', data.numero_licence],
|
|
['pays_inscription', data.pays_inscription],
|
|
['domiciliation', countryLabel(data.domiciliation)],
|
|
['pays_operation', Array.isArray(data.pays_operation) ? data.pays_operation.join(', ') : data.pays_operation],
|
|
['investissement_minimum', data.investissement_minimum != null ? data.investissement_minimum + ' €' : null],
|
|
['rendement_annonce', data.rendement_annonce != null ? data.rendement_annonce + ' %' : null],
|
|
['nb_investisseurs', data.nb_investisseurs],
|
|
['volume_total_finance', data.volume_total_finance != null ? data.volume_total_finance + ' €' : null],
|
|
['duree_moyenne_pret', data.duree_moyenne_pret != null ? data.duree_moyenne_pret + ' mois' : null],
|
|
['garantie_rachat', data.garantie_rachat ? 'oui' : 'non'],
|
|
['statistiques_publiques', data.statistiques_publiques ? 'oui' : 'non'],
|
|
['bonus_inscription', data.bonus_inscription ? 'oui' : 'non'],
|
|
['marche_secondaire', data.marche_secondaire ? 'oui' : 'non'],
|
|
['investissement_auto', data.investissement_auto ? 'oui' : 'non'],
|
|
['url', data.url],
|
|
['url_trustpilot', data.url_trustpilot],
|
|
['url_linkedin', data.url_linkedin],
|
|
];
|
|
return fields.map(([k, v]) => `- ${k}: ${v ?? '(vide)'}`).join('\n');
|
|
};
|
|
|
|
const resolvedPrompt = () => {
|
|
const catsList = (catsInv || []).map(c => c.nom).join(' | ') || 'aucune catégorie définie';
|
|
const sectsList = (secteursInv || []).map(s => s.nom).join(' | ') || 'aucun secteur défini';
|
|
return prompt
|
|
.replace('{{PLATFORM_NAME}}', data.nom)
|
|
.replace('{{PLATFORM_URL}}', url || '(à renseigner ci-dessus)')
|
|
.replace('{{CURRENT_DATA}}', currentDataLines())
|
|
.replace('{{CATEGORIES_INV_LIST}}', catsList)
|
|
.replace('{{SECTEURS_INV_LIST}}', sectsList);
|
|
};
|
|
|
|
const startEdit = () => { setDraft(prompt); setEditing(true); };
|
|
const saveEdit = () => { setPrompt(draft); localStorage.setItem(LS_KEY, draft); setEditing(false); };
|
|
const reset = () => { setPrompt(DEFAULT_PROFIL_PROMPT); localStorage.removeItem(LS_KEY); setEditing(false); };
|
|
|
|
const copy = async () => {
|
|
const text = resolvedPrompt();
|
|
try { await navigator.clipboard.writeText(text); }
|
|
catch { const t = document.createElement('textarea'); t.value = text; document.body.appendChild(t); t.select(); document.execCommand('copy'); document.body.removeChild(t); }
|
|
setCopied(true); setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
return (
|
|
<div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden' }}>
|
|
<button onClick={() => setOpen(o => !o)} style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
width: '100%', padding: '12px 16px', background: 'var(--surface-2)',
|
|
border: 'none', cursor: 'pointer', textAlign: 'left',
|
|
borderBottom: open ? '1px solid var(--border)' : 'none',
|
|
}}>
|
|
<span style={{ display: 'flex', alignItems: 'center', gap: 8, fontWeight: 600, fontSize: 13 }}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-1H1a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"/>
|
|
<circle cx="7.5" cy="14.5" r="1.5"/><circle cx="16.5" cy="14.5" r="1.5"/>
|
|
</svg>
|
|
PROMPT IA — Collecte du profil plateforme
|
|
</span>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
|
style={{ transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .2s' }}>
|
|
<polyline points="6 9 12 15 18 9"/>
|
|
</svg>
|
|
</button>
|
|
|
|
{open && (
|
|
<div style={{ padding: 16 }}>
|
|
<p style={{ margin: '0 0 12px', fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.5 }}>
|
|
Le prompt inclut les données actuelles de la plateforme. Renseignez l'URL à analyser (CrowdSpace ou site officiel), copiez, puis collez dans un assistant IA capable de naviguer sur le web.
|
|
</p>
|
|
|
|
{/* URL input */}
|
|
<div style={{ marginBottom: 12 }}>
|
|
<label style={{ fontSize: 12, color: 'var(--text-muted)', display: 'block', marginBottom: 4 }}>
|
|
URL de la page à analyser
|
|
</label>
|
|
<input type="url" value={url} onChange={e => setUrl(e.target.value)}
|
|
placeholder="https://thecrowdspace.com/fr/platform/..."
|
|
style={{ width: '100%', fontSize: 13, boxSizing: 'border-box' }} />
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
|
|
<button className="primary" onClick={copy} style={{ fontSize: 12, padding: '6px 14px' }}>
|
|
{copied ? '✓ Copié !' : '📋 Copier le prompt'}
|
|
</button>
|
|
{!editing ? (
|
|
<button onClick={startEdit} style={{ fontSize: 12, padding: '6px 14px' }}>✏️ Modifier le template</button>
|
|
) : (
|
|
<>
|
|
<button className="primary" onClick={saveEdit} style={{ fontSize: 12, padding: '6px 14px' }}>Enregistrer</button>
|
|
<button onClick={() => setEditing(false)} style={{ fontSize: 12, padding: '6px 14px' }}>Annuler</button>
|
|
<button onClick={reset} style={{ fontSize: 12, padding: '6px 14px', color: 'var(--text-muted)', marginLeft: 'auto' }}>Remettre par défaut</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{editing ? (
|
|
<textarea value={draft} onChange={e => setDraft(e.target.value)}
|
|
style={{ width: '100%', minHeight: 400, fontFamily: 'monospace', fontSize: 12, lineHeight: 1.5,
|
|
resize: 'vertical', boxSizing: 'border-box', background: 'var(--surface-2)',
|
|
border: '1px solid var(--border)', borderRadius: 6, padding: 12, color: 'var(--text)' }} />
|
|
) : (
|
|
<pre style={{ margin: 0, padding: 14, background: 'var(--surface-2)', border: '1px solid var(--border)',
|
|
borderRadius: 6, fontSize: 12, fontFamily: 'monospace', lineHeight: 1.6,
|
|
whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: 'var(--text)', maxHeight: 400, overflowY: 'auto' }}>
|
|
{resolvedPrompt()}
|
|
</pre>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|