import { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import PageIcon from '../components/PageIcon.jsx'; import EmptyState from '../components/EmptyState.jsx'; import Pagination from '../components/Pagination.jsx'; import { usePagination } from '../hooks/usePagination.js'; import { api } from '../api.js'; import { useInvestisseur } from '../context/InvestisseurContext.jsx'; import { useUi } from '../context/UiContext.jsx'; import { fmtEUR, fmtStatut } from '../utils/format.js'; import Cerfa2561Preview from '../components/Cerfa2561Preview.jsx'; import CerfaRecapTable from '../components/CerfaRecapTable.jsx'; import Cerfa2778Preview from '../components/Cerfa2778Preview.jsx'; import Cerfa2047Preview from '../components/Cerfa2047Preview.jsx'; import Cerfa2042Preview from '../components/Cerfa2042Preview.jsx'; import Cerfa2042CProPreview from '../components/Cerfa2042CProPreview.jsx'; /* ── YearSelector ────────────────────────────────────────────── */ function YearSelector({ annee, setAnnee, availableYears }) { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { if (!open) return; const h = e => { if (!ref.current?.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h); }, [open]); return (
setOpen(v => !v)} style={{ height: '100%', boxSizing: 'border-box', background: 'linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%)', borderRadius: 10, padding: '12px 16px', boxShadow: open ? '0 6px 28px color-mix(in srgb, var(--primary) 45%, transparent)' : '0 4px 20px color-mix(in srgb, var(--primary) 30%, transparent)', cursor: 'pointer', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', userSelect: 'none', transition: 'box-shadow .15s', }} >
Année
{annee}
{open && (
{availableYears.map((yr, i) => { const isActive = String(yr) === String(annee); return (
{ setAnnee(String(yr)); setOpen(false); }} style={{ padding: '10px 16px', cursor: 'pointer', background: isActive ? 'color-mix(in srgb, var(--primary) 8%, transparent)' : 'transparent', color: isActive ? 'var(--primary)' : 'var(--text)', fontWeight: isActive ? 700 : 400, fontSize: 'var(--fs-sm)', borderBottom: i < availableYears.length - 1 ? '1px solid var(--border)' : 'none', display: 'flex', alignItems: 'center', justifyContent: 'space-between', transition: 'background .1s', }} onMouseEnter={e => { if (!isActive) e.currentTarget.style.background = 'var(--surface-2)'; }} onMouseLeave={e => { if (!isActive) e.currentTarget.style.background = 'transparent'; }} > {yr} {isActive && ( )}
); })}
)}
); } /* ── ExportDropdown ──────────────────────────────────────────── */ function ExportDropdown({ disabled, onCSV, onJSON }) { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { if (!open) return; const h = e => { if (!ref.current?.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h); }, [open]); const choose = fn => { setOpen(false); fn(); }; return (
{open && (
)}
); } /* ── Ligne d'explication d'un formulaire CERFA ── */ function InfoLigneCerfa({ code, titre, texte }) { return (
{code}
{titre}
{texte}
); } /* ── Schéma : quel formulaire alimente quel formulaire ── */ function CerfaFlowDiagram({ hasEtranger }) { const boxFill = 'var(--surface-2)'; const box = (x, y, w, h, code, label, highlight) => ( {code} {label} ); const arrow = (x1, y1, x2, y2, key) => ( ); const markerDef = ( ); if (!hasEtranger) { return ( {markerDef} {arrow(160, 40, 228, 40, 'a1')} {box(20, 15, 140, 50, '2561', 'IFU')} {box(230, 15, 170, 50, '2042', 'Déclaration revenus', true)} ); } return ( {markerDef} {arrow(520, 58, 520, 98, 'a1')} {arrow(100, 148, 260, 190, 'a2')} {arrow(520, 148, 362, 190, 'a3')} {box(440, 8, 160, 50, '2778-SD', 'PFO mensuel')} {box(20, 98, 160, 50, '2561', 'IFU')} {box(440, 98, 160, 50, '2047', 'Revenus étranger')} {box(210, 190, 200, 54, '2042', 'Déclaration revenus', true)} ); } /* ── Composant principal ─────────────────────────────────────── */ export default function TaxReport() { const navigate = useNavigate(); const { activeId, activeView } = useInvestisseur(); const { pfoAssujetti } = useUi(); const [annee, setAnnee] = useState(String(new Date().getFullYear())); const [availableYears, setAvailableYears] = useState([]); const [data, setData] = useState(null); const [data2042, setData2042] = useState(null); const [loading, setLoading] = useState(false); const [activeTab, setActiveTab] = useState('2042'); const [detailExpanded, setDetailExpanded] = useState(false); const [cerfaExpanded, setCerfaExpanded] = useState(false); const [hasPlateformes, setHasPlateformes] = useState(true); // true par défaut pour éviter un flash du mauvais message const [hasEtranger, setHasEtranger] = useState(false); // au moins une plateforme étrangère PSFP configurée (pour l'onglet CERFA 2047 — indépendant de l'année sélectionnée, même principe que le 2778-SD ; les plateformes PSAN ne sont pas concernées) const [hasPsan, setHasPsan] = useState(false); // au moins une plateforme PSAN (crypto) configurée, quelle que soit sa domiciliation (pour l'onglet CERFA 2042 C PRO — indépendant de l'année sélectionnée, même principe que hasEtranger) /* ── Plateformes configurées ? (pour distinguer "pas de plateforme" de "pas encore de données") ── */ useEffect(() => { api.get('/plateformes').then(p => { setHasPlateformes((p || []).length > 0); setHasEtranger((p || []).some(pl => pl.domiciliation && pl.domiciliation !== 'FR' && (pl.categorie_plateforme ?? 'psfp') === 'psfp')); setHasPsan((p || []).some(pl => (pl.categorie_plateforme ?? 'psfp') === 'psan')); }).catch(() => {}); }, []); /* ── Chargement des années disponibles ── */ useEffect(() => { if (!activeId && activeView !== 'all') return; const scopeParams = activeView === 'all' ? { scope: 'all' } : {}; api.get('/taxreport/years', scopeParams).then(years => { setAvailableYears(years); // Si l'année courante n'est pas dans la liste, prendre la première disponible if (years.length > 0 && !years.map(String).includes(annee)) { setAnnee(String(years[0])); } }); }, [activeId, activeView]); // eslint-disable-line const load = () => { if (!activeId && activeView !== 'all') return; setData(null); setLoading(true); const scopeParams = activeView === 'all' ? { scope: 'all' } : {}; const LS_EXCL = 'cl_2778_excluded_plats'; const excluded = new Set(JSON.parse(localStorage.getItem(LS_EXCL) ?? '[]')); Promise.all([ api.get('/taxreport', { annee, ...scopeParams }), api.get('/taxreport/cerfa2561', { annee, ...scopeParams }), api.get('/taxreport/2778', { annee, ...scopeParams }), ]).then(([d, d2561, d2778]) => { setData(d); const frLignes = (d2561?.lignes ?? []).filter(l => l.domiciliation === 'FR'); // case_2TY (pertes en capital) n'est pas spécifique FR/étranger contrairement aux autres // cases — une perte définitive sur une plateforme étrangère doit compter aussi. const allLignes = d2561?.lignes ?? []; const platEtr = (d2778?.plateformes ?? []).filter(p => !excluded.has(p.id)); const etrBA = p => Object.values(p.mois ?? {}).reduce((s, v) => s + v, 0); const pfo = 0.128; // taux par défaut — affiné par pfuList si dispo setData2042({ case_2TT: frLignes.reduce((s, l) => s + (l.case_2TT ?? 0), 0), case_2TR: frLignes.reduce((s, l) => s + (l.case_2TR ?? 0), 0) + Math.round(platEtr.reduce((s, p) => s + etrBA(p), 0)), case_2BH: frLignes.reduce((s, l) => s + (l.case_2BH ?? 0), 0) + (pfoAssujetti ? Math.round(platEtr.reduce((s, p) => s + etrBA(p), 0)) : 0), case_2CK: frLignes.reduce((s, l) => s + (l.case_2CK ?? 0), 0) + (pfoAssujetti ? Math.round(platEtr.reduce((s, p) => s + etrBA(p), 0) * pfo) : 0), case_2TY: allLignes.reduce((s, l) => s + (l.case_2TY ?? 0), 0), }); }).finally(() => setLoading(false)); }; useEffect(load, [activeId, activeView, annee, pfoAssujetti]); // eslint-disable-line /* ── Pagination détail ── */ const detail = data?.detail ?? []; const { pagedItems: pagedDetail, page: detPage, setPage: setDetPage, pageSize: detPageSize, setPageSize: setDetPageSize, totalPages: detTotalPages, totalItems: detTotalItems, PAGE_SIZES, } = usePagination(detail, 'cl_pagesize_fiscal_detail', [activeTab]); /* ── Exports ── */ const dlBlob = (content, filename, type) => { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }; const downloadCsv = () => { const token = localStorage.getItem('cl_token'); const investisseurId = localStorage.getItem('cl_investisseur_id'); const exportParams = activeView === 'all' ? { annee, scope: 'all' } : { annee }; fetch(api.exportUrl('/taxreport/export', exportParams), { headers: { Authorization: `Bearer ${token}`, 'X-Investisseur-Id': investisseurId, }, }) .then(r => r.blob()) .then(blob => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `2778-SD-${annee}.csv`; a.click(); URL.revokeObjectURL(url); }); }; const downloadJson = () => { if (!data) return; const payload = { annee: data.annee, recap: data.recap, cases: data.cases, detail: data.detail, pertes: data.pertes, pertesTotales: data.pertesTotales, }; dlBlob(JSON.stringify(payload, null, 2), `2778-SD-${annee}.json`, 'application/json'); }; if (!loading && data && availableYears.length === 0) return ( <>

Fiscalité

{hasPlateformes ? ( ) : ( )} ); return ( <>

Fiscalité — Récapitulatif fiscal

{loading || !data ? (
Chargement…
) : ( <> {!detailExpanded && !cerfaExpanded && data2042 && (

Cases fiscales 2042 — synthèse {annee}

{data2042.case_2TT > 0 &&
Case 2TT — Prêts participatifs (FR)
{fmtEUR(data2042.case_2TT)}
} {data2042.case_2TR > 0 &&
Case 2TR — Revenus fixes (FR + étranger)
{fmtEUR(data2042.case_2TR)}
}
Case 2BH — Base CSG/CRDS
{fmtEUR(data2042.case_2BH)}
Case 2CK — Crédit d'impôt
{fmtEUR(data2042.case_2CK)}
{data2042.case_2TY > 0 &&
Case 2TY — Pertes en capital
{fmtEUR(data2042.case_2TY)}
}

⚠ Cases indicatives combinant plateformes françaises (IFU automatique) et étrangères. Référez-vous à la notice 2041-GFI.

{hasPsan && data?.cerfa2042cpro?.donnees2042cpro && (

Cases fiscales 2042 C PRO — synthèse {annee}

Case 5HQ — Micro-BNC (PSAN)
{fmtEUR(data.cerfa2042cpro.donnees2042cpro.case_5HQ)}

⚠ Montant brut ; l'abattement de 34 % (régime micro-BNC) est appliqué automatiquement par l'administration.

)}
)} {!detailExpanded && !cerfaExpanded && data2042 && (

À quoi correspond chaque CERFA ?

{hasEtranger && ( )} {hasEtranger && ( )} {hasPsan && ( )}
{hasEtranger && (
{pfoAssujetti ? ( <>Vous avez des plateformes étrangères et l'option de déclaration 2778-SD est activée. ) : ( <>Vous avez des plateformes étrangères. Vérifiez si la déclaration mensuelle 2778-SD s'applique à votre situation. )}
)}

⚠ Ce module est une aide à la préparation de votre déclaration ; il ne la remplace pas. Vérifiez chaque montant avant de le reporter. Vous restez seul responsable de l'exactitude de votre déclaration fiscale — Crowdlending ne saurait être tenu responsable des données déclarées.

)} {!detailExpanded && !cerfaExpanded && (
{hasPsan && } {hasEtranger && } {pfoAssujetti && }
)} )} {activeTab === '2042' && !detailExpanded && !cerfaExpanded && data && ( )} {(activeTab === 'cerfa' || cerfaExpanded) && !detailExpanded && data && ( { setCerfaExpanded(e => !e); setActiveTab('cerfa'); }} /> )} {activeTab === '2047' && hasEtranger && !detailExpanded && !cerfaExpanded && ( )} {activeTab === '2778' && pfoAssujetti && !detailExpanded && !cerfaExpanded && ( )} {activeTab === '2042cpro' && hasPsan && !detailExpanded && !cerfaExpanded && ( )} ); }