import { useEffect, useRef, useState } from 'react'; 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 Cerfa2042Preview from '../components/Cerfa2042Preview.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, #7c3aed 0%, #4f46e5 100%)', borderRadius: 10, padding: '12px 16px', boxShadow: open ? '0 6px 28px rgba(109,40,217,0.45)' : '0 4px 20px rgba(109,40,217,0.30)', 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 ? 'rgba(109,40,217,0.08)' : 'transparent', color: isActive ? '#7c3aed' : '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 && (
)}
); } /* ── Composant principal ─────────────────────────────────────── */ export default function TaxReport() { 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 /* ── Plateformes configurées ? (pour distinguer "pas de plateforme" de "pas encore de données") ── */ useEffect(() => { api.get('/plateformes').then(p => setHasPlateformes((p || []).length > 0)).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'); 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) + Math.round(platEtr.reduce((s, p) => s + etrBA(p), 0)), case_2CK: frLignes.reduce((s, l) => s + (l.case_2CK ?? 0), 0) + Math.round(platEtr.reduce((s, p) => s + etrBA(p), 0) * pfo), case_2TY: frLignes.reduce((s, l) => s + (l.case_2TY ?? 0), 0), }); }).finally(() => setLoading(false)); }; useEffect(load, [activeId, activeView, annee]); // 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.

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