import { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { api } from '../api.js'; import { useInvestisseur } from '../context/InvestisseurContext.jsx'; import { fmtDate } from '../utils/format.js'; import ResultBanner from '../components/ResultBanner.jsx'; const MODULES = { depots_retraits: { label: 'Dépôts / Retraits', required: ['date_operation', 'type', 'montant'], optional: ['plateforme_id', 'libelle', 'reference'], needsInvestisseur: true, }, investissements: { label: 'Investissements', required: ['nom_projet', 'date_souscription', 'montant_investi'], optional: ['plateforme_id', 'emetteur', 'date_premiere_echeance', 'date_cible', 'taux_interet', 'duree_mois', 'type_remb', 'freq_interets', 'statut', 'reference'], needsInvestisseur: true, }, remboursements: { label: 'Remboursements', required: ['investissement_id', 'date_remb'], optional: ['capital', 'interets_bruts', 'prelev_sociaux', 'prelev_forfaitaire', 'net_recu', 'statut'], needsInvestisseur: true, }, plateformes: { label: 'Plateformes', required: ['nom'], optional: ['url', 'notes'], needsInvestisseur: false, note: 'Les plateformes dont le nom existe déjà seront ignorées (pas d\'écrasement).', }, taux_pfu: { label: 'Flat Tax — Taux PFU', required: ['annee', 'pfu_total', 'impot_revenu', 'prelev_sociaux'], optional: [], needsInvestisseur: false, global: true, note: 'Table de référence globale. Si une année existe déjà, ses taux seront mis à jour (upsert).', }, }; const MODULE_LABEL = { depots_retraits: 'Dépôts / Retraits', investissements: 'Investissements', remboursements: 'Remboursements', plateformes: 'Plateformes', taux_pfu: 'Flat Tax — Taux PFU', }; export default function Imports() { const { activeId } = useInvestisseur(); const navigate = useNavigate(); // ── Import classique (xlsx/csv/json) ────────────────────────── const [module, setModule] = useState('depots_retraits'); const [file, setFile] = useState(null); const [preview, setPreview] = useState(null); const [mapping, setMapping] = useState({}); const [defaults, setDefaults] = useState({}); const [plats, setPlats] = useState([]); const [investissements,setInvestissements] = useState([]); const [history, setHistory] = useState([]); const [result, setResult] = useState(null); const [busy, setBusy] = useState(false); const [err, setErr] = useState(null); // ── Import dossier investissement ───────────────────────────── const [dossierFile, setDossierFile] = useState(null); const [dossierPreview, setDossierPreview] = useState(null); // parsed JSON for review const [dossierResult, setDossierResult] = useState(null); const [dossierBusy, setDossierBusy] = useState(false); const [dossierErr, setDossierErr] = useState(null); const dossierInputRef = useRef(null); // History + plateformes sont user-scoped → chargement sans activeId useEffect(() => { api.get('/imports/history').then(setHistory).catch(() => {}); api.get('/plateformes').then(setPlats).catch(() => {}); }, []); // Investissements sont investisseur-scoped → besoin de activeId useEffect(() => { if (!activeId) return; api.get('/investissements').then(setInvestissements).catch(() => {}); }, [activeId]); const def = MODULES[module]; const allTargets = def ? [...def.required, ...def.optional] : []; const missingInv = def?.needsInvestisseur && !activeId; const onPreview = async () => { if (!file) return; setBusy(true); setErr(null); setResult(null); try { const fd = new FormData(); fd.append('file', file); const r = await api.upload('/imports/preview', fd); setPreview(r); // Auto-map colonnes dont le nom correspond à une cible const auto = {}; for (const t of allTargets) { const col = r.headers.find(h => h.toLowerCase().replace(/\W/g, '_') === t); if (col) auto[t] = col; } setMapping(auto); } catch (e) { setErr(e.message); } finally { setBusy(false); } }; const apply = async () => { setBusy(true); setErr(null); try { const r = await api.post('/imports/apply', { tempId: preview.tempId, module, mapping, defaults, originalFilename: file?.name ?? preview.filename, }); setResult({ ok: true, msg: `✔ Import terminé : ${r.inserted} / ${r.total} lignes insérées${r.skipped > 0 ? `, ${r.skipped} ignorées` : ''}.${r.errors?.length > 0 ? ` (${r.errors.length} avertissement(s))` : ''}`, }); setPreview(null); setFile(null); setMapping({}); setDefaults({}); api.get('/imports/history').then(setHistory).catch(() => {}); // Recharger les plateformes si c'est ce qui vient d'être importé if (module === 'plateformes') { api.get('/plateformes').then(setPlats).catch(() => {}); } } catch (e) { setErr(e.message); } finally { setBusy(false); } }; return ( <>

Import Données

1. Fichier source

{ setFile(e.target.files[0]); setPreview(null); setResult(null); setErr(null); }} />
{/* Note contextuelle du module sélectionné */} {def?.note && (
{def.global && ( )} {def.note}
)} {/* Avertissement si module investisseur-scoped mais aucun investisseur actif */} {missingInv && (
Sélectionnez un investisseur actif avant d'importer ce module.
)} {err &&
{err}
} setResult(null)} style={{ marginTop: 12 }} />
{preview && ( <>

2. Mappage des colonnes

Fichier : {preview.filename} — feuille {preview.sheetName} — {preview.allRowCount} lignes. {' '}Champs marqués * obligatoires. {' '}Si la colonne n'existe pas, fournissez une valeur par défaut.

{allTargets.map(t => { const isReq = def.required.includes(t); return ( ); })}
Champ cible Colonne Excel Valeur par défaut
{t} {isReq && *} {t === 'plateforme_id' ? ( ) : t === 'investissement_id' ? ( ) : ( setDefaults({ ...defaults, [t]: e.target.value })} placeholder={t === 'statut' ? 'ex. en_cours' : t === 'type' ? 'ex. depot' : ''} /> )}

Aperçu (10 premières lignes)

{preview.headers.map(h => )} {preview.sampleRows.map((r, i) => ( {preview.headers.map(h => )} ))}
{h}
{String(r[h] ?? '')}
)} {/* ── Import Dossier Investissement ──────────────────────── */} api.get('/imports/history').then(setHistory).catch(() => {})} />

Historique des imports

{history.length === 0 && ( )} {history.map(h => ( ))}
Date Module Fichier Total OK KO
Aucun import
{fmtDate(h.created_at)} {MODULE_LABEL[h.module] ?? h.module} {h.filename} {h.rows_total} {h.rows_inserted} 0 ? 'var(--warning)' : undefined }}> {h.rows_skipped}
); } /* ── Composant import dossier ──────────────────────────────────── */ function DossierImport({ activeId, navigate, dossierFile, setDossierFile, dossierPreview, setDossierPreview, dossierResult, setDossierResult, dossierBusy, setDossierBusy, dossierErr, setDossierErr, dossierInputRef, reloadHistory, }) { const missingInv = !activeId; const onFileChange = (e) => { const f = e.target.files[0]; setDossierFile(f || null); setDossierPreview(null); setDossierResult(null); setDossierErr(null); if (!f) return; const reader = new FileReader(); reader.onload = (ev) => { try { const parsed = JSON.parse(ev.target.result); if (parsed.type !== 'dossier_investissement') { setDossierErr('Ce fichier n\'est pas un dossier investissement valide (type incorrect).'); return; } setDossierPreview(parsed); } catch { setDossierErr('Fichier JSON invalide — vérifiez la syntaxe.'); } }; reader.readAsText(f); }; const onImport = async () => { if (!dossierPreview) return; setDossierBusy(true); setDossierErr(null); setDossierResult(null); try { const r = await api.post('/imports/dossier', { dossier: dossierPreview }); setDossierResult(r); setDossierFile(null); setDossierPreview(null); if (dossierInputRef.current) dossierInputRef.current.value = ''; reloadHistory(); } catch (e) { setDossierErr(e.message); } finally { setDossierBusy(false); } }; const dp = dossierPreview; const inv = dp?.investissement; const multiDetenteur = new Set(plats.map(p => p.investisseur_id)).size > 1; return (

Import — Dossier investissement

Restaure ou migre un dossier complet (investissement + remboursements + historique) depuis un fichier .json exporté par cette application. Si le dossier existe déjà, il sera mis à jour ; sinon il sera créé.

{missingInv && (
Sélectionnez un investisseur actif avant d'importer un dossier.
)}
{dossierErr &&
{dossierErr}
} {/* Aperçu du dossier avant import */} {dp && inv && (

Aperçu du dossier

Projet{inv.nom_projet}
Plateforme{dp.plateforme?.nom}
Date souscription{fmtDate(inv.date_souscription)}
Montant investi{inv.montant_investi} €
Statut{inv.statut}
Remboursements{dp.remboursements?.length ?? 0} enregistrement(s)
Projections{dp.projections?.length ?? 0} échéance(s)
Historique{dp.historique?.length ?? 0} entrée(s)
Exporté le{dp.exported_at}
)} {dossierResult && (
{dossierResult.action === 'created' ? '✔ Dossier créé avec succès.' : '✔ Dossier mis à jour avec succès.' } {' '}
)}
); }