import { useEffect, useState } from 'react'; import { api } from '../api.js'; import { useUi } from '../context/UiContext.jsx'; import { SuiviMensuelMatrix, Report2042Block, computeCases, getRatesForYear, MOIS_NUMS, r, } from './Cerfa2778Preview.jsx'; const fmtPct = (v, nonPlafonne) => v == null ? (nonPlafonne ? Non plafonné : —) : `${Number(v).toLocaleString('fr-FR', { minimumFractionDigits: 1, maximumFractionDigits: 1 })} %`; // Cases CERFA 2047 : montants arrondis à l'euro le plus proche (règle fiscale), sans décimales — // contrairement au Suivi mensuel (intermédiaire, non déclaré tel quel) qui garde 2 décimales. const fmt0 = v => { if (v == null || v === 0) return - €; return `${Number(v).toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} €`; }; /* ── Badge de case CERFA — même habillage que Case2042Row / Case2042 (Report 2042) ── */ function CaseBadge({ code, tone }) { const success = tone === 'success'; return ( {code} ); } function CaseHeader({ code, label, tone }) { return (
{label}
); } /* ── Composant principal ── */ export default function Cerfa2047Preview({ annee, activeView }) { const LS_KEY = 'cl_2778_excluded_plats'; // partagé avec le 2778-SD (mêmes plateformes étrangères) const { pfoAssujetti } = useUi(); const [data2778, setData2778] = useState(null); const [data2047, setData2047] = useState(null); const [loading, setLoading] = useState(false); const [pfuList, setPfuList] = useState([]); const [excluded, setExcluded] = useState(() => { try { return new Set(JSON.parse(localStorage.getItem(LS_KEY)) ?? []); } catch { return new Set(); } }); const [view, setView] = useState('matrice'); // 'matrice' | 'donnees' | 'report2042' useEffect(() => { setLoading(true); setData2778(null); setData2047(null); const scopeParams = activeView === 'all' ? { scope: 'all' } : {}; Promise.all([ api.get('/taxreport/2778', { annee, ...scopeParams }), api.get('/taxreport/2047', { annee, ...scopeParams }), pfuList.length === 0 ? api.get('/pfu') : Promise.resolve(null), ]).then(([d2778, d2047, pfu]) => { setData2778(d2778); setData2047(d2047); if (pfu) setPfuList(pfu); }).finally(() => setLoading(false)); }, [annee, activeView]); // eslint-disable-line if (loading || !data2778 || !data2047) return
Chargement…
; const plateformes = data2778.plateformes; const rates = getRatesForYear(annee, pfuList); const TOTAL_TAX = rates.pfo + rates.csg + rates.crds + rates.solidarite; if (plateformes.length === 0) { return (

Aucun revenu de plateforme étrangère pour {annee}.

); } const togglePlat = (id) => { setExcluded(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); localStorage.setItem(LS_KEY, JSON.stringify([...next])); return next; }); }; const totauxMois = MOIS_NUMS.map(m => { let sum = 0; for (const plat of plateformes) { if (!excluded.has(plat.id)) sum += plat.mois[m] ?? 0; } return r(sum); }); const totalBA = r(totauxMois.reduce((a, b) => a + b, 0)); const totalIA = computeCases(totalBA, rates).IA; // Lignes 2047 filtrées sur les plateformes non exclues (cohérence avec le Suivi mensuel) const lignes2047 = data2047.lignes.filter(l => !excluded.has(`${l.plateforme_id}_${l.investisseur_id}`)); const lignesEligibles = lignes2047.filter(l => l.eligible_ci); const lignesNonEligibles = lignes2047.filter(l => !l.eligible_ci); // Regroupement par pays pour faciliter le report dans la déclaration 2047 // (le formulaire officiel attend une ligne par pays, pas par plateforme) const groupesEligibles = Object.values( lignesEligibles.reduce((acc, l) => { const key = l.domiciliation || l.pays_nom || '?'; if (!acc[key]) acc[key] = { pays_nom: l.pays_nom, domiciliation: l.domiciliation, lignes: [] }; acc[key].lignes.push(l); return acc; }, {}) ).sort((a, b) => (a.pays_nom ?? '').localeCompare(b.pays_nom ?? '')); const t = lignes2047.reduce((acc, l) => ({ case_233: acc.case_233 + l.case_233, case_236: acc.case_236 + l.case_236, case_237: acc.case_237 + l.case_237, case_238: acc.case_238 + l.case_238, case_250: acc.case_250 + l.case_250, }), { case_233: 0, case_236: 0, case_237: 0, case_238: 0, case_250: 0 }); t.case_251 = r(t.case_238 + t.case_250); t.case_8VL = r(t.case_237); t.case_8PL = r(t.case_233); const platLabel = (l) => { const n = l.investisseur_nom ?? ''; const p = l.investisseur_prenom ?? ''; const who = p && n.startsWith(p) ? n : [p, n].filter(Boolean).join(' '); return ( <> {l.plateforme_nom} {who && — {who}} ); }; const cellStyle = { textAlign: 'right', padding: '7px 10px', fontSize: 'var(--fs-sm)', whiteSpace: 'nowrap', borderBottom: '1px solid var(--border)', }; const headStyle = { textAlign: 'center', padding: '7px 10px', fontSize: 'var(--fs-xs)', fontWeight: 600, color: 'var(--text-muted)', background: 'var(--surface-2)', borderBottom: '1px solid var(--border)', whiteSpace: 'nowrap', }; return (
{/* ── Vue Matrice (partagée avec le 2778-SD) ── */} {view === 'matrice' && ( )} {/* ── Vue Données 2047 ── */} {view === 'donnees' && (

Formulaire 2047 — Rubrique 230 (Intérêts)

Vision annuelle {annee} — plateformes étrangères
{/* Lignes 231-238 : éligibles au crédit d'impôt */}
Intérêts ouvrant droit à un crédit d'impôt égal à l'impôt payé à l'étranger
{lignesEligibles.length === 0 ? (
Aucune plateforme éligible pour {annee}.
) : ( {groupesEligibles.flatMap(g => { const sub = g.lignes.reduce((acc, l) => ({ case_233: acc.case_233 + l.case_233, case_235: acc.case_235 + l.case_235, case_236: acc.case_236 + l.case_236, case_237: acc.case_237 + l.case_237, case_238: acc.case_238 + l.case_238, }), { case_233: 0, case_235: 0, case_236: 0, case_237: 0, case_238: 0 }); const rows = g.lignes.map(l => ( )); if (g.lignes.length > 1) { rows.push( ); } return rows; })}
Plateforme — Détenteur Pays
{platLabel(l)} {l.pays_nom} {fmt0(l.case_233)} {fmtPct(l.case_234, !l.taux_plafonne)} {fmt0(l.case_235)} {fmt0(l.case_236)} {fmt0(l.case_237)} {fmt0(l.case_238)}
Sous-total {g.pays_nom} ({g.lignes.length} plateformes) {fmt0(sub.case_233)} {fmt0(sub.case_235)} {fmt0(sub.case_236)} {fmt0(sub.case_237)} {fmt0(sub.case_238)}
Total {fmt0(t.case_233)} {fmt0(t.case_236)} {fmt0(t.case_237)} {fmt0(t.case_238)}
)} {/* Ligne 250 : n'ouvrant pas droit à crédit d'impôt */}
Intérêts n'ouvrant pas droit à crédit d'impôt
{lignesNonEligibles.length === 0 ? (
Aucune plateforme dans ce cas pour {annee}.
) : ( {lignesNonEligibles.map(l => ( ))}
Plateforme — Détenteur Pays Motif
{platLabel(l)} {l.domiciliation} {l.motif} {fmt0(l.case_250)}
Total {fmt0(t.case_250)}
)} {/* Totaux 251 / 252 */}
Total des intérêts imposables (238 + 250)
dont intérêts et autres produits de placement à revenu fixe → reporté en case 2TR de la 2042 (voir onglet « Report 2042 »)
{fmt0(t.case_251)}
L'éligibilité au crédit d'impôt (231-238 vs 250) est déterminée automatiquement à partir du pays de domiciliation de chaque plateforme et du référentiel « 2047 — Taux crédit d'impôts » (Admin > Fiscalité). Le crédit d'impôt retenu (237) est plafonné au plus petit des deux montants : taux conventionnel × montant net (235), ou impôt réellement prélevé à l'étranger (236).
{/* Rubrique 9 : revenus déjà soumis aux prélèvements sociaux (2778-SD) */}
Rubrique 9 — Revenus déjà soumis aux prélèvements sociaux (2778-SD)
{pfoAssujetti ? (
) : (
Les cases 272 et 273 ne s'appliquent que si l'option de déclaration 2778-SD est activée (Paramétrage > Ma fiscalité).
)}
)} {/* ── Vue Report 2042 ── */} {view === 'report2042' && ( <>

Crédit d'impôt étranger (cadre 7 du 2047)

À reporter sur la 2042C
)}
); } /* ── Ligne de case CERFA 2047 → report 2042/2042C ── */ function Case2042Row({ code, label, note, value }) { return (
{code}
{label}
{note &&
{note}
}
{Number(value ?? 0).toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} €
); }