Amélioration de la fonctionnalité Fiscalité
This commit is contained in:
1 parent
5437091884
commit
5dc6f21e61
7 files changed
+1184
-167
No files matched your search
@@ -623,4 +623,173 @@ router.get('/2778', (req, res) => {
|
||||
res.json({ annee, plateformes });
|
||||
});
|
||||
|
||||
|
||||
|
||||
/* ── 2047 — Cadre 2, rubrique 230 (Intérêts) — vision annuelle par pays/plateforme ──
|
||||
Détermine, pour chaque plateforme étrangère × investisseur, si les intérêts perçus
|
||||
ouvrent droit à un crédit d'impôt conventionnel (cases 231-238) ou non (case 250),
|
||||
en s'appuyant sur le référentiel pays "taux_credit_impot" (paramétrage admin).
|
||||
Éligibilité = convention active + taux d'intérêt renseigné + non "exclusif résidence"
|
||||
(le pictogramme c/ de la notice signifie imposable exclusivement en France : aucun
|
||||
crédit d'impôt possible, même si la plateforme a réellement prélevé une taxe locale). ── */
|
||||
router.get('/2047', (req, res) => {
|
||||
const annee = req.query.annee || String(new Date().getFullYear() - 1);
|
||||
const scopeAll = req.query.scope === 'all';
|
||||
const invCond = scopeAll
|
||||
? 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
|
||||
: 'i.investisseur_id = ?';
|
||||
const invArg = scopeAll ? req.user.id : req.investisseur.id;
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT
|
||||
p.id AS plateforme_id, p.nom AS plateforme_nom, p.domiciliation,
|
||||
p.fiscalite, p.taux_fiscalite_locale,
|
||||
inv.id AS investisseur_id, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom,
|
||||
COALESCE(SUM(
|
||||
CASE WHEN r.interets_bruts_avant_local IS NOT NULL AND r.interets_bruts_avant_local > 0
|
||||
THEN r.interets_bruts_avant_local ELSE r.interets_bruts END
|
||||
), 0) AS montant_avant_local,
|
||||
COALESCE(SUM(r.interets_bruts), 0) AS montant_net,
|
||||
COALESCE(SUM(r.taxe_locale), 0) AS impot_local
|
||||
FROM remboursements r
|
||||
JOIN investissements i ON i.id = r.investissement_id
|
||||
JOIN plateformes p ON p.id = i.plateforme_id
|
||||
JOIN investisseurs inv ON inv.id = i.investisseur_id
|
||||
WHERE ${invCond}
|
||||
AND substr(r.date_remb,1,4) = ?
|
||||
AND r.statut IN ('paye','partiel')
|
||||
AND r.type = 'normal'
|
||||
AND p.domiciliation != 'FR'
|
||||
GROUP BY p.id, inv.id
|
||||
`).all(invArg, annee);
|
||||
|
||||
// Intérêts plateforme (bonus, sans investissement rattaché) — même logique de fusion que /2778
|
||||
const invCondBonus2047 = scopeAll
|
||||
? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
|
||||
: 'r.bonus_investisseur_id = ?';
|
||||
const rowsInteretsPlateforme = db.prepare(`
|
||||
SELECT
|
||||
p.id AS plateforme_id, p.nom AS plateforme_nom, p.domiciliation,
|
||||
p.fiscalite, p.taux_fiscalite_locale,
|
||||
inv.id AS investisseur_id, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom,
|
||||
COALESCE(SUM(
|
||||
CASE WHEN r.interets_bruts_avant_local IS NOT NULL AND r.interets_bruts_avant_local > 0
|
||||
THEN r.interets_bruts_avant_local ELSE r.interets_bruts END
|
||||
), 0) AS montant_avant_local,
|
||||
COALESCE(SUM(r.interets_bruts), 0) AS montant_net,
|
||||
COALESCE(SUM(r.taxe_locale), 0) AS impot_local
|
||||
FROM remboursements r
|
||||
JOIN plateformes p ON p.id = r.bonus_plateforme_id
|
||||
JOIN investisseurs inv ON inv.id = r.bonus_investisseur_id
|
||||
WHERE ${invCondBonus2047}
|
||||
AND substr(r.date_remb,1,4) = ?
|
||||
AND r.statut IN ('paye','partiel')
|
||||
AND r.type = 'interets_plateforme'
|
||||
AND p.domiciliation != 'FR'
|
||||
GROUP BY p.id, inv.id
|
||||
`).all(invArg, annee);
|
||||
|
||||
const rowsMap2047 = {};
|
||||
for (const r0 of rows) rowsMap2047[`${r0.plateforme_id}_${r0.investisseur_id}`] = { ...r0 };
|
||||
for (const r0 of rowsInteretsPlateforme) {
|
||||
const key = `${r0.plateforme_id}_${r0.investisseur_id}`;
|
||||
if (rowsMap2047[key]) {
|
||||
rowsMap2047[key].montant_avant_local += r0.montant_avant_local;
|
||||
rowsMap2047[key].montant_net += r0.montant_net;
|
||||
rowsMap2047[key].impot_local += r0.impot_local;
|
||||
} else {
|
||||
rowsMap2047[key] = { ...r0 };
|
||||
}
|
||||
}
|
||||
const rowsMerged2047 = Object.values(rowsMap2047).sort((a, b) =>
|
||||
a.plateforme_nom.localeCompare(b.plateforme_nom) || (a.investisseur_nom ?? '').localeCompare(b.investisseur_nom ?? ''));
|
||||
|
||||
// Référentiel pays (taux crédit d'impôt) — jointure par code pays (= plateformes.domiciliation)
|
||||
const paysRows = db.prepare('SELECT * FROM taux_credit_impot').all();
|
||||
const paysMap = {};
|
||||
for (const p of paysRows) {
|
||||
if (p.code_pays) paysMap[p.code_pays.trim().toUpperCase()] = p;
|
||||
}
|
||||
|
||||
const lignes = rowsMerged2047.map(r0 => {
|
||||
const pays = paysMap[(r0.domiciliation || '').trim().toUpperCase()] || null;
|
||||
// Règle fiscale : les montants portés sur les cases d'une déclaration sont arrondis à
|
||||
// l'euro le plus proche (même convention que les cases BA/IA/QR du 2778-SD — cf. computeCases
|
||||
// côté frontend). On arrondit donc ici la base (net encaissé / impôt local) avant d'en dériver
|
||||
// les cases 235/237/238, pour rester cohérent avec le mode de calcul du 2778-SD.
|
||||
const montantNet = Math.round(r0.montant_net);
|
||||
const impotLocal = Math.round(r0.impot_local);
|
||||
|
||||
// Éligible aux cases 231-238 seulement si DEUX conditions sont réunies :
|
||||
// 1) le pays ouvre droit à un crédit d'impôt (convention active, non "exclusif résidence" —
|
||||
// pictogramme c/ = imposable exclusivement en France, donc aucun crédit possible) ;
|
||||
// 2) la plateforme a réellement prélevé un impôt à la source sur l'année (impotLocal > 0).
|
||||
// Sans retenue réelle, il n'y a rien à créditer : la ligne va en case 250 avec son montant
|
||||
// net intégral (= montant brut, puisqu'aucun impôt étranger n'a été prélevé dessus), même
|
||||
// si le pays serait par ailleurs éligible (ex. plateforme "sans fiscalité locale").
|
||||
const paysEligible = !!pays
|
||||
&& pays.statut_convention === 'active'
|
||||
&& !pays.int_exclusif_residence;
|
||||
const eligible = paysEligible && impotLocal > 0;
|
||||
// Certains pays (ex. Bénin, Saint-Barthélemy) n'ont pas de taux forfaitaire chiffré dans la
|
||||
// notice ("le crédit d'impôt est égal à l'impôt prélevé", sans plafond en %) : le crédit est
|
||||
// alors non plafonné et égal à l'impôt réellement supporté à l'étranger.
|
||||
const tauxPlafonne = eligible && pays.int_taux != null;
|
||||
|
||||
const tauxApplicable = tauxPlafonne ? pays.int_taux : null;
|
||||
const resultat = tauxPlafonne ? Math.round(montantNet * pays.int_taux / 100) : (eligible ? impotLocal : 0);
|
||||
const creditRetenu = eligible ? Math.round(Math.min(resultat, impotLocal)) : 0;
|
||||
const interetsInclus = eligible ? montantNet + creditRetenu : 0;
|
||||
|
||||
let motif = null;
|
||||
if (!eligible) {
|
||||
if (!pays) motif = "Aucune convention fiscale identifiée pour ce pays";
|
||||
else if (pays.statut_convention !== 'active') motif = `Convention fiscale ${pays.statut_convention} avec ce pays`;
|
||||
else if (pays.int_exclusif_residence) motif = "Revenus imposables exclusivement en France (pas de crédit d'impôt)";
|
||||
else motif = "Aucune retenue à la source appliquée par cette plateforme (rien à créditer)";
|
||||
}
|
||||
|
||||
return {
|
||||
plateforme_id: r0.plateforme_id,
|
||||
plateforme_nom: r0.plateforme_nom,
|
||||
domiciliation: r0.domiciliation,
|
||||
investisseur_id: r0.investisseur_id,
|
||||
investisseur_nom: r0.investisseur_nom,
|
||||
investisseur_prenom: r0.investisseur_prenom,
|
||||
pays_nom: pays?.nom_pays ?? null,
|
||||
statut_convention: pays?.statut_convention ?? null,
|
||||
eligible_ci: eligible,
|
||||
taux_plafonne: tauxPlafonne,
|
||||
motif,
|
||||
montant_avant_local: round2(r0.montant_avant_local),
|
||||
case_233: eligible ? montantNet : 0,
|
||||
case_234: tauxApplicable,
|
||||
case_235: resultat,
|
||||
case_236: eligible ? impotLocal : 0,
|
||||
case_237: creditRetenu,
|
||||
case_238: interetsInclus,
|
||||
case_250: eligible ? 0 : montantNet,
|
||||
};
|
||||
});
|
||||
|
||||
const totals = lignes.reduce((acc, l) => ({
|
||||
montant_avant_local: acc.montant_avant_local + l.montant_avant_local,
|
||||
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,
|
||||
}), { montant_avant_local: 0, case_233: 0, case_236: 0, case_237: 0, case_238: 0, case_250: 0 });
|
||||
|
||||
totals.case_251 = totals.case_238 + totals.case_250; // Total des intérêts imposables (238+240+250, 240 non utilisé ici)
|
||||
totals.case_252 = totals.case_251; // dont intérêts et produits de placement à revenu fixe → 2042 case 2TR
|
||||
totals.case_8VL = totals.case_237; // Crédit d'impôt conventionnel total (cadre 7) → 2042C case 8VL
|
||||
totals.case_8PL = totals.case_233; // Montant net ouvrant droit à crédit d'impôt étranger → 2042C case 8PL
|
||||
// case_233/236/237/238/250/251/252/8VL/8PL sont déjà des entiers (euros) — seul montant_avant_local
|
||||
// (info complémentaire, non reportée sur une case du formulaire) reste arrondi au centime.
|
||||
totals.montant_avant_local = round2(totals.montant_avant_local);
|
||||
|
||||
res.json({ annee, lignes, totals });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -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 }}
|
||||
<SuiviMensuelMatrix
|
||||
annee={annee}
|
||||
plateformes={plateformes}
|
||||
excluded={excluded}
|
||||
togglePlat={togglePlat}
|
||||
totauxMois={totauxMois}
|
||||
rates={rates}
|
||||
TOTAL_TAX={TOTAL_TAX}
|
||||
/>
|
||||
<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>
|
||||
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── 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,10 +513,12 @@ 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) }))}
|
||||
/>
|
||||
{pfoAssujetti && (
|
||||
<>
|
||||
<Case2042
|
||||
code="2BH"
|
||||
label="Produits soumis aux prélèvements sociaux (idem 2TR)"
|
||||
@@ -496,23 +529,44 @@ function Report2042Block({ annee, totauxMois, rates, plateformes, excluded }) {
|
||||
<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é"
|
||||
note={noteIA}
|
||||
value={totalIA}
|
||||
breakdown={platDetail.map(p => ({ nom: p.nom, val: p.ia }))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
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.
|
||||
{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é.
|
||||
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.
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -343,6 +343,172 @@ export default function Aide() {
|
||||
</p>
|
||||
</FaqItem>
|
||||
|
||||
<FaqSectionTitle>Fiscalité</FaqSectionTitle>
|
||||
|
||||
<FaqItem question="Comment la fiscalité fonctionne-t-elle dans Crowdlending ?" openQuestion={openQuestion} setOpenQuestion={setOpenQuestion}>
|
||||
<p style={{ marginTop: 0 }}>
|
||||
La page <strong style={{ color: 'var(--text)' }}>Fiscalité</strong> (menu de gauche) reprend, année par année,
|
||||
les montants nécessaires pour préparer votre déclaration de revenus. Elle est organisée en quatre onglets,
|
||||
chacun correspondant à un formulaire CERFA officiel :
|
||||
</p>
|
||||
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 'var(--fs-sm)', margin: '0 0 14px' }}>
|
||||
<tbody>
|
||||
<tr style={{ borderBottom: '1px solid var(--border)' }}>
|
||||
<td style={{ padding: '8px 10px 8px 0', verticalAlign: 'top', whiteSpace: 'nowrap' }}>
|
||||
<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2561</code>
|
||||
</td>
|
||||
<td style={{ padding: '8px 0', verticalAlign: 'top' }}><strong style={{ color: 'var(--text)' }}>IFU — Imprimé fiscal unique.</strong> Récapitulatif annuel transmis par les plateformes françaises soumises au prélèvement forfaitaire unique (PFU).</td>
|
||||
</tr>
|
||||
<tr style={{ borderBottom: '1px solid var(--border)' }}>
|
||||
<td style={{ padding: '8px 10px 8px 0', verticalAlign: 'top', whiteSpace: 'nowrap' }}>
|
||||
<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2047</code>
|
||||
</td>
|
||||
<td style={{ padding: '8px 0', verticalAlign: 'top' }}><strong style={{ color: 'var(--text)' }}>Revenus encaissés à l'étranger.</strong> Visible 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é.</td>
|
||||
</tr>
|
||||
<tr style={{ borderBottom: '1px solid var(--border)' }}>
|
||||
<td style={{ padding: '8px 10px 8px 0', verticalAlign: 'top', whiteSpace: 'nowrap' }}>
|
||||
<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2778-SD</code>
|
||||
</td>
|
||||
<td style={{ padding: '8px 0', verticalAlign: 'top' }}><strong style={{ color: 'var(--text)' }}>Prélèvement forfaitaire obligatoire (PFO)</strong>, selon votre situation fiscale — déclaration mensuelle de l'acompte dû sur les intérêts de plateformes étrangères (voir la question suivante).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ padding: '8px 10px 8px 0', verticalAlign: 'top', whiteSpace: 'nowrap' }}>
|
||||
<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2042</code>
|
||||
</td>
|
||||
<td style={{ padding: '8px 0', verticalAlign: 'top' }}><strong style={{ color: 'var(--text)' }}>Déclaration des revenus.</strong> La déclaration principale : reprend les cases calculées à partir des trois formulaires ci-dessus (2TT, 2TR, 2BH, 2CK, 2TY).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>Le sens de circulation des données est toujours le même :</p>
|
||||
<ul style={{ margin: '8px 0 12px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
|
||||
<li><code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2561</code> et <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2047</code> alimentent directement la <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2042</code>.</li>
|
||||
<li>La <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2778-SD</code> (mensuelle) alimente la <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2047</code> (annuelle), qui seule est ensuite reportée sur la 2042 — la 2778-SD n'est qu'un mécanisme d'acompte intermédiaire, jamais une déclaration de substitution à la 2047.</li>
|
||||
</ul>
|
||||
<p>
|
||||
Un pavé « À quoi correspond chaque CERFA ? », avec un schéma récapitulatif, est affiché directement en haut de
|
||||
l'onglet 2047 lorsque vous avez des plateformes étrangères.
|
||||
</p>
|
||||
|
||||
<p style={{ marginBottom: 0 }}>
|
||||
<strong style={{ color: 'var(--text)' }}>Important</strong> — 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 sur le site des impôts :
|
||||
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>
|
||||
</FaqItem>
|
||||
|
||||
<FaqItem question="Comment savoir si je suis concerné par la déclaration mensuelle sur mes plateformes étrangères ?" openQuestion={openQuestion} setOpenQuestion={setOpenQuestion}>
|
||||
<p style={{ marginTop: 0 }}>
|
||||
Cette question concerne le formulaire <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2778-SD</code> (prélèvement
|
||||
forfaitaire obligatoire, ou PFO). Elle ne se pose que si vous détenez au moins une plateforme domiciliée
|
||||
hors de France.
|
||||
</p>
|
||||
|
||||
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>La règle</h4>
|
||||
<p>
|
||||
Le PFO s'applique aux personnes physiques fiscalement domiciliées en France dont le <strong style={{ color: 'var(--text)' }}>revenu
|
||||
fiscal de référence (RFR)</strong> de l'avant-dernière année est égal ou supérieur à :
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 12, margin: '8px 0 12px', flexWrap: 'wrap' }}>
|
||||
<div style={{ padding: '6px 14px', borderRadius: 20, background: 'rgba(99,102,241,0.08)', border: '1px solid rgba(99,102,241,0.2)', fontSize: 'var(--fs-sm)', fontWeight: 600 }}>
|
||||
25 000 € — célibataire, divorcé ou veuf
|
||||
</div>
|
||||
<div style={{ padding: '6px 14px', borderRadius: 20, background: 'rgba(99,102,241,0.08)', border: '1px solid rgba(99,102,241,0.2)', fontSize: 'var(--fs-sm)', fontWeight: 600 }}>
|
||||
50 000 € — couple marié ou pacsé
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Votre RFR figure sur votre <strong style={{ color: 'var(--text)' }}>avis d'imposition</strong> de l'année précédente
|
||||
(ligne « Revenu fiscal de référence »). En dessous de ces seuils, vous êtes dispensé du PFO — mais les
|
||||
prélèvements sociaux restent dus.
|
||||
</p>
|
||||
|
||||
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Où l'activer dans l'application</h4>
|
||||
<ol style={{ margin: '0 0 12px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
|
||||
<li>Allez dans <strong style={{ color: 'var(--text)' }}>Paramétrage → Ma fiscalité</strong>.</li>
|
||||
<li>Dépliez la carte <strong style={{ color: 'var(--text)' }}>« CERFA 2778-SD — Prélèvement Forfaitaire Obligatoire (PFO) pour des revenus de source étrangère »</strong> pour revoir le détail des seuils.</li>
|
||||
<li>Activez le bouton si votre situation vous rend redevable du PFO.</li>
|
||||
</ol>
|
||||
<p>
|
||||
Ce réglage est global : une fois activé, l'onglet <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2778-SD</code> apparaît
|
||||
dans la page Fiscalité, les cases <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2BH</code> / <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2CK</code> se
|
||||
remplissent dans le report 2042, et les lignes de suivi mensuel du PFO (taux, montant à payer, intérêt net)
|
||||
s'affichent dans les tableaux « Suivi mensuel » des onglets 2047 et 2778-SD. Si vous le désactivez, ces
|
||||
éléments sont masqués car ils ne s'appliquent pas à votre situation.
|
||||
</p>
|
||||
|
||||
<p style={{ marginBottom: 0 }}>
|
||||
<strong style={{ color: 'var(--text)' }}>À retenir</strong> — que le PFO s'applique ou non à vous, la déclaration
|
||||
annuelle via le CERFA 2047 reste <strong style={{ color: 'var(--text)' }}>obligatoire</strong> dès que vous avez
|
||||
perçu des intérêts d'une plateforme étrangère. Ce paramètre reflète votre propre situation fiscale : en cas
|
||||
de doute, vérifiez votre avis d'imposition ou consultez un professionnel.
|
||||
</p>
|
||||
</FaqItem>
|
||||
|
||||
<FaqItem question="Comment est calculé le crédit d'impôt sur les intérêts perçus d'une plateforme étrangère ?" openQuestion={openQuestion} setOpenQuestion={setOpenQuestion}>
|
||||
<p style={{ marginTop: 0 }}>
|
||||
Dans l'onglet <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2047</code>, vue
|
||||
« Données 2047 », chaque plateforme étrangère est classée automatiquement selon la convention fiscale
|
||||
signée entre la France et son pays de domiciliation.
|
||||
</p>
|
||||
|
||||
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Éligible au crédit d'impôt (cases 233 à 238)</h4>
|
||||
<p>
|
||||
Le pays a une convention fiscale <strong style={{ color: 'var(--text)' }}>active</strong> et n'impose pas
|
||||
l'imposition exclusivement dans le pays de résidence. Le crédit d'impôt (case 237) est égal à l'impôt
|
||||
réellement prélevé à l'étranger (case 236), plafonné au taux conventionnel du pays lorsque celui-ci est
|
||||
chiffré (case 234) — ou <strong style={{ color: 'var(--text)' }}>non plafonné</strong> pour certains pays dont
|
||||
la convention prévoit un crédit égal à l'impôt prélevé sans limite de taux.
|
||||
</p>
|
||||
|
||||
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Non éligible (case 250)</h4>
|
||||
<p>
|
||||
Le pays est absent des conventions fiscales référencées, sa convention est suspendue ou caduque, ou elle
|
||||
prévoit une imposition exclusivement dans le pays de résidence. Dans ce cas, aucun crédit d'impôt n'est
|
||||
calculé.
|
||||
</p>
|
||||
|
||||
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Sous-totaux par pays</h4>
|
||||
<p style={{ marginBottom: 0 }}>
|
||||
Le formulaire papier 2047 se remplit <strong style={{ color: 'var(--text)' }}>par pays</strong>, pas par
|
||||
plateforme : si plusieurs de vos plateformes sont domiciliées dans le même pays, une ligne « Sous-total »
|
||||
apparaît automatiquement sous le groupe pour faciliter le report. Les totaux 8VL et 8PL (report 2042)
|
||||
ainsi que les cases 251/252 cumulent l'ensemble de vos plateformes étrangères, arrondis à l'euro comme
|
||||
l'exige le formulaire officiel.
|
||||
</p>
|
||||
</FaqItem>
|
||||
|
||||
<FaqItem question="Comment utiliser les onglets de la page Fiscalité pour remplir ma déclaration ?" openQuestion={openQuestion} setOpenQuestion={setOpenQuestion}>
|
||||
<p style={{ marginTop: 0 }}>
|
||||
Chaque onglet CERFA propose généralement plusieurs vues : un <strong style={{ color: 'var(--text)' }}>suivi
|
||||
mensuel</strong> (le détail plateforme par plateforme, mois par mois), une vue <strong style={{ color: 'var(--text)' }}>« Données »</strong> (les
|
||||
cases du formulaire lui-même), et une vue <strong style={{ color: 'var(--text)' }}>« Report 2042 »</strong> (ce
|
||||
qui doit être reporté sur la déclaration principale).
|
||||
</p>
|
||||
|
||||
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Ordre suggéré</h4>
|
||||
<ol style={{ margin: '0 0 12px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
|
||||
<li>Sélectionnez l'<strong style={{ color: 'var(--text)' }}>année</strong> concernée en haut de la page.</li>
|
||||
<li>Onglet <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2561</code> si vous avez des plateformes françaises soumises au PFU.</li>
|
||||
<li>Onglets <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2778-SD</code> puis <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2047</code> si vous avez des plateformes étrangères — le suivi mensuel du 2778-SD documente le détail, la vue « Données 2047 » regroupe l'annuel par pays.</li>
|
||||
<li>Onglet <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2042</code> en dernier : sa vue « Données 2042 » et sa synthèse fiscale récapitulent toutes les cases (2TT, 2TR, 2BH, 2CK, 2TY) à saisir sur le site des impôts.</li>
|
||||
</ol>
|
||||
|
||||
<p>
|
||||
Les montants affichés dans les vues « Données » et les cases officielles sont arrondis à l'euro, comme
|
||||
l'exigent les formulaires CERFA (les montants intermédiaires du suivi mensuel restent au centime près,
|
||||
pour la traçabilité).
|
||||
</p>
|
||||
|
||||
<p style={{ marginBottom: 0 }}>
|
||||
Comme rappelé sur la page elle-même : ce module est une <strong style={{ color: 'var(--text)' }}>aide à la
|
||||
préparation</strong> de votre déclaration, il ne s'y substitue pas. Contrôlez chaque case avant de la
|
||||
reporter — vous restez seul responsable de l'exactitude de votre déclaration.
|
||||
</p>
|
||||
</FaqItem>
|
||||
|
||||
<FaqSectionTitle>API et Serveur MCP pour l'IA</FaqSectionTitle>
|
||||
|
||||
<FaqItem question="Comment configurer le serveur MCP pour l'utiliser dans Claude Desktop" openQuestion={openQuestion} setOpenQuestion={setOpenQuestion}>
|
||||
|
||||
@@ -313,7 +313,7 @@ export default function Plateformes() {
|
||||
setSearchParams(p => { const n = new URLSearchParams(p); n.set('plat', nom); return n; }, { replace: true });
|
||||
};
|
||||
const [selectedDetenteurId, setSelectedDetenteurId] = useState(() => { try { return localStorage.getItem('cl_plat_detenteur') || 'all'; } catch { return 'all'; } });
|
||||
const [selectedYear, setSelectedYear] = useState(() => { try { return localStorage.getItem('cl_plat_year') || String(new Date().getFullYear()); } catch { return String(new Date().getFullYear()); } });
|
||||
const [selectedYear, setSelectedYear] = useState(''); // toujours "Depuis le début" au démarrage de la page
|
||||
const [windowStart, setWindowStart] = useState(0);
|
||||
const [drDetaille, setDrDetaille] = useState(() => {
|
||||
try { return localStorage.getItem('cl_dr_plat_detaille') === 'true'; } catch { return false; }
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user