import { useEffect, useState } from 'react'; import { api } from '../api.js'; const MOIS_LABELS = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre']; const MOIS_NUMS = ['01','02','03','04','05','06','07','08','09','10','11','12']; const r2 = v => Math.round((v ?? 0) * 100) / 100; const fmtN = v => Number(v).toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const fmtI = v => Number(v).toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); const fullName = l => { const n = l.investisseur_nom ?? ''; const p = l.investisseur_prenom ?? ''; return p && n.startsWith(p) ? n : [p, n].filter(Boolean).join(' '); }; const BADGE_AUTO = { text: 'déclaration automatique', bg: 'rgba(22,163,74,0.1)', color: '#16a34a' }; const BADGE_DECL = { text: 'à déclarer', bg: 'rgba(239,68,68,0.1)', color: '#dc2626' }; function BadgeTag({ badge }) { if (!badge) return null; return ( {badge.text} ); } /* breakdown item: { nom, val, badge } */ function Case2042({ code, label, note, value, breakdown }) { if (!value) return null; return (
{code}
{label}
{note &&
{note}
}
{fmtI(value)} €
{breakdown && breakdown.length > 0 && (
{breakdown.map((p, i) => (
└ {p.nom} {fmtI(p.val)} €
))}
)}
); } export default function Cerfa2042Preview({ annee, activeView, pfoAssujetti }) { const LS_EXCL = 'cl_2778_excluded_plats'; const [data2561, setData2561] = useState(null); const [data2778, setData2778] = useState(null); const [pfuList, setPfuList] = useState([]); const [excluded, setExcluded] = useState(() => { try { return new Set(JSON.parse(localStorage.getItem(LS_EXCL)) ?? []); } catch { return new Set(); } }); const [loading, setLoading] = useState(true); const [view, setView] = useState('matrice'); const [filterMode, setFilterMode] = useState('all'); // 'all' | 'auto' | 'decl' useEffect(() => { setLoading(true); const scopeParams = activeView === 'all' ? { scope: 'all' } : {}; Promise.all([ api.get('/taxreport/cerfa2561', { annee, ...scopeParams }), pfuList.length === 0 ? api.get('/pfu') : Promise.resolve(null), api.get('/taxreport/2778', { annee, ...scopeParams }), ]).then(([d2561, pfu, d2778]) => { setData2561(d2561); if (pfu) setPfuList(pfu); if (d2778) setData2778(d2778); }) .finally(() => setLoading(false)); }, [annee, activeView]); // eslint-disable-line const frLignes = (data2561?.lignes ?? []).filter(l => l.domiciliation === 'FR'); const platEtr = (data2778?.plateformes ?? []).filter(p => !excluded.has(p.id)); const ratesForYear = () => { const sorted = [...pfuList].sort((a, b) => b.annee - a.annee); const m = sorted.find(r => r.annee <= Number(annee)); if (!m) return { pfo: 0.128, csg: 0.106, crds: 0.005, solidarite: 0.075 }; return { pfo: (m.impot_revenu ?? 12.8) / 100, csg: (m.csg ?? 10.6) / 100, crds: (m.crds ?? 0.5) / 100, solidarite: (m.solidarite ?? 7.5) / 100 }; }; const rates = ratesForYear(); const totalTaxRate = rates.pfo + rates.csg + rates.crds + rates.solidarite; /* ── Suivi mensuel combiné ── */ const matriceView = (
{loading &&
Chargement…
} {!loading && frLignes.length === 0 && platEtr.length === 0 && (

Aucune donnée pour {annee}.

)} {!loading && (frLignes.length > 0 || platEtr.length > 0) && (

Suivi mensuel des intérêts bruts

Revenus {annee} — toutes plateformes
{MOIS_LABELS.map(m => ( ))} {frLignes.length > 0 && ( )} {frLignes.map(l => { const total = r2(Object.values(l.mois ?? {}).reduce((s, m) => s + (m.interets_bruts ?? 0), 0)); return ( {MOIS_NUMS.map(m => { const v = l.mois?.[m]?.interets_bruts ?? 0; return ; })} ); })} {frLignes.length > 0 && ( {MOIS_NUMS.map(m => { const v = r2(frLignes.reduce((s, l) => s + (l.mois?.[m]?.interets_bruts ?? 0), 0)); return ; })} )} {platEtr.length > 0 && ( )} {platEtr.map(p => { const total = r2(Object.values(p.mois ?? {}).reduce((s, v) => s + v, 0)); return ( {MOIS_NUMS.map(m => { const v = p.mois?.[m] ?? 0; return ; })} ); })} {platEtr.length > 0 && ( {MOIS_NUMS.map(m => { const v = r2(platEtr.reduce((s, p) => s + (p.mois?.[m] ?? 0), 0)); return ; })} )} {MOIS_NUMS.map(m => { const v = r2(frLignes.reduce((s, l) => s + (l.mois?.[m]?.interets_bruts ?? 0), 0) + platEtr.reduce((s, p) => s + (p.mois?.[m] ?? 0), 0)); return ; })} {/* Taux total prélevé */} {MOIS_NUMS.map(m => { const brut = r2(frLignes.reduce((s, l) => s + (l.mois?.[m]?.interets_bruts ?? 0), 0) + platEtr.reduce((s, p) => s + (p.mois?.[m] ?? 0), 0)); const prelevFR = r2(frLignes.reduce((s, l) => s + ((l.mois?.[m]?.prelev_sociaux ?? 0) + (l.mois?.[m]?.prelev_forfaitaire ?? 0)), 0)); const prelevEtr = r2(platEtr.reduce((s, p) => s + (p.mois?.[m] ?? 0), 0) * totalTaxRate); const taux = brut > 0 ? ((prelevFR + prelevEtr) / brut * 100).toFixed(1) + ' %' : '—'; return ; })} {/* Total intérêt net */} {MOIS_NUMS.map(m => { const netFR = r2(frLignes.reduce((s, l) => { const mo = l.mois?.[m]; return s + ((mo?.interets_bruts ?? 0) - (mo?.prelev_sociaux ?? 0) - (mo?.prelev_forfaitaire ?? 0)); }, 0)); const netEtr = r2(platEtr.reduce((s, p) => s + (p.mois?.[m] ?? 0), 0) * (1 - totalTaxRate)); const net = r2(netFR + netEtr); return ; })}
Plateforme — Détenteur{m}Total
Plateformes françaises — déclaration automatique
{l.plateforme_nom} — {fullName(l)} 0 ? 'var(--text)' : 'var(--text-muted)' }}>{v > 0 ? fmtN(v) + ' €' : '—'}{total > 0 ? fmtN(total) + ' €' : '—'}
Total Plateformes françaises (brut) 0 ? '#16a34a' : 'var(--text-muted)' }}>{v > 0 ? fmtN(v) + ' €' : '—'} {fmtN(r2(frLignes.reduce((s, l) => s + r2(Object.values(l.mois ?? {}).reduce((ss, m) => ss + (m.interets_bruts ?? 0), 0)), 0)))} €
Plateformes étrangères — à déclarer
{p.nom} {(p.investisseur_nom || p.investisseur_prenom) && ( — {fullName(p)} )} 0 ? 'var(--text)' : 'var(--text-muted)' }}>{v > 0 ? fmtN(v) + ' €' : '—'}{total > 0 ? fmtN(total) + ' €' : '—'}
Total Plateformes étrangères (brut) 0 ? '#dc2626' : 'var(--text-muted)' }}>{v > 0 ? fmtN(v) + ' €' : '—'} {fmtN(r2(platEtr.reduce((s, p) => s + r2(Object.values(p.mois ?? {}).reduce((ss, v) => ss + v, 0)), 0)))} €
Total général (brut) 0 ? 'var(--text)' : 'var(--text-muted)' }}>{v > 0 ? fmtN(v) + ' €' : '—'} {fmtN(r2( frLignes.reduce((s, l) => s + r2(Object.values(l.mois ?? {}).reduce((ss, m) => ss + (m.interets_bruts ?? 0), 0)), 0) + platEtr.reduce((s, p) => s + r2(Object.values(p.mois ?? {}).reduce((ss, v) => ss + v, 0)), 0) ))} €
Taux total prélevé ({(totalTaxRate * 100).toFixed(1)} %) {taux} {(totalTaxRate * 100).toFixed(1)} %
Total intérêt net 0 ? 'var(--success)' : 'var(--text-muted)' }}>{net > 0 ? fmtN(net) : '—'} {fmtN(r2( frLignes.reduce((s, l) => s + r2(Object.values(l.mois ?? {}).reduce((ss, m) => ss + ((m.interets_bruts ?? 0) - (m.prelev_sociaux ?? 0) - (m.prelev_forfaitaire ?? 0)), 0)), 0) + platEtr.reduce((s, p) => s + r2(Object.values(p.mois ?? {}).reduce((ss, v) => ss + v, 0)) * (1 - totalTaxRate), 0) ))} €
Plateformes françaises : prélèvements à la source (taux {(totalTaxRate * 100).toFixed(1)} %) — déclaration automatique sur 2042  ·  Plateformes étrangères : à déclarer
)}
); /* ── Données 2042 — cases mixtes par code ── */ const données2042View = (() => { if (loading) return
Chargement…
; // Totaux FR const fr2TT = frLignes.reduce((s, l) => s + (l.case_2TT ?? 0), 0); const fr2TR = frLignes.reduce((s, l) => s + (l.case_2TR ?? 0), 0); const fr2BH = frLignes.reduce((s, l) => s + (l.case_2BH ?? 0), 0); const fr2CK = frLignes.reduce((s, l) => s + (l.case_2CK ?? 0), 0); const fr2TY = frLignes.reduce((s, l) => s + (l.case_2TY ?? 0), 0); // Totaux étrangers const etrBA = p => r2(Object.values(p.mois ?? {}).reduce((s, v) => s + v, 0)); const applyFilter = items => filterMode === 'all' ? items : items.filter(i => (filterMode === 'auto' ? i.badge === BADGE_AUTO : i.badge === BADGE_DECL)); const totalEtrBA = r2(platEtr.reduce((s, p) => s + etrBA(p), 0)); const totalEtrIA = Math.round(totalEtrBA * rates.pfo); // Breakdown 2TT : FR seulement const bd2TT = applyFilter(frLignes.filter(l => l.case_2TT > 0).map(l => ({ nom: `${l.plateforme_nom} — ${fullName(l)}`, val: l.case_2TT, badge: BADGE_AUTO }))); const total2TT = bd2TT.reduce((s, i) => s + i.val, 0); // Breakdown 2TR : FR (case_2TR) + étrangères const bd2TR = applyFilter([ ...frLignes.filter(l => l.case_2TR > 0).map(l => ({ nom: `${l.plateforme_nom} — ${fullName(l)}`, val: l.case_2TR, badge: BADGE_AUTO })), ...platEtr.filter(p => etrBA(p) > 0).map(p => ({ nom: p.nom, val: Math.round(etrBA(p)), badge: BADGE_DECL })), ]); const total2TR = bd2TR.reduce((s, i) => s + i.val, 0); // Breakdown 2BH : FR + étrangères const bd2BH = applyFilter([ ...frLignes.filter(l => l.case_2BH > 0).map(l => ({ nom: `${l.plateforme_nom} — ${fullName(l)}`, val: l.case_2BH, badge: BADGE_AUTO })), ...platEtr.filter(p => etrBA(p) > 0).map(p => ({ nom: p.nom, val: Math.round(etrBA(p)), badge: BADGE_DECL })), ]); const total2BH = bd2BH.reduce((s, i) => s + i.val, 0); // Breakdown 2CK : FR (PFNL retenu) + étrangères (acompte 2778-SD) const bd2CK = applyFilter([ ...frLignes.filter(l => l.case_2CK > 0).map(l => ({ nom: `${l.plateforme_nom} — ${fullName(l)}`, val: l.case_2CK, badge: BADGE_AUTO })), ...platEtr.filter(p => etrBA(p) > 0).map(p => ({ nom: p.nom, val: Math.round(etrBA(p) * rates.pfo), badge: BADGE_DECL })), ]); const total2CK = bd2CK.reduce((s, i) => s + i.val, 0); // Breakdown 2TY : FR seulement const bd2TY = applyFilter(frLignes.filter(l => l.case_2TY > 0).map(l => ({ nom: `${l.plateforme_nom} — ${fullName(l)}`, val: l.case_2TY, badge: BADGE_AUTO }))); const total2TY = bd2TY.reduce((s, i) => s + i.val, 0); return (

Report annuel — Déclaration 2042

Revenus {annee} à reporter sur la 2042 déposée en {Number(annee) + 1}
{[['all','Tout'],['auto','Automatique'],['decl','À déclarer']].map(([val, label]) => ( ))}
Synthèse fiscale Les montants sont automatiquement reportés par la plateforme (IFU). Les montants nécessitent une déclaration mensuelle 2778-SD et un report manuel sur la 2042.
); })(); return (
{view === 'matrice' && matriceView} {view === 'donnees' && données2042View}
); }