Amélioration de la fonctionnalité Fiscalité
This commit is contained in:
1 parent
5437091884
commit
5dc6f21e61
7 files changed
+1206
-189
No files matched your search
@@ -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 > 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 > 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>
|
||||
);
|
||||
}
|
||||
@@ -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 > Ma fiscalité).
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user