Amélioration de la fonctionnalité Fiscalité

This commit is contained in:
ocroguennec committed 2026-08-22 11:58:52 +02:00
1 parent 5437091884
commit 5dc6f21e61
7 files changed
+1206 -189

No files matched your search

+41 -3
View File
@@ -92,6 +92,10 @@ export default function Cerfa2042Preview({ annee, activeView, pfoAssujetti }) {
const totalTaxRate = rates.pfo + rates.csg + rates.crds + rates.solidarite;
/* ── Suivi mensuel combiné ── */
const totalPrelevFR = r2(frLignes.reduce((s, l) => s + r2(Object.values(l.mois ?? {}).reduce((ss, mo) => ss + (mo.prelev_sociaux ?? 0) + (mo.prelev_forfaitaire ?? 0), 0)), 0));
const totalPrelevEtr = r2(platEtr.reduce((s, p) => s + r2(Object.values(p.mois ?? {}).reduce((ss, v) => ss + v, 0)) * totalTaxRate, 0));
const totalDejaPreleve = r2(totalPrelevFR + (pfoAssujetti ? totalPrelevEtr : 0));
const totalAPrelever = pfoAssujetti ? 0 : totalPrelevEtr;
const matriceView = (
<div style={{ overflowX: 'auto' }}>
{loading && <div className="card text-muted">Chargement…</div>}
@@ -218,6 +222,40 @@ export default function Cerfa2042Preview({ annee, activeView, pfoAssujetti }) {
{(totalTaxRate * 100).toFixed(1)} %
</td>
</tr>
{platEtr.length > 0 && (
<>
{/* Montant des prélèvements déjà prélevés */}
<tr style={{ background: 'transparent' }}>
<td style={{ padding: '4px 12px', color: 'var(--text-muted)', fontSize: 'var(--fs-xs)' }}>
Montant des prélèvements déjà prélevés
</td>
{MOIS_NUMS.map(m => {
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 dejaPreleve = r2(prelevFR + (pfoAssujetti ? prelevEtr : 0));
return <td key={m} style={{ padding: '4px 8px', textAlign: 'right', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>{dejaPreleve > 0 ? fmtN(dejaPreleve) + ' €' : '—'}</td>;
})}
<td style={{ padding: '4px 12px', textAlign: 'right', fontSize: 'var(--fs-xs)', fontWeight: 600, color: 'var(--text-muted)' }}>
{fmtN(totalDejaPreleve)} €
</td>
</tr>
{/* Montant des prélèvements à prélever */}
<tr style={{ background: 'transparent' }}>
<td style={{ padding: '4px 12px', color: 'var(--text-muted)', fontSize: 'var(--fs-xs)' }}>
Montant des prélèvements à prélever
{pfoAssujetti && <span style={{ marginLeft: 6, fontStyle: 'italic' }}>(couvert par la 2778-SD)</span>}
</td>
{MOIS_NUMS.map(m => {
const prelevEtr = r2(platEtr.reduce((s, p) => s + (p.mois?.[m] ?? 0), 0) * totalTaxRate);
const aPrelever = pfoAssujetti ? 0 : prelevEtr;
return <td key={m} style={{ padding: '4px 8px', textAlign: 'right', fontSize: 'var(--fs-xs)', color: aPrelever > 0 ? 'var(--danger)' : 'var(--text-muted)' }}>{aPrelever > 0 ? fmtN(aPrelever) + ' €' : '—'}</td>;
})}
<td style={{ padding: '4px 12px', textAlign: 'right', fontSize: 'var(--fs-xs)', fontWeight: 600, color: totalAPrelever > 0 ? 'var(--danger)' : 'var(--text-muted)' }}>
{fmtN(totalAPrelever)} €
</td>
</tr>
</>
)}
{/* Total intérêt net */}
<tr style={{ background: 'rgba(22,163,74,0.06)', borderTop: '1px solid var(--border)' }}>
<td style={{ padding: '6px 12px', fontWeight: 700, color: 'var(--success)' }}>Total intérêt net</td>
@@ -284,7 +322,7 @@ export default function Cerfa2042Preview({ annee, activeView, pfoAssujetti }) {
]);
const total2BH = bd2BH.reduce((s, i) => s + i.val, 0);
// Breakdown 2CK : FR (PFNL retenu) + étrangères (acompte 2778-SD)
// Breakdown 2CK : FR (PFNL retenu) + étrangères (case 273 du 2047, alimentée par les acomptes 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 })),
@@ -313,13 +351,13 @@ export default function Cerfa2042Preview({ annee, activeView, pfoAssujetti }) {
<Case2042 code="2TT" label="Produits des prêts participatifs" note="Intérêts bruts — plateformes françaises (financement participatif, case KR)" value={total2TT || undefined} breakdown={bd2TT} />
<Case2042 code="2TR" label="Produits de placement à revenu fixe" note="Intérêts bruts — plateformes françaises (case AR) + étrangères (base imposable BA)" value={total2TR || undefined} breakdown={bd2TR} />
<Case2042 code="2BH" label="Produits pour lesquels les PS ont déjà été appliqués" note="Même montant que 2TT/2TR — neutralise la double imposition aux prélèvements sociaux" value={total2BH || undefined} breakdown={bd2BH} />
<Case2042 code="2CK" label="Crédit d'impôt — prélèvement forfaitaire déjà retenu" note={`FR : PFNL retenu à la source (12,8 %) · Étranger : acompte versé via 2778-SD (${(rates.pfo*100).toFixed(1)} %)`} value={total2CK || undefined} breakdown={bd2CK} />
<Case2042 code="2CK" label="Crédit d'impôt — prélèvement forfaitaire déjà retenu" note={`FR : PFNL retenu à la source (12,8 %) · Étranger : case 273 du 2047 (${(rates.pfo*100).toFixed(1)} %)`} value={total2CK || undefined} breakdown={bd2CK} />
<Case2042 code="2TY" label="Pertes en capital sur prêts participatifs" note="Capital non remboursé sur prêts en défaut — plateformes françaises" value={total2TY || undefined} breakdown={bd2TY} />
</div>
<div style={{ padding: '10px 14px', borderRadius: 8, background: 'rgba(99,102,241,0.05)', border: '1px solid rgba(99,102,241,0.15)', fontSize: 'var(--fs-xs)', lineHeight: 1.6, color: 'var(--text-muted)' }}>
<strong style={{ color: 'var(--text)', display: 'block', marginBottom: 4 }}>Synthèse fiscale</strong>
Les montants <BadgeTag badge={BADGE_AUTO} /> sont automatiquement reportés par la plateforme (IFU).
Les montants <BadgeTag badge={BADGE_DECL} /> nécessitent une déclaration mensuelle 2778-SD et un report manuel sur la 2042.
Les montants <BadgeTag badge={BADGE_DECL} /> nécessitent une déclaration annuelle 2047 et un report manuel sur la 2042.
</div>
</div>
);
@@ -0,0 +1,447 @@
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
? <span title="Convention active sans taux forfaitaire chiffré : crédit égal à l'impôt réellement prélevé, sans plafond" style={{ color: 'var(--text-muted)', fontStyle: 'italic' }}>Non plafonné</span>
: <span style={{ color: 'var(--text-muted)' }}>—</span>)
: `${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 <span style={{ color: 'var(--text-muted)' }}>- €</span>;
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 (
<span style={{
display: 'inline-block', fontFamily: 'monospace', fontWeight: 800, fontSize: 'var(--fs-xs)',
color: success ? 'var(--success)' : 'var(--primary)',
background: success ? 'rgba(16,185,129,0.12)' : 'rgba(99,102,241,0.07)',
borderRadius: 6, padding: '2px 6px', letterSpacing: '.04em', whiteSpace: 'nowrap',
}}>{code}</span>
);
}
function CaseHeader({ code, label, tone }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
<CaseBadge code={code} tone={tone} />
<span>{label}</span>
</div>
);
}
/* ── 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 <div className="card text-muted">Chargement…</div>;
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 (
<div className="card">
<p className="text-muted" style={{ margin: 0 }}>
Aucun revenu de plateforme étrangère pour {annee}.
</p>
</div>
);
}
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 (
<>
<span style={{ fontWeight: 500 }}>{l.plateforme_nom}</span>
{who && <span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', marginLeft: 6 }}>— {who}</span>}
</>
);
};
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 (
<div className="card">
<div style={{ display: 'flex', gap: 0, alignItems: 'flex-start' }}>
<nav style={{
width: 200, flexShrink: 0, marginRight: 20,
background: 'var(--surface)', border: '1px solid var(--border)',
borderRadius: 10, padding: '8px 0', boxSizing: 'border-box',
}}>
<div style={{ padding: '6px 12px 8px', fontSize: 'var(--fs-xs)', fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '.06em' }}>
Sections
</div>
<button
className={`account-nav-item${view === 'matrice' ? ' active' : ''}`}
onClick={() => setView('matrice')}
>
<svg width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='1.8' strokeLinecap='round' strokeLinejoin='round'>
<path d='M3 3h18v2H3V3zm0 7h12v2H3v-2zm0 7h18v2H3v-2z'/>
</svg>
Suivi mensuel
</button>
<button
className={`account-nav-item${view === 'donnees' ? ' active' : ''}`}
onClick={() => setView('donnees')}
>
<svg width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='1.8' strokeLinecap='round' strokeLinejoin='round'>
<path d='M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z'/>
</svg>
Données 2047
</button>
<button
className={`account-nav-item${view === 'report2042' ? ' active' : ''}`}
onClick={() => setView('report2042')}
>
<svg width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='1.8' strokeLinecap='round' strokeLinejoin='round'>
<path d='M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z'/>
</svg>
Report 2042
</button>
<div style={{ margin: '10px 12px 6px', borderTop: '1px solid var(--border)' }} />
<div style={{ padding: '4px 12px', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', lineHeight: 1.5 }}>
Plateformes non-françaises<br/>Cadre 2, rubrique 230 — Intérêts
</div>
</nav>
<div style={{ flex: 1, minWidth: 0 }}>
{/* ── Vue Matrice (partagée avec le 2778-SD) ── */}
{view === 'matrice' && (
<SuiviMensuelMatrix
annee={annee}
plateformes={plateformes}
excluded={excluded}
togglePlat={togglePlat}
totauxMois={totauxMois}
rates={rates}
TOTAL_TAX={TOTAL_TAX}
/>
)}
{/* ── Vue Données 2047 ── */}
{view === 'donnees' && (
<div className="card" style={{ padding: 0, overflow: 'auto' }}>
<div style={{ padding: '14px 14px 10px' }}>
<h4 style={{ margin: 0, fontWeight: 700 }}>Formulaire 2047 — Rubrique 230 (Intérêts)</h4>
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>Vision annuelle {annee} — plateformes étrangères</span>
</div>
{/* Lignes 231-238 : éligibles au crédit d'impôt */}
<div style={{ padding: '0 14px 6px' }}>
<div style={{ fontSize: 'var(--fs-sm)', fontWeight: 600, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 8 }}>
<CaseBadge code="231-238" /> Intérêts ouvrant droit à un crédit d'impôt égal à l'impôt payé à l'étranger
</div>
</div>
{lignesEligibles.length === 0 ? (
<div style={{ padding: '4px 14px 14px', color: 'var(--text-muted)', fontSize: 'var(--fs-sm)' }}>Aucune plateforme éligible pour {annee}.</div>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 900 }}>
<thead>
<tr>
<th style={{ ...headStyle, textAlign: 'left' }}>Plateforme — Détenteur</th>
<th style={headStyle}>Pays</th>
<th style={headStyle}><CaseHeader code="233" label="Montant net encaissé" /></th>
<th style={headStyle}><CaseHeader code="234" label="Taux applicable" /></th>
<th style={headStyle}><CaseHeader code="235" label="Résultat" /></th>
<th style={headStyle}><CaseHeader code="236" label="Impôt supporté à l'étranger" /></th>
<th style={{ ...headStyle, background: 'rgba(16,185,129,0.10)' }}><CaseHeader code="237" label="Crédit d'impôt retenu" tone="success" /></th>
<th style={headStyle}><CaseHeader code="238" label="Intérêts crédit d'impôt inclus" /></th>
</tr>
</thead>
<tbody>
{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 => (
<tr key={`${l.plateforme_id}_${l.investisseur_id}`}>
<td style={{ ...cellStyle, textAlign: 'left' }}>{platLabel(l)}</td>
<td style={{ ...cellStyle, textAlign: 'center' }}>{l.pays_nom}</td>
<td style={cellStyle}>{fmt0(l.case_233)}</td>
<td style={{ ...cellStyle, textAlign: 'center' }}>{fmtPct(l.case_234, !l.taux_plafonne)}</td>
<td style={cellStyle}>{fmt0(l.case_235)}</td>
<td style={cellStyle}>{fmt0(l.case_236)}</td>
<td style={{ ...cellStyle, fontWeight: 600, color: 'var(--success)', background: 'rgba(16,185,129,0.05)' }}>{fmt0(l.case_237)}</td>
<td style={{ ...cellStyle, fontWeight: 600 }}>{fmt0(l.case_238)}</td>
</tr>
));
if (g.lignes.length > 1) {
rows.push(
<tr key={`sous-total-${g.domiciliation}`} style={{ background: 'rgba(99,102,241,0.06)' }}>
<td style={{ ...cellStyle, textAlign: 'left', fontStyle: 'italic', color: 'var(--text-muted)' }} colSpan={2}>
Sous-total {g.pays_nom} ({g.lignes.length} plateformes)
</td>
<td style={{ ...cellStyle, fontWeight: 700 }}>{fmt0(sub.case_233)}</td>
<td style={cellStyle}></td>
<td style={{ ...cellStyle, fontWeight: 700 }}>{fmt0(sub.case_235)}</td>
<td style={{ ...cellStyle, fontWeight: 700 }}>{fmt0(sub.case_236)}</td>
<td style={{ ...cellStyle, fontWeight: 700, color: 'var(--success)' }}>{fmt0(sub.case_237)}</td>
<td style={{ ...cellStyle, fontWeight: 700 }}>{fmt0(sub.case_238)}</td>
</tr>
);
}
return rows;
})}
<tr style={{ background: 'var(--surface-2)' }}>
<td style={{ ...cellStyle, textAlign: 'left', fontWeight: 700 }} colSpan={2}>Total</td>
<td style={{ ...cellStyle, fontWeight: 700 }}>{fmt0(t.case_233)}</td>
<td style={cellStyle}></td>
<td style={cellStyle}></td>
<td style={{ ...cellStyle, fontWeight: 700 }}>{fmt0(t.case_236)}</td>
<td style={{ ...cellStyle, fontWeight: 700, color: 'var(--success)' }}>{fmt0(t.case_237)}</td>
<td style={{ ...cellStyle, fontWeight: 700 }}>{fmt0(t.case_238)}</td>
</tr>
</tbody>
</table>
)}
{/* Ligne 250 : n'ouvrant pas droit à crédit d'impôt */}
<div style={{ padding: '18px 14px 6px', borderTop: '1px solid var(--border)' }}>
<div style={{ fontSize: 'var(--fs-sm)', fontWeight: 600, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 8 }}>
<CaseBadge code="250" /> Intérêts n'ouvrant pas droit à crédit d'impôt
</div>
</div>
{lignesNonEligibles.length === 0 ? (
<div style={{ padding: '4px 14px 14px', color: 'var(--text-muted)', fontSize: 'var(--fs-sm)' }}>Aucune plateforme dans ce cas pour {annee}.</div>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 900 }}>
<thead>
<tr>
<th style={{ ...headStyle, textAlign: 'left' }}>Plateforme — Détenteur</th>
<th style={headStyle}>Pays</th>
<th style={{ ...headStyle, textAlign: 'left' }}>Motif</th>
<th style={headStyle}><CaseHeader code="250" label="Montant" /></th>
</tr>
</thead>
<tbody>
{lignesNonEligibles.map(l => (
<tr key={`${l.plateforme_id}_${l.investisseur_id}`}>
<td style={{ ...cellStyle, textAlign: 'left' }}>{platLabel(l)}</td>
<td style={{ ...cellStyle, textAlign: 'center' }}>{l.domiciliation}</td>
<td style={{ ...cellStyle, textAlign: 'left', color: 'var(--text-muted)', fontSize: 'var(--fs-xs)' }}>{l.motif}</td>
<td style={cellStyle}>{fmt0(l.case_250)}</td>
</tr>
))}
<tr style={{ background: 'var(--surface-2)' }}>
<td style={{ ...cellStyle, textAlign: 'left', fontWeight: 700 }} colSpan={3}>Total</td>
<td style={{ ...cellStyle, fontWeight: 700 }}>{fmt0(t.case_250)}</td>
</tr>
</tbody>
</table>
)}
{/* Totaux 251 / 252 */}
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '12px 14px', marginTop: 4, borderTop: '2px solid var(--border)',
background: 'rgba(99,102,241,0.05)',
}}>
<div>
<div style={{ fontWeight: 700, fontSize: 'var(--fs-sm)', display: 'flex', alignItems: 'center', gap: 8 }}>
<CaseBadge code="251" /> Total des intérêts imposables (238 + 250)
</div>
<div style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', marginTop: 4, display: 'flex', alignItems: 'center', gap: 8 }}>
<CaseBadge code="252" /> dont intérêts et autres produits de placement à revenu fixe → reporté en case <strong>2TR</strong> de la 2042 (voir onglet « Report 2042 »)
</div>
</div>
<div style={{ fontWeight: 800, fontSize: '1.2rem' }}>{fmt0(t.case_251)}</div>
</div>
<div style={{ padding: '10px 14px 14px', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', lineHeight: 1.6 }}>
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 <strong>« 2047 — Taux crédit d'impôts »</strong> (Admin&nbsp;&gt;&nbsp;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).
</div>
{/* Rubrique 9 : revenus déjà soumis aux prélèvements sociaux (2778-SD) */}
<div style={{ padding: '18px 14px 6px', borderTop: '1px solid var(--border)' }}>
<div style={{ fontSize: 'var(--fs-sm)', fontWeight: 600, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 8 }}>
<CaseBadge code="272-273" /> Rubrique 9 — Revenus déjà soumis aux prélèvements sociaux (2778-SD)
</div>
</div>
{pfoAssujetti ? (
<div style={{ padding: '0 14px 14px' }}>
<div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden' }}>
<Case2042Row
code="272"
label="Revenus déjà soumis aux prélèvements sociaux (avec CSG déductible si option barème)"
note="Même montant que la case 2BH — intérêts bruts totaux avant retenue locale, déclarés via vos 2778-SD"
value={totalBA}
/>
<Case2042Row
code="273"
label="Prélèvement forfaitaire non libératoire déjà versé"
note="Même montant que la case 2CK — somme des acomptes PFO (case IA) versés via vos 2778-SD"
value={totalIA}
/>
</div>
</div>
) : (
<div style={{
margin: '0 14px 14px', padding: '10px 14px', borderRadius: 8,
background: 'var(--surface-2)', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', lineHeight: 1.6,
}}>
Les cases 272 et 273 ne s'appliquent que si l'option de déclaration 2778-SD est activée
(Paramétrage&nbsp;&gt;&nbsp;Ma fiscalité).
</div>
)}
</div>
)}
{/* ── Vue Report 2042 ── */}
{view === 'report2042' && (
<>
<Report2042Block annee={annee} totauxMois={totauxMois} rates={rates} plateformes={plateformes} excluded={excluded} source="2047" />
<div className="card" style={{ marginTop: 12 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 14 }}>
<h4 style={{ margin: 0, fontWeight: 700 }}>Crédit d'impôt étranger (cadre 7 du 2047)</h4>
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>À reporter sur la 2042C</span>
</div>
<div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden' }}>
<Case2042Row
code="8VL"
label="Crédit d'impôt sur revenus de valeurs mobilières et assimilés"
note="Somme des crédits d'impôt retenus (case 237) — plateformes dont le pays ouvre droit à un crédit d'impôt conventionnel"
value={t.case_8VL}
/>
<Case2042Row
code="8PL"
label="Revenus nets ouvrant droit à un crédit d'impôt étranger"
note="Somme des montants nets encaissés (case 233) sur ces mêmes plateformes"
value={t.case_8PL}
/>
</div>
</div>
</>
)}
</div>
</div>
</div>
);
}
/* ── Ligne de case CERFA 2047 → report 2042/2042C ── */
function Case2042Row({ code, label, note, value }) {
return (
<div style={{ borderBottom: '1px solid var(--border)' }}>
<div style={{
display: 'grid', gridTemplateColumns: '90px 1fr auto',
gap: '0 12px', alignItems: 'center', padding: '10px 14px',
}}>
<div style={{
fontFamily: 'monospace', fontWeight: 800, fontSize: 'var(--fs-base)',
color: 'var(--primary)', background: 'rgba(99,102,241,0.07)',
borderRadius: 6, padding: '2px 6px', textAlign: 'center', letterSpacing: '.04em',
}}>{code}</div>
<div>
<div style={{ fontSize: 'var(--fs-sm)', fontWeight: 600 }}>{label}</div>
{note && <div style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', marginTop: 2 }}>{note}</div>}
</div>
<div style={{ fontWeight: 800, fontSize: 'var(--fs-lg)', whiteSpace: 'nowrap', color: 'var(--text)' }}>
{Number(value ?? 0).toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} €
</div>
</div>
</div>
);
}
+235 -181
View File
@@ -1,13 +1,14 @@
import { useEffect, useState } from 'react';
import { api } from '../api.js';
import { useUi } from '../context/UiContext.jsx';
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'];
export const MOIS_LABELS = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'];
export const MOIS_NUMS = ['01','02','03','04','05','06','07','08','09','10','11','12'];
// Taux par défaut (2026+) — remplacés par les données de /api/pfu
const DEFAULT_RATES = { pfo: 0.128, csg: 0.106, crds: 0.005, solidarite: 0.075 };
function getRatesForYear(annee, pfuList) {
export function getRatesForYear(annee, pfuList) {
const yr = Number(annee);
// Chercher l'année exacte, puis l'année précédente la plus proche
const sorted = [...pfuList].sort((a, b) => b.annee - a.annee);
@@ -21,18 +22,18 @@ function getRatesForYear(annee, pfuList) {
};
}
const r = v => Math.round((v ?? 0) * 100) / 100;
const fmtEUR = v => {
export const r = v => Math.round((v ?? 0) * 100) / 100;
export const fmtEUR = v => {
if (v == null || v === 0) return <span style={{ color: 'var(--text-muted)' }}>- €</span>;
return `${Number(v).toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`;
};
const fmtInt = v => {
export const fmtInt = v => {
if (v == null || v === 0) return <span style={{ color: 'var(--text-muted)' }}>-</span>;
return `${v} €`;
};
/* ── Simulation cases CERFA pour un montant brut ── */
function computeCases(ba, rates) {
export function computeCases(ba, rates) {
const R = rates ?? DEFAULT_RATES;
const BA = Math.round(ba);
const IA = Math.round(BA * R.pfo);
@@ -137,24 +138,6 @@ export default function Cerfa2778Preview({ annee, activeView }) {
});
};
const cellStyle = {
textAlign: 'right',
padding: '6px 10px',
fontSize: 'var(--fs-sm)',
whiteSpace: 'nowrap',
borderBottom: '1px solid var(--border)',
};
const headStyle = {
textAlign: 'center',
padding: '6px 10px',
fontSize: 'var(--fs-xs)',
fontWeight: 600,
color: 'var(--text-muted)',
background: 'var(--surface-2)',
borderBottom: '1px solid var(--border)',
whiteSpace: 'nowrap',
};
return (
<div className="card">
{/* ── Navigation sidebar ── */}
@@ -202,135 +185,15 @@ export default function Cerfa2778Preview({ annee, activeView }) {
<div style={{ flex: 1, minWidth: 0 }}>
{/* ── Vue Matrice ── */}
{view === 'matrice' && (
<>
<div className="card" style={{ padding: 0, overflow: 'auto' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, padding: '14px 14px 10px' }}>
<h4 style={{ margin: 0, fontWeight: 700 }}>Suivi mensuel des intérêts bruts</h4>
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>Revenus {annee} — plateformes étrangères</span>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 900 }}>
<thead>
<tr>
<th style={{ ...headStyle, textAlign: 'left', minWidth: 180, position: 'sticky', left: 0, background: 'var(--surface-2)' }}>
Plateforme — Détenteur
</th>
{MOIS_LABELS.map((m, i) => (
<th key={m} style={headStyle}>{m}</th>
))}
<th style={{ ...headStyle, background: 'var(--surface-2)' }}>Total</th>
</tr>
</thead>
<tbody>
{plateformes.map(plat => {
const isExcluded = excluded.has(plat.id);
const total = Object.values(plat.mois).reduce((a, b) => a + b, 0);
return (
<tr key={plat.id} style={{ opacity: isExcluded ? 0.4 : 1, transition: 'opacity .15s' }}>
<td style={{
...cellStyle, textAlign: 'left',
position: 'sticky', left: 0,
background: 'var(--surface)',
fontWeight: 500,
}}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', userSelect: 'none', fontSize: 'var(--fs-sm)', textTransform: 'none', letterSpacing: 'normal', color: 'var(--text)', fontWeight: 400, marginBottom: 0 }}>
<input
type="checkbox"
checked={!isExcluded}
onChange={() => togglePlat(plat.id)}
style={{ accentColor: 'var(--primary)', width: 14, height: 14, flexShrink: 0 }}
/>
<span style={{ fontWeight: 500 }}>
{plat.nom}
{(plat.investisseur_nom || plat.investisseur_prenom) && (
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', marginLeft: 6, fontWeight: 400 }}>
— {(() => { const n = plat.investisseur_nom ?? ''; const p = plat.investisseur_prenom ?? ''; return p && n.startsWith(p) ? n : [p, n].filter(Boolean).join(' '); })()}
</span>
)}
</span>
</label>
</td>
{MOIS_NUMS.map(m => (
<td key={m} style={cellStyle}>{fmtEUR(plat.mois[m] ?? null)}</td>
))}
<td style={{ ...cellStyle, fontWeight: 600 }}>{fmtEUR(r(total))}</td>
</tr>
);
})}
{/* ── Séparateur ── */}
<tr><td colSpan={14} style={{ height: 4, background: 'var(--surface-2)' }} /></tr>
{/* ── Total brut mensuel ── */}
<tr style={{ background: 'var(--surface-2)' }}>
<td style={{ ...cellStyle, textAlign: 'left', fontWeight: 600, position: 'sticky', left: 0, background: 'var(--surface-2)' }}>
Total intérêt brut
</td>
{totauxMois.map((t, i) => (
<td key={i} style={{ ...cellStyle, fontWeight: 600 }}>{fmtEUR(t || null)}</td>
))}
<td style={{ ...cellStyle, fontWeight: 700 }}>
{fmtEUR(r(totauxMois.reduce((a, b) => a + b, 0)))}
</td>
</tr>
{/* ── Taux Flat Tax ── */}
<tr>
<td style={{ ...cellStyle, textAlign: 'left', color: 'var(--text-muted)', position: 'sticky', left: 0, background: 'var(--surface)' }}>
Taux total prélevé ({(TOTAL_TAX * 100).toFixed(1)} %)
</td>
{MOIS_NUMS.map((_, i) => (
<td key={i} style={{ ...cellStyle, color: 'var(--text-muted)' }}>
{totauxMois[i] > 0 ? `${(TOTAL_TAX * 100).toFixed(2).replace('.', ',')} %` : '—'}
</td>
))}
<td style={{ ...cellStyle, color: 'var(--text-muted)' }}>{`${(TOTAL_TAX * 100).toFixed(2).replace('.', ',')} %`}</td>
</tr>
{/* ── Total Flat Tax ── */}
<tr style={{ background: 'rgba(239,68,68,0.06)' }}>
<td style={{ ...cellStyle, textAlign: 'left', fontWeight: 600, color: 'var(--danger)', position: 'sticky', left: 0, background: 'rgba(239,68,68,0.06)' }}>
Montant total à payer (QR)
</td>
{totauxMois.map((t, i) => {
const { QR } = computeCases(t, rates);
return (
<td key={i} style={{ ...cellStyle, fontWeight: 600, color: t > 0 ? 'var(--danger)' : undefined }}>
{t > 0 ? fmtInt(QR) : <span style={{ color: 'var(--text-muted)' }}>-</span>}
</td>
);
})}
<td style={{ ...cellStyle, fontWeight: 700, color: 'var(--danger)' }}>
{fmtInt(computeCases(r(totauxMois.reduce((a, b) => a + b, 0)), rates).QR)}
</td>
</tr>
{/* ── Intérêt net ── */}
<tr style={{ background: 'rgba(16,185,129,0.05)' }}>
<td style={{ ...cellStyle, textAlign: 'left', fontWeight: 600, color: 'var(--success)', position: 'sticky', left: 0, background: 'rgba(16,185,129,0.05)' }}>
Total intérêt net
</td>
{totauxMois.map((t, i) => (
<td key={i} style={{ ...cellStyle, fontWeight: 600, color: t > 0 ? 'var(--success)' : undefined }}>
{fmtEUR(t > 0 ? r(t * (1 - TOTAL_TAX)) : null)}
</td>
))}
<td style={{ ...cellStyle, fontWeight: 700, color: 'var(--success)' }}>
{fmtEUR(r(totauxMois.reduce((a, b) => a + b, 0) * (1 - TOTAL_TAX)))}
</td>
</tr>
</tbody>
</table>
<div style={{ padding: '10px 16px', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', borderTop: '1px solid var(--border)', display: 'flex', gap: 24, flexWrap: 'wrap' }}>
<span>Taux {annee} : PFO {(rates.pfo*100).toFixed(1)} % + CSG {(rates.csg*100).toFixed(1)} % + CRDS {(rates.crds*100).toFixed(1)} % + Solidarité {(rates.solidarite*100).toFixed(1)} % = {(TOTAL_TAX*100).toFixed(1)} %</span>
<span>·</span>
<span>⏱ Déclaration et paiement dus dans les <strong>15 premiers jours du mois suivant</strong> l'encaissement</span>
<span>·</span>
<span>Base imposable : intérêts bruts <em>après</em> déduction de l'impôt prélevé à la source à l'étranger, <em>avant</em> déduction retenue "directive épargne"</span>
</div>
</div>
</>
<SuiviMensuelMatrix
annee={annee}
plateformes={plateformes}
excluded={excluded}
togglePlat={togglePlat}
totauxMois={totauxMois}
rates={rates}
TOTAL_TAX={TOTAL_TAX}
/>
)}
{/* ── Vue Report 2042 ── */}
@@ -421,12 +284,180 @@ export default function Cerfa2778Preview({ annee, activeView }) {
);
}
/* ── Suivi mensuel (matrice) — partagé entre 2778-SD et 2047 ── */
export function SuiviMensuelMatrix({ annee, plateformes, excluded, togglePlat, totauxMois, rates, TOTAL_TAX }) {
const { pfoAssujetti } = useUi();
const cellStyle = {
textAlign: 'right',
padding: '6px 10px',
fontSize: 'var(--fs-sm)',
whiteSpace: 'nowrap',
borderBottom: '1px solid var(--border)',
};
const headStyle = {
textAlign: 'center',
padding: '6px 10px',
fontSize: 'var(--fs-xs)',
fontWeight: 600,
color: 'var(--text-muted)',
background: 'var(--surface-2)',
borderBottom: '1px solid var(--border)',
whiteSpace: 'nowrap',
};
return (
<div className="card" style={{ padding: 0, overflow: 'auto' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, padding: '14px 14px 10px' }}>
<h4 style={{ margin: 0, fontWeight: 700 }}>Suivi mensuel des intérêts bruts</h4>
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>Revenus {annee} — plateformes étrangères</span>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 900 }}>
<thead>
<tr>
<th style={{ ...headStyle, textAlign: 'left', minWidth: 180, position: 'sticky', left: 0, background: 'var(--surface-2)' }}>
Plateforme — Détenteur
</th>
{MOIS_LABELS.map((m, i) => (
<th key={m} style={headStyle}>{m}</th>
))}
<th style={{ ...headStyle, background: 'var(--surface-2)' }}>Total</th>
</tr>
</thead>
<tbody>
{plateformes.map(plat => {
const isExcluded = excluded.has(plat.id);
const total = Object.values(plat.mois).reduce((a, b) => a + b, 0);
return (
<tr key={plat.id} style={{ opacity: isExcluded ? 0.4 : 1, transition: 'opacity .15s' }}>
<td style={{
...cellStyle, textAlign: 'left',
position: 'sticky', left: 0,
background: 'var(--surface)',
fontWeight: 500,
}}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', userSelect: 'none', fontSize: 'var(--fs-sm)', textTransform: 'none', letterSpacing: 'normal', color: 'var(--text)', fontWeight: 400, marginBottom: 0 }}>
<input
type="checkbox"
checked={!isExcluded}
onChange={() => togglePlat(plat.id)}
style={{ accentColor: 'var(--primary)', width: 14, height: 14, flexShrink: 0 }}
/>
<span style={{ fontWeight: 500 }}>
{plat.nom}
{(plat.investisseur_nom || plat.investisseur_prenom) && (
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', marginLeft: 6, fontWeight: 400 }}>
— {(() => { const n = plat.investisseur_nom ?? ''; const p = plat.investisseur_prenom ?? ''; return p && n.startsWith(p) ? n : [p, n].filter(Boolean).join(' '); })()}
</span>
)}
</span>
</label>
</td>
{MOIS_NUMS.map(m => (
<td key={m} style={cellStyle}>{fmtEUR(plat.mois[m] ?? null)}</td>
))}
<td style={{ ...cellStyle, fontWeight: 600 }}>{fmtEUR(r(total))}</td>
</tr>
);
})}
{/* ── Séparateur ── */}
<tr><td colSpan={14} style={{ height: 4, background: 'var(--surface-2)' }} /></tr>
{/* ── Total brut mensuel ── */}
<tr style={{ background: 'var(--surface-2)' }}>
<td style={{ ...cellStyle, textAlign: 'left', fontWeight: 600, position: 'sticky', left: 0, background: 'var(--surface-2)' }}>
Total intérêt brut
</td>
{totauxMois.map((t, i) => (
<td key={i} style={{ ...cellStyle, fontWeight: 600 }}>{fmtEUR(t || null)}</td>
))}
<td style={{ ...cellStyle, fontWeight: 700 }}>
{fmtEUR(r(totauxMois.reduce((a, b) => a + b, 0)))}
</td>
</tr>
{pfoAssujetti && (
<>
{/* ── Taux Flat Tax ── */}
<tr>
<td style={{ ...cellStyle, textAlign: 'left', color: 'var(--text-muted)', position: 'sticky', left: 0, background: 'var(--surface)' }}>
Taux total prélevé ({(TOTAL_TAX * 100).toFixed(1)} %)
</td>
{MOIS_NUMS.map((_, i) => (
<td key={i} style={{ ...cellStyle, color: 'var(--text-muted)' }}>
{totauxMois[i] > 0 ? `${(TOTAL_TAX * 100).toFixed(2).replace('.', ',')} %` : '—'}
</td>
))}
<td style={{ ...cellStyle, color: 'var(--text-muted)' }}>{`${(TOTAL_TAX * 100).toFixed(2).replace('.', ',')} %`}</td>
</tr>
{/* ── Total Flat Tax ── */}
<tr style={{ background: 'rgba(239,68,68,0.06)' }}>
<td style={{ ...cellStyle, textAlign: 'left', fontWeight: 600, color: 'var(--danger)', position: 'sticky', left: 0, background: 'rgba(239,68,68,0.06)' }}>
Montant total à payer (QR)
</td>
{totauxMois.map((t, i) => {
const { QR } = computeCases(t, rates);
return (
<td key={i} style={{ ...cellStyle, fontWeight: 600, color: t > 0 ? 'var(--danger)' : undefined }}>
{t > 0 ? fmtInt(QR) : <span style={{ color: 'var(--text-muted)' }}>-</span>}
</td>
);
})}
<td style={{ ...cellStyle, fontWeight: 700, color: 'var(--danger)' }}>
{fmtInt(computeCases(r(totauxMois.reduce((a, b) => a + b, 0)), rates).QR)}
</td>
</tr>
{/* ── Intérêt net ── */}
<tr style={{ background: 'rgba(16,185,129,0.05)' }}>
<td style={{ ...cellStyle, textAlign: 'left', fontWeight: 600, color: 'var(--success)', position: 'sticky', left: 0, background: 'rgba(16,185,129,0.05)' }}>
Total intérêt net
</td>
{totauxMois.map((t, i) => (
<td key={i} style={{ ...cellStyle, fontWeight: 600, color: t > 0 ? 'var(--success)' : undefined }}>
{fmtEUR(t > 0 ? r(t * (1 - TOTAL_TAX)) : null)}
</td>
))}
<td style={{ ...cellStyle, fontWeight: 700, color: 'var(--success)' }}>
{fmtEUR(r(totauxMois.reduce((a, b) => a + b, 0) * (1 - TOTAL_TAX)))}
</td>
</tr>
</>
)}
</tbody>
</table>
<div style={{ padding: '10px 16px', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', borderTop: '1px solid var(--border)', display: 'flex', gap: 24, flexWrap: 'wrap' }}>
{pfoAssujetti && (
<>
<span>Taux {annee} : PFO {(rates.pfo*100).toFixed(1)} % + CSG {(rates.csg*100).toFixed(1)} % + CRDS {(rates.crds*100).toFixed(1)} % + Solidarité {(rates.solidarite*100).toFixed(1)} % = {(TOTAL_TAX*100).toFixed(1)} %</span>
<span>·</span>
<span>⏱ Déclaration et paiement dus dans les <strong>15 premiers jours du mois suivant</strong> l'encaissement</span>
<span>·</span>
</>
)}
<span>Base imposable : intérêts bruts <em>après</em> déduction de l'impôt prélevé à la source à l'étranger, <em>avant</em> déduction retenue "directive épargne"</span>
</div>
</div>
);
}
/* ── Report annuel 2042 ── */
function Report2042Block({ annee, totauxMois, rates, plateformes, excluded }) {
export function Report2042Block({ annee, totauxMois, rates, plateformes, excluded, source = '2778' }) {
const totalBA = r(totauxMois.reduce((a, b) => a + b, 0));
if (totalBA === 0) return null;
const totalIA = computeCases(totalBA, rates).IA;
const { pfoAssujetti } = useUi();
const isFrom2047 = source === '2047';
const noteBA = isFrom2047
? "Intérêts bruts totaux encaissés sur l'année (cases 233 + 250 des Données 2047)"
: "Intérêts bruts totaux encaissés sur l'année (somme des cases BA)";
const noteIA = isFrom2047
? "Correspond à la case 273 des Données 2047 — s'impute sur l'IR définitif, l'excédent est restitué"
: "Somme des cases IA de vos 2778-SD — s'impute sur l'IR définitif, l'excédent est restitué";
// Détail par plateforme incluse (annuel)
const platDetail = plateformes
@@ -482,37 +513,60 @@ function Report2042Block({ annee, totauxMois, rates, plateformes, excluded }) {
<Case2042
code="2TR"
label="Produits de placement à revenu fixe de source étrangère"
note="Intérêts bruts totaux encaissés sur l'année (somme des cases BA)"
note={noteBA}
value={Math.round(totalBA)}
breakdown={platDetail.map(p => ({ nom: p.nom, val: Math.round(p.ba) }))}
/>
<Case2042
code="2BH"
label="Produits soumis aux prélèvements sociaux (idem 2TR)"
note="Même montant que 2TR — évite la double imposition aux prélèvements sociaux"
value={Math.round(totalBA)}
breakdown={platDetail.map(p => ({ nom: p.nom, val: Math.round(p.ba) }))}
/>
<Case2042
code="2CK"
label="Prélèvement forfaitaire non libératoire déjà versé (acompte)"
note="Somme des cases IA de vos 2778-SD — s'impute sur l'IR définitif, l'excédent est restitué"
value={totalIA}
breakdown={platDetail.map(p => ({ nom: p.nom, val: p.ia }))}
/>
{pfoAssujetti && (
<>
<Case2042
code="2BH"
label="Produits soumis aux prélèvements sociaux (idem 2TR)"
note="Même montant que 2TR — évite la double imposition aux prélèvements sociaux"
value={Math.round(totalBA)}
breakdown={platDetail.map(p => ({ nom: p.nom, val: Math.round(p.ba) }))}
/>
<Case2042
code="2CK"
label="Prélèvement forfaitaire non libératoire déjà versé (acompte)"
note={noteIA}
value={totalIA}
breakdown={platDetail.map(p => ({ nom: p.nom, val: p.ia }))}
/>
</>
)}
</div>
<div style={{
padding: '10px 14px', borderRadius: 8,
background: 'rgba(99,102,241,0.05)', border: '1px solid rgba(99,102,241,0.15)',
fontSize: 'var(--fs-xs)', lineHeight: 1.6, color: 'var(--text-muted)',
}}>
<strong style={{ color: 'var(--text)', display: 'block', marginBottom: 4 }}>Comment fonctionne le PFO ?</strong>
Le prélèvement forfaitaire obligatoire (PFO, case 2CK) versé via la 2778-SD est un <em>acompte</em> sur l'impôt sur le revenu, non libératoire.
L'imposition définitive est calculée lors de votre 2042 : par défaut au taux de <strong>{(rates.pfo * 100).toFixed(1)} %</strong> (Flat Tax),
ou sur option expresse au barème progressif. L'acompte déjà versé (2CK) s'impute sur l'IR définitif — tout excédent vous est restitué.
Le montant 2BH est déclaré en sus de 2TR uniquement pour neutraliser les prélèvements sociaux déjà prélevés via la 2778-SD.
</div>
{pfoAssujetti ? (
<div style={{
padding: '10px 14px', borderRadius: 8,
background: 'rgba(99,102,241,0.05)', border: '1px solid rgba(99,102,241,0.15)',
fontSize: 'var(--fs-xs)', lineHeight: 1.6, color: 'var(--text-muted)',
}}>
<strong style={{ color: 'var(--text)', display: 'block', marginBottom: 4 }}>Comment fonctionne le PFO ?</strong>
{isFrom2047 ? (
<>Le prélèvement forfaitaire obligatoire (PFO, case 2CK) correspond à la case <strong>273</strong> des Données 2047 : c'est un <em>acompte</em> sur l'impôt sur le revenu, non libératoire.</>
) : (
<>Le prélèvement forfaitaire obligatoire (PFO, case 2CK) versé via la 2778-SD est un <em>acompte</em> sur l'impôt sur le revenu, non libératoire.</>
)}{' '}
L'imposition définitive est calculée lors de votre 2042 : par défaut au taux de <strong>{(rates.pfo * 100).toFixed(1)} %</strong> (Flat Tax),
ou sur option expresse au barème progressif. L'acompte déjà versé (2CK) s'impute sur l'IR définitif — tout excédent vous est restitué.{' '}
{isFrom2047 ? (
<>Le montant 2BH (case <strong>272</strong> des Données 2047) est déclaré en sus de 2TR uniquement pour neutraliser les prélèvements sociaux déjà prélevés.</>
) : (
<>Le montant 2BH est déclaré en sus de 2TR uniquement pour neutraliser les prélèvements sociaux déjà prélevés via la 2778-SD.</>
)}
</div>
) : (
<div style={{
padding: '10px 14px', borderRadius: 8,
background: 'var(--surface-2)',
fontSize: 'var(--fs-xs)', lineHeight: 1.6, color: 'var(--text-muted)',
}}>
Les cases 2BH et 2CK ne s'appliquent que si l'option de déclaration 2778-SD est activée
(Paramétrage&nbsp;&gt;&nbsp;Ma fiscalité).
</div>
)}
</div>
);
}