Initial commit
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '../api.js';
|
||||
|
||||
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'];
|
||||
const r2 = v => Math.round((v ?? 0) * 100) / 100;
|
||||
const fmtN = v => Number(v).toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
const fmtI = v => Number(v).toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 0 });
|
||||
const fullName = l => { const n = l.investisseur_nom ?? ''; const p = l.investisseur_prenom ?? ''; return p && n.startsWith(p) ? n : [p, n].filter(Boolean).join(' '); };
|
||||
|
||||
const BADGE_AUTO = { text: 'déclaration automatique', bg: 'rgba(22,163,74,0.1)', color: '#16a34a' };
|
||||
const BADGE_DECL = { text: 'à déclarer', bg: 'rgba(239,68,68,0.1)', color: '#dc2626' };
|
||||
|
||||
function BadgeTag({ badge }) {
|
||||
if (!badge) return null;
|
||||
return (
|
||||
<span style={{ fontSize: 10, padding: '1px 6px', borderRadius: 10, background: badge.bg, color: badge.color, fontWeight: 600, whiteSpace: 'nowrap', marginLeft: 6 }}>
|
||||
{badge.text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* breakdown item: { nom, val, badge } */
|
||||
function Case2042({ code, label, note, value, breakdown }) {
|
||||
if (!value) return null;
|
||||
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)' }}>{fmtI(value)} €</div>
|
||||
</div>
|
||||
{breakdown && breakdown.length > 0 && (
|
||||
<div style={{ padding: '0 14px 10px 118px', display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{breakdown.map((p, i) => (
|
||||
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>
|
||||
<span style={{ display: 'flex', alignItems: 'center' }}>
|
||||
└ {p.nom}
|
||||
<BadgeTag badge={p.badge} />
|
||||
</span>
|
||||
<span style={{ fontWeight: 500, marginLeft: 12 }}>{fmtI(p.val)} €</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Cerfa2042Preview({ annee, activeView, pfoAssujetti }) {
|
||||
const LS_EXCL = 'cl_2778_excluded_plats';
|
||||
|
||||
const [data2561, setData2561] = useState(null);
|
||||
const [data2778, setData2778] = useState(null);
|
||||
const [pfuList, setPfuList] = useState([]);
|
||||
const [excluded, setExcluded] = useState(() => {
|
||||
try { return new Set(JSON.parse(localStorage.getItem(LS_EXCL)) ?? []); }
|
||||
catch { return new Set(); }
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [view, setView] = useState('matrice');
|
||||
const [filterMode, setFilterMode] = useState('all'); // 'all' | 'auto' | 'decl'
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
const scopeParams = activeView === 'all' ? { scope: 'all' } : {};
|
||||
Promise.all([
|
||||
api.get('/taxreport/cerfa2561', { annee, ...scopeParams }),
|
||||
pfuList.length === 0 ? api.get('/pfu') : Promise.resolve(null),
|
||||
api.get('/taxreport/2778', { annee, ...scopeParams }),
|
||||
]).then(([d2561, pfu, d2778]) => {
|
||||
setData2561(d2561);
|
||||
if (pfu) setPfuList(pfu);
|
||||
if (d2778) setData2778(d2778);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [annee, activeView]); // eslint-disable-line
|
||||
|
||||
const frLignes = (data2561?.lignes ?? []).filter(l => l.domiciliation === 'FR');
|
||||
const platEtr = (data2778?.plateformes ?? []).filter(p => !excluded.has(p.id));
|
||||
|
||||
const ratesForYear = () => {
|
||||
const sorted = [...pfuList].sort((a, b) => b.annee - a.annee);
|
||||
const m = sorted.find(r => r.annee <= Number(annee));
|
||||
if (!m) return { pfo: 0.128, csg: 0.106, crds: 0.005, solidarite: 0.075 };
|
||||
return { pfo: (m.impot_revenu ?? 12.8) / 100, csg: (m.csg ?? 10.6) / 100, crds: (m.crds ?? 0.5) / 100, solidarite: (m.solidarite ?? 7.5) / 100 };
|
||||
};
|
||||
const rates = ratesForYear();
|
||||
const totalTaxRate = rates.pfo + rates.csg + rates.crds + rates.solidarite;
|
||||
|
||||
/* ── Suivi mensuel combiné ── */
|
||||
const matriceView = (
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
{loading && <div className="card text-muted">Chargement…</div>}
|
||||
{!loading && frLignes.length === 0 && platEtr.length === 0 && (
|
||||
<div className="card"><p className="text-muted" style={{ margin: 0 }}>Aucune donnée pour {annee}.</p></div>
|
||||
)}
|
||||
{!loading && (frLignes.length > 0 || platEtr.length > 0) && (
|
||||
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
|
||||
<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} — toutes plateformes</span>
|
||||
</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 'var(--fs-xs)' }}>
|
||||
<thead>
|
||||
<tr style={{ background: 'var(--surface-2)', borderBottom: '1px solid var(--border)' }}>
|
||||
<th style={{ padding: '8px 12px', textAlign: 'left', fontWeight: 600, color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>Plateforme — Détenteur</th>
|
||||
{MOIS_LABELS.map(m => (
|
||||
<th key={m} style={{ padding: '6px 8px', textAlign: 'right', fontWeight: 600, color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>{m}</th>
|
||||
))}
|
||||
<th style={{ padding: '6px 12px', textAlign: 'right', fontWeight: 600, color: 'var(--text-muted)' }}>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{frLignes.length > 0 && (
|
||||
<tr style={{ background: 'rgba(22,163,74,0.05)' }}>
|
||||
<td colSpan={14} style={{ padding: '4px 12px', fontSize: 'var(--fs-xs)', fontWeight: 600, color: '#16a34a', letterSpacing: '.04em', textTransform: 'uppercase' }}>
|
||||
Plateformes françaises — déclaration automatique
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{frLignes.map(l => {
|
||||
const total = r2(Object.values(l.mois ?? {}).reduce((s, m) => s + (m.interets_bruts ?? 0), 0));
|
||||
return (
|
||||
<tr key={`fr_${l.plateforme_id}_${l.investisseur_id}`} style={{ borderBottom: '1px solid var(--border)', background: 'transparent' }}>
|
||||
<td style={{ padding: '6px 12px', fontWeight: 500, whiteSpace: 'nowrap' }}>
|
||||
{l.plateforme_nom}
|
||||
<span style={{ color: 'var(--text-muted)', marginLeft: 6 }}>— {fullName(l)}</span>
|
||||
</td>
|
||||
{MOIS_NUMS.map(m => {
|
||||
const v = l.mois?.[m]?.interets_bruts ?? 0;
|
||||
return <td key={m} style={{ padding: '6px 8px', textAlign: 'right', color: v > 0 ? 'var(--text)' : 'var(--text-muted)' }}>{v > 0 ? fmtN(v) + ' €' : '—'}</td>;
|
||||
})}
|
||||
<td style={{ padding: '6px 12px', textAlign: 'right', fontWeight: 700 }}>{total > 0 ? fmtN(total) + ' €' : '—'}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{frLignes.length > 0 && (
|
||||
<tr style={{ borderTop: '2px solid var(--border)', background: 'rgba(22,163,74,0.06)' }}>
|
||||
<td style={{ padding: '6px 12px', fontWeight: 700, color: '#16a34a' }}>Total Plateformes françaises (brut)</td>
|
||||
{MOIS_NUMS.map(m => {
|
||||
const v = r2(frLignes.reduce((s, l) => s + (l.mois?.[m]?.interets_bruts ?? 0), 0));
|
||||
return <td key={m} style={{ padding: '6px 8px', textAlign: 'right', fontWeight: 700, color: v > 0 ? '#16a34a' : 'var(--text-muted)' }}>{v > 0 ? fmtN(v) + ' €' : '—'}</td>;
|
||||
})}
|
||||
<td style={{ padding: '6px 12px', textAlign: 'right', fontWeight: 700, color: '#16a34a' }}>
|
||||
{fmtN(r2(frLignes.reduce((s, l) => s + r2(Object.values(l.mois ?? {}).reduce((ss, m) => ss + (m.interets_bruts ?? 0), 0)), 0)))} €
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{platEtr.length > 0 && (
|
||||
<tr style={{ background: 'rgba(239,68,68,0.05)' }}>
|
||||
<td colSpan={14} style={{ padding: '4px 12px', fontSize: 'var(--fs-xs)', fontWeight: 600, color: '#dc2626', letterSpacing: '.04em', textTransform: 'uppercase' }}>
|
||||
Plateformes étrangères — à déclarer
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{platEtr.map(p => {
|
||||
const total = r2(Object.values(p.mois ?? {}).reduce((s, v) => s + v, 0));
|
||||
return (
|
||||
<tr key={`etr_${p.id}`} style={{ borderBottom: '1px solid var(--border)', background: 'transparent' }}>
|
||||
<td style={{ padding: '6px 12px', fontWeight: 500, whiteSpace: 'nowrap' }}>
|
||||
{p.nom}
|
||||
{(p.investisseur_nom || p.investisseur_prenom) && (
|
||||
<span style={{ color: 'var(--text-muted)', marginLeft: 6 }}>— {fullName(p)}</span>
|
||||
)}
|
||||
</td>
|
||||
{MOIS_NUMS.map(m => {
|
||||
const v = p.mois?.[m] ?? 0;
|
||||
return <td key={m} style={{ padding: '6px 8px', textAlign: 'right', color: v > 0 ? 'var(--text)' : 'var(--text-muted)' }}>{v > 0 ? fmtN(v) + ' €' : '—'}</td>;
|
||||
})}
|
||||
<td style={{ padding: '6px 12px', textAlign: 'right', fontWeight: 700 }}>{total > 0 ? fmtN(total) + ' €' : '—'}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
{platEtr.length > 0 && (
|
||||
<tr style={{ borderTop: '2px solid var(--border)', background: 'rgba(239,68,68,0.06)' }}>
|
||||
<td style={{ padding: '6px 12px', fontWeight: 700, color: '#dc2626' }}>Total Plateformes étrangères (brut)</td>
|
||||
{MOIS_NUMS.map(m => {
|
||||
const v = r2(platEtr.reduce((s, p) => s + (p.mois?.[m] ?? 0), 0));
|
||||
return <td key={m} style={{ padding: '6px 8px', textAlign: 'right', fontWeight: 700, color: v > 0 ? '#dc2626' : 'var(--text-muted)' }}>{v > 0 ? fmtN(v) + ' €' : '—'}</td>;
|
||||
})}
|
||||
<td style={{ padding: '6px 12px', textAlign: 'right', fontWeight: 700, color: '#dc2626' }}>
|
||||
{fmtN(r2(platEtr.reduce((s, p) => s + r2(Object.values(p.mois ?? {}).reduce((ss, v) => ss + v, 0)), 0)))} €
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr style={{ borderTop: '2px solid var(--border)', background: 'var(--surface-2)' }}>
|
||||
<td style={{ padding: '6px 12px', fontWeight: 700 }}>Total général (brut)</td>
|
||||
{MOIS_NUMS.map(m => {
|
||||
const v = r2(frLignes.reduce((s, l) => s + (l.mois?.[m]?.interets_bruts ?? 0), 0) + platEtr.reduce((s, p) => s + (p.mois?.[m] ?? 0), 0));
|
||||
return <td key={m} style={{ padding: '6px 8px', textAlign: 'right', fontWeight: 700, color: v > 0 ? 'var(--text)' : 'var(--text-muted)' }}>{v > 0 ? fmtN(v) + ' €' : '—'}</td>;
|
||||
})}
|
||||
<td style={{ padding: '6px 12px', textAlign: 'right', fontWeight: 700 }}>
|
||||
{fmtN(r2(
|
||||
frLignes.reduce((s, l) => s + r2(Object.values(l.mois ?? {}).reduce((ss, m) => ss + (m.interets_bruts ?? 0), 0)), 0) +
|
||||
platEtr.reduce((s, p) => s + r2(Object.values(p.mois ?? {}).reduce((ss, v) => ss + v, 0)), 0)
|
||||
))} €
|
||||
</td>
|
||||
</tr>
|
||||
{/* Taux total prélevé */}
|
||||
<tr style={{ background: 'transparent' }}>
|
||||
<td style={{ padding: '4px 12px', color: 'var(--text-muted)', fontSize: 'var(--fs-xs)' }}>
|
||||
Taux total prélevé ({(totalTaxRate * 100).toFixed(1)} %)
|
||||
</td>
|
||||
{MOIS_NUMS.map(m => {
|
||||
const brut = r2(frLignes.reduce((s, l) => s + (l.mois?.[m]?.interets_bruts ?? 0), 0) + platEtr.reduce((s, p) => s + (p.mois?.[m] ?? 0), 0));
|
||||
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 taux = brut > 0 ? ((prelevFR + prelevEtr) / brut * 100).toFixed(1) + ' %' : '—';
|
||||
return <td key={m} style={{ padding: '4px 8px', textAlign: 'right', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>{taux}</td>;
|
||||
})}
|
||||
<td style={{ padding: '4px 12px', textAlign: 'right', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>
|
||||
{(totalTaxRate * 100).toFixed(1)} %
|
||||
</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>
|
||||
{MOIS_NUMS.map(m => {
|
||||
const netFR = r2(frLignes.reduce((s, l) => {
|
||||
const mo = l.mois?.[m];
|
||||
return s + ((mo?.interets_bruts ?? 0) - (mo?.prelev_sociaux ?? 0) - (mo?.prelev_forfaitaire ?? 0));
|
||||
}, 0));
|
||||
const netEtr = r2(platEtr.reduce((s, p) => s + (p.mois?.[m] ?? 0), 0) * (1 - totalTaxRate));
|
||||
const net = r2(netFR + netEtr);
|
||||
return <td key={m} style={{ padding: '6px 8px', textAlign: 'right', fontWeight: 700, color: net > 0 ? 'var(--success)' : 'var(--text-muted)' }}>{net > 0 ? fmtN(net) : '—'}</td>;
|
||||
})}
|
||||
<td style={{ padding: '6px 12px', textAlign: 'right', fontWeight: 700, color: 'var(--success)' }}>
|
||||
{fmtN(r2(
|
||||
frLignes.reduce((s, l) => s + r2(Object.values(l.mois ?? {}).reduce((ss, m) => ss + ((m.interets_bruts ?? 0) - (m.prelev_sociaux ?? 0) - (m.prelev_forfaitaire ?? 0)), 0)), 0) +
|
||||
platEtr.reduce((s, p) => s + r2(Object.values(p.mois ?? {}).reduce((ss, v) => ss + v, 0)) * (1 - totalTaxRate), 0)
|
||||
))} €
|
||||
</td>
|
||||
</tr>
|
||||
<tr style={{ background: 'var(--surface-2)' }}>
|
||||
<td colSpan={14} style={{ padding: '8px 12px', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', borderTop: '1px solid var(--border)' }}>
|
||||
Plateformes françaises : prélèvements à la source (taux {(totalTaxRate * 100).toFixed(1)} %) — déclaration automatique sur 2042 · Plateformes étrangères : à déclarer
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
/* ── Données 2042 — cases mixtes par code ── */
|
||||
const données2042View = (() => {
|
||||
if (loading) return <div className="card text-muted">Chargement…</div>;
|
||||
|
||||
// Totaux FR
|
||||
const fr2TT = frLignes.reduce((s, l) => s + (l.case_2TT ?? 0), 0);
|
||||
const fr2TR = frLignes.reduce((s, l) => s + (l.case_2TR ?? 0), 0);
|
||||
const fr2BH = frLignes.reduce((s, l) => s + (l.case_2BH ?? 0), 0);
|
||||
const fr2CK = frLignes.reduce((s, l) => s + (l.case_2CK ?? 0), 0);
|
||||
const fr2TY = frLignes.reduce((s, l) => s + (l.case_2TY ?? 0), 0);
|
||||
|
||||
// Totaux étrangers
|
||||
const etrBA = p => r2(Object.values(p.mois ?? {}).reduce((s, v) => s + v, 0));
|
||||
const applyFilter = items => filterMode === 'all' ? items : items.filter(i => (filterMode === 'auto' ? i.badge === BADGE_AUTO : i.badge === BADGE_DECL));
|
||||
const totalEtrBA = r2(platEtr.reduce((s, p) => s + etrBA(p), 0));
|
||||
const totalEtrIA = Math.round(totalEtrBA * rates.pfo);
|
||||
|
||||
// Breakdown 2TT : FR seulement
|
||||
const bd2TT = applyFilter(frLignes.filter(l => l.case_2TT > 0).map(l => ({ nom: `${l.plateforme_nom} — ${fullName(l)}`, val: l.case_2TT, badge: BADGE_AUTO })));
|
||||
const total2TT = bd2TT.reduce((s, i) => s + i.val, 0);
|
||||
|
||||
// Breakdown 2TR : FR (case_2TR) + étrangères
|
||||
const bd2TR = applyFilter([
|
||||
...frLignes.filter(l => l.case_2TR > 0).map(l => ({ nom: `${l.plateforme_nom} — ${fullName(l)}`, val: l.case_2TR, badge: BADGE_AUTO })),
|
||||
...platEtr.filter(p => etrBA(p) > 0).map(p => ({ nom: p.nom, val: Math.round(etrBA(p)), badge: BADGE_DECL })),
|
||||
]);
|
||||
const total2TR = bd2TR.reduce((s, i) => s + i.val, 0);
|
||||
|
||||
// Breakdown 2BH : FR + étrangères
|
||||
const bd2BH = applyFilter([
|
||||
...frLignes.filter(l => l.case_2BH > 0).map(l => ({ nom: `${l.plateforme_nom} — ${fullName(l)}`, val: l.case_2BH, badge: BADGE_AUTO })),
|
||||
...platEtr.filter(p => etrBA(p) > 0).map(p => ({ nom: p.nom, val: Math.round(etrBA(p)), badge: BADGE_DECL })),
|
||||
]);
|
||||
const total2BH = bd2BH.reduce((s, i) => s + i.val, 0);
|
||||
|
||||
// Breakdown 2CK : FR (PFNL retenu) + étrangères (acompte 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 })),
|
||||
]);
|
||||
const total2CK = bd2CK.reduce((s, i) => s + i.val, 0);
|
||||
|
||||
// Breakdown 2TY : FR seulement
|
||||
const bd2TY = applyFilter(frLignes.filter(l => l.case_2TY > 0).map(l => ({ nom: `${l.plateforme_nom} — ${fullName(l)}`, val: l.case_2TY, badge: BADGE_AUTO })));
|
||||
const total2TY = bd2TY.reduce((s, i) => s + i.val, 0);
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
|
||||
<h4 style={{ margin: 0, fontWeight: 700 }}>Report annuel — Déclaration 2042</h4>
|
||||
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>Revenus {annee} à reporter sur la 2042 déposée en {Number(annee) + 1}</span>
|
||||
<div style={{ flex: 1 }} />
|
||||
<div style={{ display: 'flex', gap: 2, background: 'var(--surface-2)', borderRadius: 8, padding: 2, border: '1px solid var(--border)' }}>
|
||||
{[['all','Tout'],['auto','Automatique'],['decl','À déclarer']].map(([val, label]) => (
|
||||
<button key={val} onClick={() => setFilterMode(val)} style={{ padding: '4px 10px', borderRadius: 6, border: 'none', cursor: 'pointer', fontSize: 'var(--fs-xs)', fontWeight: 600, background: filterMode === val ? 'var(--primary)' : 'transparent', color: filterMode === val ? '#fff' : 'var(--text-muted)', transition: 'all .15s' }}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden', marginBottom: 14 }}>
|
||||
<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="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.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})();
|
||||
|
||||
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 17v-2m3 2v-4m3 4v-6m2 10H7a2 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 2042
|
||||
</button>
|
||||
</nav>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
{view === 'matrice' && matriceView}
|
||||
{view === 'donnees' && données2042View}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user