Catégorisation des platformes et application de la fiscalité adapté (Impact PSAN , régime micro-BIC)

This commit is contained in:
ocroguennec committed 2026-08-30 16:53:04 +02:00
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>
);
}