Amélioration de la fonctionnalité Fiscalité

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

No files matched your search

+147 -4
View File
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import PageIcon from '../components/PageIcon.jsx';
import EmptyState from '../components/EmptyState.jsx';
import Pagination from '../components/Pagination.jsx';
@@ -10,6 +11,7 @@ import { fmtEUR, fmtStatut } from '../utils/format.js';
import Cerfa2561Preview from '../components/Cerfa2561Preview.jsx';
import CerfaRecapTable from '../components/CerfaRecapTable.jsx';
import Cerfa2778Preview from '../components/Cerfa2778Preview.jsx';
import Cerfa2047Preview from '../components/Cerfa2047Preview.jsx';
import Cerfa2042Preview from '../components/Cerfa2042Preview.jsx';
/* ── YearSelector ────────────────────────────────────────────── */
@@ -144,8 +146,78 @@ function ExportDropdown({ disabled, onCSV, onJSON }) {
);
}
/* ── Ligne d'explication d'un formulaire CERFA ── */
function InfoLigneCerfa({ code, titre, texte }) {
return (
<div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
<div style={{
flexShrink: 0, minWidth: 62, textAlign: 'center',
fontFamily: 'monospace', fontWeight: 800, fontSize: 'var(--fs-xs)',
color: 'var(--primary)', background: 'rgba(99,102,241,0.07)',
borderRadius: 6, padding: '4px 6px', letterSpacing: '.03em', whiteSpace: 'nowrap',
}}>{code}</div>
<div>
<div style={{ fontSize: 'var(--fs-sm)', fontWeight: 600 }}>{titre}</div>
<div style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', marginTop: 2, lineHeight: 1.5 }}>{texte}</div>
</div>
</div>
);
}
/* ── Schéma : quel formulaire alimente quel formulaire ── */
function CerfaFlowDiagram({ hasEtranger }) {
const boxFill = 'var(--surface-2)';
const box = (x, y, w, h, code, label, highlight) => (
<g key={code}>
<rect x={x} y={y} width={w} height={h} rx={10}
fill={highlight ? 'rgba(99,102,241,0.08)' : boxFill}
stroke={highlight ? 'var(--primary)' : 'var(--border)'} strokeWidth={highlight ? 1.8 : 1.3} />
<text x={x + w / 2} y={y + h / 2 - 6} textAnchor="middle" fontFamily="monospace" fontWeight="800"
fontSize="14" fill="var(--primary)">{code}</text>
<text x={x + w / 2} y={y + h / 2 + 13} textAnchor="middle" fontSize="10" fill="var(--text-muted)">{label}</text>
</g>
);
const arrow = (x1, y1, x2, y2, key) => (
<line key={key} x1={x1} y1={y1} x2={x2} y2={y2}
stroke="var(--text-muted)" strokeWidth={1.6} markerEnd="url(#cerfaFlowArrow)" />
);
const markerDef = (
<defs>
<marker id="cerfaFlowArrow" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
<path d="M0,0 L6,3 L0,6 Z" fill="var(--text-muted)" />
</marker>
</defs>
);
if (!hasEtranger) {
return (
<svg viewBox="0 0 420 80" style={{ width: '100%', maxWidth: 420, height: 'auto', display: 'block' }}>
{markerDef}
{arrow(160, 40, 228, 40, 'a1')}
{box(20, 15, 140, 50, '2561', 'IFU')}
{box(230, 15, 170, 50, '2042', 'Déclaration revenus', true)}
</svg>
);
}
return (
<svg viewBox="0 0 620 250" style={{ width: '100%', maxWidth: 560, height: 'auto', display: 'block' }}>
{markerDef}
{arrow(520, 58, 520, 98, 'a1')}
{arrow(100, 148, 260, 190, 'a2')}
{arrow(520, 148, 362, 190, 'a3')}
{box(440, 8, 160, 50, '2778-SD', 'PFO mensuel')}
{box(20, 98, 160, 50, '2561', 'IFU')}
{box(440, 98, 160, 50, '2047', 'Revenus étranger')}
{box(210, 190, 200, 54, '2042', 'Déclaration revenus', true)}
</svg>
);
}
/* ── Composant principal ─────────────────────────────────────── */
export default function TaxReport() {
const navigate = useNavigate();
const { activeId, activeView } = useInvestisseur();
const { pfoAssujetti } = useUi();
const [annee, setAnnee] = useState(String(new Date().getFullYear()));
@@ -157,10 +229,14 @@ export default function TaxReport() {
const [detailExpanded, setDetailExpanded] = useState(false);
const [cerfaExpanded, setCerfaExpanded] = useState(false);
const [hasPlateformes, setHasPlateformes] = useState(true); // true par défaut pour éviter un flash du mauvais message
const [hasEtranger, setHasEtranger] = useState(false); // au moins une plateforme étrangère configurée (pour l'onglet CERFA 2047 — indépendant de l'année sélectionnée, même principe que le 2778-SD)
/* ── Plateformes configurées ? (pour distinguer "pas de plateforme" de "pas encore de données") ── */
useEffect(() => {
api.get('/plateformes').then(p => setHasPlateformes((p || []).length > 0)).catch(() => {});
api.get('/plateformes').then(p => {
setHasPlateformes((p || []).length > 0);
setHasEtranger((p || []).some(pl => pl.domiciliation && pl.domiciliation !== 'FR'));
}).catch(() => {});
}, []);
/* ── Chargement des années disponibles ── */
@@ -196,14 +272,14 @@ export default function TaxReport() {
setData2042({
case_2TT: frLignes.reduce((s, l) => s + (l.case_2TT ?? 0), 0),
case_2TR: frLignes.reduce((s, l) => s + (l.case_2TR ?? 0), 0) + Math.round(platEtr.reduce((s, p) => s + etrBA(p), 0)),
case_2BH: frLignes.reduce((s, l) => s + (l.case_2BH ?? 0), 0) + Math.round(platEtr.reduce((s, p) => s + etrBA(p), 0)),
case_2CK: frLignes.reduce((s, l) => s + (l.case_2CK ?? 0), 0) + Math.round(platEtr.reduce((s, p) => s + etrBA(p), 0) * pfo),
case_2BH: frLignes.reduce((s, l) => s + (l.case_2BH ?? 0), 0) + (pfoAssujetti ? Math.round(platEtr.reduce((s, p) => s + etrBA(p), 0)) : 0),
case_2CK: frLignes.reduce((s, l) => s + (l.case_2CK ?? 0), 0) + (pfoAssujetti ? Math.round(platEtr.reduce((s, p) => s + etrBA(p), 0) * pfo) : 0),
case_2TY: frLignes.reduce((s, l) => s + (l.case_2TY ?? 0), 0),
});
}).finally(() => setLoading(false));
};
useEffect(load, [activeId, activeView, annee]); // eslint-disable-line
useEffect(load, [activeId, activeView, annee, pfoAssujetti]); // eslint-disable-line
/* ── Pagination détail ── */
const detail = data?.detail ?? [];
@@ -301,10 +377,73 @@ export default function TaxReport() {
</p>
</div>}
{!detailExpanded && !cerfaExpanded && data2042 && (
<div className="card" style={{ marginTop: 16 }}>
<h3 style={{ marginTop: 0, marginBottom: 10 }}>À quoi correspond chaque CERFA ?</h3>
<div style={{ display: 'flex', gap: 28, alignItems: 'center', flexWrap: 'wrap' }}>
<div style={{ flex: '1 1 320px', minWidth: 280, display: 'flex', flexDirection: 'column', gap: 12 }}>
<InfoLigneCerfa
code="2042"
titre="Déclaration des revenus"
texte="La déclaration principale : reprend les cases calculées ci-dessus (2TT, 2TR, 2BH, 2CK, 2TY)."
/>
<InfoLigneCerfa
code="2561"
titre="IFU — Imprimé fiscal unique"
texte="Récapitulatif annuel transmis par les plateformes françaises soumises au prélèvement forfaitaire unique (PFU)."
/>
{hasEtranger && (
<InfoLigneCerfa
code="2047"
titre="Revenus encaissés à l'étranger"
texte="Obligatoire dès qu'une ou plusieurs de vos plateformes sont domiciliées hors de France — détaille les intérêts perçus pays par pays et le crédit d'impôt associé."
/>
)}
{hasEtranger && (
<InfoLigneCerfa
code="2778-SD"
titre="Prélèvement forfaitaire obligatoire (PFO) - selon votre situation fiscale"
texte="Les intérêts perçus via des plateformes étrangères sont soumis à un prélèvement forfaitaire obligatoire (acompte) si vous avez un revenu fiscal de référence de l'avant-dernière année de 25 000 € pour un personne seule ou 50 000 € pour couple marié ou pacsé. Malgré la déclaration mensuelle aux autorités fiscales, la déclaration annuelle au travers du CERFA 2047 reste obligatoire."
/>
)}
</div>
<div style={{ flex: '1 1 320px', minWidth: 280, display: 'flex', justifyContent: 'center' }}>
<CerfaFlowDiagram hasEtranger={hasEtranger} />
</div>
</div>
{hasEtranger && (
<div style={{
marginTop: 14, padding: '12px 14px', borderRadius: 10,
background: pfoAssujetti ? 'rgba(16,185,129,0.06)' : 'rgba(245,158,11,0.08)',
border: `1px solid ${pfoAssujetti ? 'rgba(16,185,129,0.2)' : 'rgba(245,158,11,0.25)'}`,
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, flexWrap: 'wrap',
}}>
<div style={{ fontSize: 'var(--fs-sm)', lineHeight: 1.5 }}>
{pfoAssujetti ? (
<>Vous avez des plateformes étrangères et l'option de déclaration <strong>2778-SD</strong> est activée.</>
) : (
<>Vous avez des plateformes étrangères. Vérifiez si la déclaration mensuelle <strong>2778-SD</strong> s'applique à votre situation.</>
)}
</div>
<button className="btn btn-sm btn-primary" onClick={() => navigate('/settings?section=ma-fiscalite')}>
{pfoAssujetti ? 'Gérer dans les paramètres' : 'Vérifier dans les paramètres'}
</button>
</div>
)}
<p className="text-muted" style={{ marginTop: 14, marginBottom: 0, fontSize: 12, lineHeight: 1.6 }}>
⚠ Ce module est une aide à la préparation de votre déclaration ; il ne la remplace pas. Vérifiez chaque montant avant de le reporter.
Vous restez seul responsable de l'exactitude de votre déclaration fiscale — Crowdlending ne saurait être tenu responsable des données déclarées.
</p>
</div>
)}
{!detailExpanded && !cerfaExpanded && (
<div className="dr-tabs">
<button className={`dr-tab${activeTab === '2042' ? ' active' : ''}`} onClick={() => setActiveTab('2042')}>CERFA 2042</button>
<button className={`dr-tab${activeTab === 'cerfa' ? ' active' : ''}`} onClick={() => setActiveTab('cerfa')}>CERFA 2561 (IFU)</button>
{hasEtranger && <button className={`dr-tab${activeTab === '2047' ? ' active' : ''}`} onClick={() => setActiveTab('2047')}>CERFA 2047</button>}
{pfoAssujetti && <button className={`dr-tab${activeTab === '2778' ? ' active' : ''}`} onClick={() => setActiveTab('2778')}>CERFA 2778-SD</button>}
</div>
)}
@@ -324,6 +463,10 @@ export default function TaxReport() {
/>
)}
{activeTab === '2047' && hasEtranger && !detailExpanded && !cerfaExpanded && (
<Cerfa2047Preview annee={annee} activeView={activeView} />
)}
{activeTab === '2778' && pfoAssujetti && !detailExpanded && !cerfaExpanded && (
<Cerfa2778Preview annee={annee} activeView={activeView} />
)}