Catégorisation des platformes et application de la fiscalité adapté (Impact PSAN , régime micro-BIC)
This commit is contained in:
1 parent
8953d0bb35
commit
33fee587aa
10 files changed
+610
-53
No files matched your search
@@ -0,0 +1,266 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '../api.js';
|
||||
import { MOIS_LABELS, MOIS_NUMS, r, fmtEUR } from './Cerfa2778Preview.jsx';
|
||||
|
||||
// Cases CERFA : 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 })} €`;
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Suivi mensuel — table plateforme × mois, sans les lignes PFO/2778-SD (non applicables : les
|
||||
recettes PSAN sont des BNC, pas des intérêts soumis au prélèvement forfaitaire obligatoire). ── */
|
||||
function SuiviMensuelPsan({ annee, plateformes, totauxMois }) {
|
||||
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 recettes crypto (BNC)</h4>
|
||||
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>Revenus {annee} — plateformes PSAN</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 => <th key={m} style={headStyle}>{m}</th>)}
|
||||
<th style={{ ...headStyle, background: 'var(--surface-2)' }}>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{plateformes.map(plat => {
|
||||
const total = Object.values(plat.mois).reduce((a, b) => a + b, 0);
|
||||
return (
|
||||
<tr key={plat.id}>
|
||||
<td style={{
|
||||
...cellStyle, textAlign: 'left', position: 'sticky', left: 0,
|
||||
background: 'var(--surface)', 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>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
<tr><td colSpan={14} style={{ height: 4, background: 'var(--surface-2)' }} /></tr>
|
||||
<tr style={{ background: 'var(--surface-2)' }}>
|
||||
<td style={{ ...cellStyle, textAlign: 'left', fontWeight: 700, position: 'sticky', left: 0, background: 'var(--surface-2)' }}>
|
||||
Total recettes brutes
|
||||
</td>
|
||||
{totauxMois.map((t, i) => <td key={i} style={{ ...cellStyle, fontWeight: 700 }}>{fmtEUR(t || null)}</td>)}
|
||||
<td style={{ ...cellStyle, fontWeight: 800 }}>{fmtEUR(r(totauxMois.reduce((a, b) => a + b, 0)))}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style={{ padding: '10px 16px', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', borderTop: '1px solid var(--border)' }}>
|
||||
Base : intérêts/rewards bruts encaissés (valorisés en euros au jour de réception), avant tout abattement.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Composant principal ── */
|
||||
export default function Cerfa2042CProPreview({ annee, activeView }) {
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [view, setView] = useState('matrice'); // 'matrice' | 'donnees'
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setData(null);
|
||||
const scopeParams = activeView === 'all' ? { scope: 'all' } : {};
|
||||
api.get('/taxreport/2042cpro', { annee, ...scopeParams })
|
||||
.then(setData)
|
||||
.finally(() => setLoading(false));
|
||||
}, [annee, activeView]);
|
||||
|
||||
if (loading || !data) return <div className="card text-muted">Chargement…</div>;
|
||||
|
||||
const plateformes = data.plateformes ?? [];
|
||||
|
||||
if (plateformes.length === 0) {
|
||||
return (
|
||||
<div className="card">
|
||||
<p className="text-muted" style={{ margin: 0 }}>
|
||||
Aucune recette crypto (plateforme PSAN) pour {annee}.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totauxMois = MOIS_NUMS.map(m => r(plateformes.reduce((s, p) => s + (p.mois[m] ?? 0), 0)));
|
||||
const d = data.donnees2042cpro;
|
||||
|
||||
const platLabel = (p) => {
|
||||
const n = p.investisseur_nom ?? '';
|
||||
const pr = p.investisseur_prenom ?? '';
|
||||
const who = pr && n.startsWith(pr) ? n : [pr, n].filter(Boolean).join(' ');
|
||||
return (
|
||||
<>
|
||||
<span style={{ fontWeight: 500 }}>{p.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 2042 C PRO
|
||||
</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 PSAN (crypto)<br/>BNC — régime micro, case 5HQ
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
|
||||
{/* ── Vue Suivi mensuel ── */}
|
||||
{view === 'matrice' && (
|
||||
<SuiviMensuelPsan annee={annee} plateformes={plateformes} totauxMois={totauxMois} />
|
||||
)}
|
||||
|
||||
{/* ── Vue Données 2042 C PRO ── */}
|
||||
{view === 'donnees' && (
|
||||
<div className="card" style={{ padding: 0, overflow: 'auto' }}>
|
||||
<div style={{ padding: '14px 14px 10px' }}>
|
||||
<h4 style={{ margin: 0, fontWeight: 700 }}>Bénéfices non commerciaux — Régime micro-BNC</h4>
|
||||
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>Vision annuelle {annee} — plateformes PSAN (staking / rewards crypto)</span>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '0 14px 6px' }}>
|
||||
<div style={{ fontSize: 'var(--fs-sm)', fontWeight: 600, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<CaseBadge code="5HQ" /> Recettes brutes par plateforme
|
||||
</div>
|
||||
</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 700 }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ ...headStyle, textAlign: 'left' }}>Plateforme — Détenteur</th>
|
||||
<th style={headStyle}>Recette brute {annee}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{plateformes.map(p => {
|
||||
const total = r(Object.values(p.mois).reduce((a, b) => a + b, 0));
|
||||
return (
|
||||
<tr key={p.id}>
|
||||
<td style={{ ...cellStyle, textAlign: 'left' }}>{platLabel(p)}</td>
|
||||
<td style={cellStyle}>{fmt0(Math.round(total))}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
<tr style={{ background: 'var(--surface-2)' }}>
|
||||
<td style={{ ...cellStyle, textAlign: 'left', fontWeight: 700 }}>Total</td>
|
||||
<td style={{ ...cellStyle, fontWeight: 700 }}>{fmt0(d.case_5HQ)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<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="5HQ" tone="success" /> Micro-BNC — Recettes à reporter en 2042-C PRO
|
||||
</div>
|
||||
<div style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', marginTop: 4, maxWidth: 480, lineHeight: 1.5 }}>
|
||||
Montant brut, sans déduire l'abattement — celui-ci ({Math.round(d.abattement_taux * 100)} %) est appliqué automatiquement par l'administration.
|
||||
Bénéfice imposable indicatif : <strong>{fmt0(d.montant_imposable)}</strong>.
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontWeight: 800, fontSize: '1.2rem' }}>{fmt0(d.case_5HQ)}</div>
|
||||
</div>
|
||||
|
||||
{d.depasse_seuil_micro && (
|
||||
<div style={{
|
||||
margin: '14px', padding: '10px 14px', borderRadius: 8,
|
||||
background: 'rgba(245,158,11,0.08)', border: '1px solid rgba(245,158,11,0.25)',
|
||||
fontSize: 'var(--fs-xs)', color: 'var(--text)', lineHeight: 1.6,
|
||||
}}>
|
||||
⚠ Recettes supérieures au seuil du régime micro-BNC ({fmt0(d.seuil_micro_bnc)}) : le régime réel s'applique
|
||||
(cases <strong>5KO</strong> bénéfice / <strong>5KP</strong> déficit), avec déduction des charges réelles
|
||||
(non calculées par l'application) plutôt que l'abattement forfaitaire de {Math.round(d.abattement_taux * 100)} %.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ padding: '10px 14px 14px', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', lineHeight: 1.6 }}>
|
||||
Les intérêts/rewards perçus via des plateformes de type « Prestataire de Services sur Actifs Numériques »
|
||||
(staking, récompenses crypto) sont imposés en bénéfices non commerciaux (BNC), quelle que soit la
|
||||
localisation de la plateforme — il ne s'agit pas d'un revenu de source étrangère : contrairement au 2047/2778-SD,
|
||||
ce montant n'ouvre droit à aucun crédit d'impôt et ne se déclare pas via l'imprimé n° 2047. Il se reporte
|
||||
directement sur la déclaration n° 2042-C PRO, rubrique « Régime micro BNC ».
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user