Files
crowdlending-app/frontend/src/pages/Aide.jsx
T
2026-09-04 08:38:32 +02:00

990 lines
71 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { withDevOverrides } from '../utils/devOverrides.js';
/** Nom de fichier à partir de la question (slug, sans accents). */
function slugify(text) {
return text
.toLowerCase()
.normalize('NFD').replace(new RegExp('[\\u0300-\\u036f]', 'g'), '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 60);
}
/* ── Accordéon FAQ ───────────────────────────────────────────── */
/* `openQuestion`/`setOpenQuestion` sont partagés par toutes les FaqItem d'une
même page (état remonté dans le composant parent) : une seule question
ouverte à la fois, ouvrir la suivante referme automatiquement les autres. */
function FaqItem({ question, children, openQuestion, setOpenQuestion }) {
const open = openQuestion === question;
const toggle = () => setOpenQuestion(open ? null : question);
const [pdfState, setPdfState] = useState('idle'); // idle | generating | error
const contentRef = useRef(null);
const handleDownloadPdf = async () => {
if (!contentRef.current || pdfState === 'generating') return;
setPdfState('generating');
// Insère le titre directement dans le bloc réel (contentRef), le temps de
// la capture uniquement, puis le retire — évite de le dupliquer en
// permanence à l'écran tout en réutilisant l'élément réellement rendu
// (un conteneur hors-écran séparé est capturé vide par html2canvas :
// sa zone de rendu ne suit pas un élément poussé loin hors du viewport).
let heading = null;
try {
const { default: html2pdf } = await import('html2pdf.js');
// Fond capturé = fond réel du bloc (var(--surface)) : reste cohérent
// que le thème actif soit clair ou sombre, plutôt qu'un blanc forcé
// qui casserait le contraste du texte en mode sombre.
const bgColor = getComputedStyle(contentRef.current).backgroundColor || '#ffffff';
heading = document.createElement('h3');
heading.textContent = question;
heading.style.margin = '0 0 12px';
heading.style.color = 'var(--text)';
heading.style.fontSize = '1.1rem';
contentRef.current.insertBefore(heading, contentRef.current.firstChild);
await html2pdf()
.set({
margin: 28,
filename: `faq-${slugify(question) || 'crowdlending'}.pdf`,
image: { type: 'jpeg', quality: 0.95 },
html2canvas: { scale: 2, useCORS: true, backgroundColor: bgColor },
jsPDF: { unit: 'pt', format: 'a4', orientation: 'portrait' },
// 'avoid-all' : ne coupe jamais un élément (image, bloc de code,
// ligne de tableau…) au milieu — le pousse entièrement sur la page
// suivante à la place. C'est ce qui manquait avec le moteur HTML
// interne de jsPDF (doc.html()), qui tranchait au hasard.
pagebreak: { mode: ['avoid-all', 'css'] },
})
.from(contentRef.current)
.toPdf()
.get('pdf')
.then((pdf) => {
// Numérotation en bas à droite, une fois le nombre total de pages connu.
const total = pdf.internal.getNumberOfPages();
for (let i = 1; i <= total; i++) {
pdf.setPage(i);
pdf.setFontSize(9);
pdf.setTextColor(150);
pdf.text(
`${i} / ${total}`,
pdf.internal.pageSize.getWidth() - 28,
pdf.internal.pageSize.getHeight() - 16,
{ align: 'right' }
);
}
})
.save();
setPdfState('idle');
} catch (e) {
console.error('Échec de la génération du PDF :', e);
setPdfState('error');
} finally {
if (heading && heading.parentNode) heading.parentNode.removeChild(heading);
}
};
return (
<div style={{
background: 'var(--surface)',
border: '1px solid var(--border)',
borderRadius: 10,
boxShadow: 'var(--shadow)',
padding: '0 20px',
marginBottom: 12,
}}>
<button
onClick={toggle}
style={{
width: '100%', textAlign: 'left', background: 'none', border: 'none',
padding: '14px 0', cursor: 'pointer', display: 'flex',
alignItems: 'center', justifyContent: 'space-between', gap: 12,
color: 'var(--text)', fontSize: '1.05rem', fontWeight: 600,
}}
>
<span>{question}</span>
<svg
width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"
style={{ flexShrink: 0, transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .2s' }}
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
{open && (
<div style={{
paddingBottom: 20, color: 'var(--text-muted)',
fontSize: 'var(--fs-sm)', lineHeight: 1.7,
}}>
<div ref={contentRef} style={{ background: 'var(--surface)' }}>
{children}
</div>
<button
onClick={handleDownloadPdf}
disabled={pdfState === 'generating'}
style={{
display: 'inline-flex', alignItems: 'center', gap: 6, marginTop: 16,
padding: '7px 14px', border: '1px solid var(--border)', borderRadius: 8,
background: 'var(--surface-2)', color: 'var(--text)',
fontSize: 'var(--fs-sm)', fontWeight: 500,
cursor: pdfState === 'generating' ? 'default' : 'pointer',
opacity: pdfState === 'generating' ? 0.6 : 1,
}}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
<path d="M12 3v12" />
<polyline points="7 10 12 15 17 10" />
<path d="M5 21h14" />
</svg>
{pdfState === 'generating' ? 'Génération du PDF…' : 'Télécharger cette FAQ en PDF'}
</button>
{pdfState === 'error' && (
<p style={{ color: 'var(--danger)', fontSize: 'var(--fs-sm)', margin: '8px 0 0' }}>
La génération du PDF a échoué. Réessayez, ou imprimez la page (Ctrl+P / Cmd+P) et choisissez
« Enregistrer en PDF ».
</p>
)}
</div>
)}
</div>
);
}
/* ── Titre de section (regroupement thématique de la FAQ) ──────── */
function FaqSectionTitle({ children, first }) {
return (
<h3 style={{
margin: first ? '0 0 12px' : '32px 0 12px',
fontSize: '0.8rem', fontWeight: 700, textTransform: 'uppercase',
letterSpacing: '0.06em', color: 'var(--text-muted)',
}}>
{children}
</h3>
);
}
/* ── Lien de téléchargement (fichiers statiques dans public/) ──── */
function DownloadLink({ href, children }) {
return (
<a
href={href}
download
style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
color: 'var(--primary)', fontSize: 'var(--fs-sm)', fontWeight: 500,
textDecoration: 'none', margin: '0 0 12px',
}}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
<path d="M12 3v12" />
<polyline points="7 10 12 15 17 10" />
<path d="M5 21h14" />
</svg>
{children}
</a>
);
}
/* ── Navigation ─────────────────────────────────────────────── */
const NAV = [
{
id: 'faq',
label: 'FAQ',
icon: (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
),
},
];
/* ── Page principale ─────────────────────────────────────────── */
export default function Aide() {
const { search } = useLocation();
const navigate = useNavigate();
const [openQuestion, setOpenQuestion] = useState(null); // question ouverte dans la FAQ (une seule à la fois)
const [appInfo, setAppInfo] = useState({}); // { appUrl, mcpUrl } — valeurs brutes de la base, telles quelles
useEffect(() => {
fetch('/api/app-info').then(r => r.json()).then(setAppInfo).catch(() => {});
}, []);
// URL MCP annoncée comme « déjà pré-remplie » : doit refléter ce que
// Mon compte → Serveur MCP affichera réellement, y compris en dev (où
// cette page-là retombe elle-même sur localhost via withDevOverrides).
const mcpConfigUrl = withDevOverrides(appInfo).mcpUrl || 'https://mcp.<votre domaine>/mcp';
const section = new URLSearchParams(search).get('section') || 'faq';
const setSection = (s) => navigate(`/aide?section=${s}`, { replace: true });
return (
<div className="account-layout">
{/* ── Nav gauche ───────────────────────────────────────── */}
<aside className="account-sidebar">
<h1 className="account-title">Centre d'aide</h1>
{NAV.map(item => (
<button
key={item.id}
className={`account-nav-item${section === item.id ? ' active' : ''}`}
onClick={() => setSection(item.id)}
>
{item.icon}
{item.label}
</button>
))}
</aside>
{/* ── Contenu ──────────────────────────────────────────── */}
<div className="account-content">
{section === 'faq' && (
<div>
<h2 style={{ marginTop: 0, marginBottom: 24, fontSize: '1.6rem', fontWeight: 700 }}>Questions fréquentes</h2>
<FaqSectionTitle first>Comprendre la plateforme</FaqSectionTitle>
<FaqItem question="Comment est calculé le solde du porte-monnaie d'une plateforme ?" openQuestion={openQuestion} setOpenQuestion={setOpenQuestion}>
<p style={{ marginTop: 0 }}>
Le solde du porte-monnaie représente les liquidités disponibles sur une plateforme,
c'est-à-dire l'argent que vous pouvez retirer ou réinvestir. Il est calculé comme suit :
</p>
<div style={{ margin: '12px 0', padding: '12px 16px', background: 'var(--surface-2)', borderRadius: 8, fontFamily: 'monospace', fontSize: 'var(--fs-sm)', color: 'var(--text)', lineHeight: 2 }}>
Solde = Dépôts<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;− Retraits manuels<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+ Remboursements crédités au porte-monnaie<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+ Bonus (parrainage / plateforme)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;− Capital investi (en cours ou remboursé)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;− Frais (déduits à la souscription ou réglés via le porte-monnaie)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+ Corrections de solde
</div>
<p><strong style={{ color: 'var(--text)' }}>Retraits manuels</strong> — seuls les retraits que vous avez saisis manuellement sont déduits.
Les retraits générés automatiquement lors d'un remboursement en mode "compte courant" sont exclus,
car ils ne représentent pas un vrai mouvement de porte-monnaie.</p>
<p><strong style={{ color: 'var(--text)' }}>Remboursements crédités au porte-monnaie</strong> — uniquement les remboursements
dont le mode est "Portefeuille" (et non "Compte courant"). Le montant crédité dépend de la fiscalité
de la plateforme :</p>
<ul style={{ margin: '8px 0 8px 16px', paddingLeft: 0 }}>
<li style={{ marginBottom: 6 }}>
<strong style={{ color: 'var(--text)' }}>Plateforme française (Flat Tax)</strong> — le porte-monnaie
reçoit le <em>net reçu</em>, c'est-à-dire le montant après déduction du PFU français (17,2 % de prélèvements
sociaux + 12,8 % d'impôt sur le revenu), prélevé directement à la source par la plateforme.
</li>
<li style={{ marginBottom: 6 }}>
<strong style={{ color: 'var(--text)' }}>Plateforme hors France (sans fiscalité locale)</strong> — le porte-monnaie
reçoit le capital remboursé + cashback + intérêts bruts. Le PFU français n'est pas prélevé à la
source : vous devez le déclarer séparément dans votre déclaration fiscale annuelle.
</li>
<li>
<strong style={{ color: 'var(--text)' }}>Plateforme hors France (avec retenue à la source locale)</strong> — même
principe que ci-dessus, mais la plateforme a déjà prélevé une taxe locale sur les intérêts. Le
porte-monnaie reçoit le capital + cashback + intérêts bruts <em>après</em> cette retenue locale.
Le PFU français reste à déclarer séparément.
</li>
</ul>
<p><strong style={{ color: 'var(--text)' }}>Capital investi</strong> — le montant que vous avez placé dans des prêts actifs
(y compris les réinvestissements complémentaires) est soustrait du porte-monnaie, car ces fonds ne sont
plus disponibles. Ils reviennent progressivement via les remboursements.</p>
<p><strong style={{ color: 'var(--text)' }}>Frais</strong> — les frais liés aux investissements (voir la question dédiée
ci-dessous) réduisent le solde lorsqu'ils sont réglés « à la source » (déduits du versement lors de la
souscription) ou via le porte-monnaie. Les frais réglés depuis un compte courant du détenteur, ou retenus
directement sur un remboursement, n'ont aucun impact ici — ils ne transitent jamais par le porte-monnaie de
la plateforme.</p>
<p style={{ marginBottom: 0 }}><strong style={{ color: 'var(--text)' }}>Corrections de solde</strong> — ajustements manuels
permettant de réconcilier de micro-écarts de calcul (par exemple un arrondi de centimes sur la fiscalité).</p>
</FaqItem>
<FaqItem question="Comment fonctionne le suivi des frais liés aux investissements ?" openQuestion={openQuestion} setOpenQuestion={setOpenQuestion}>
<p style={{ marginTop: 0 }}>
Certaines plateformes facturent des frais en dehors des intérêts et prélèvements fiscaux habituels : frais
d'entrée à la souscription, frais de gestion, frais sur une distribution (retenus ou non sur un
remboursement), ou autre. Le bloc <em>Frais d'opération</em> de la fiche d'un investissement les recense tous.
</p>
<p>
Dès qu'au moins un frais a été déclaré sur l'une de vos plateformes, une page <strong style={{ color: 'var(--text)' }}>Frais</strong> dédiée
apparaît dans le menu de gauche (entre Remboursements et Fiscalité) — après un rechargement de la page qui
suit cette première déclaration. Elle consolide, tous investissements confondus, le montant total, la
répartition par catégorie et le détail de chaque opération, avec ajout/modification/suppression directement
depuis cette vue (plus besoin de passer par la fiche de chaque investissement). Cette page remplace
l'ancien onglet « Frais » qui se trouvait dans Remboursements.
</p>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Où saisir un frais ?</h4>
<ul style={{ margin: '0 0 12px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
<li><strong style={{ color: 'var(--text)' }}>Frais de souscription</strong> — depuis l'étape « Frais » du
formulaire de création ou modification de l'investissement. Un seul frais de cette catégorie par
investissement.</li>
<li><strong style={{ color: 'var(--text)' }}>Frais de gestion ou autre</strong> — depuis le bouton
<strong style={{ color: 'var(--text)' }}> + Ajouter</strong> du bloc <em>Frais d'opération</em> sur la fiche
de l'investissement, ou depuis la page <em>Frais</em> dédiée (avec sélecteur d'investissement).</li>
<li><strong style={{ color: 'var(--text)' }}>Frais sur distribution</strong> — depuis le champ
<strong style={{ color: 'var(--text)' }}> Frais</strong> du formulaire d'un remboursement. La ligne est créée et
synchronisée automatiquement dans le bloc <em>Frais d'opération</em> et dans la page <em>Frais</em> ; elle ne
peut être modifiée ou supprimée que depuis ce remboursement, pour éviter toute désynchronisation.</li>
</ul>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Mode de règlement et impact</h4>
<p>Chaque frais est réglé de l'une des quatre façons suivantes, avec un impact différent :</p>
<ul style={{ margin: '8px 0 12px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
<li><strong style={{ color: 'var(--text)' }}>Déduit du versement lors de la souscription</strong> — aucune
ligne de mouvement séparée : le montant est simplement documenté à titre de traçabilité, mais il réduit
le solde du porte-monnaie (voir la question ci-dessus).</li>
<li><strong style={{ color: 'var(--text)' }}>Porte-monnaie</strong> — débité du solde disponible sur la
plateforme, comme un retrait.</li>
<li><strong style={{ color: 'var(--text)' }}>Débité du compte courant</strong> — réglé depuis un compte
courant du détenteur (choisi à la saisie), sans aucun impact sur le porte-monnaie de la plateforme.</li>
<li><strong style={{ color: 'var(--text)' }}>Déduit du remboursement</strong> — retenu directement sur le
remboursement lié : le montant réduit alors la base imposable et les <em>intérêts nets</em>/<em>net reçu</em>
de ce remboursement (comportement historique). C'est le seul mode où le frais n'est pas une sortie d'argent
séparée à suivre par ailleurs.</li>
</ul>
<p>
Le KPI <strong style={{ color: 'var(--text)' }}>Gain net depuis le début</strong> du tableau de bord déduit
tous les frais réellement payés, <strong style={{ color: 'var(--text)' }}>sauf</strong> ceux du mode « Déduit
du remboursement » : ceux-là sont déjà comptés en creux dans les intérêts nets de ce remboursement (le
retirer une 2ᵉ fois compterait la même dépense deux fois). Les frais « Déduit du versement lors de la
souscription », « Porte-monnaie » et « Débité du compte courant » sont chacun une vraie sortie d'argent qui
n'apparaît nulle part ailleurs dans ce total, donc ils sont bien soustraits.
</p>
<p style={{ marginBottom: 0 }}>
Le <strong style={{ color: 'var(--text)' }}>TRI (XIRR) net</strong> d'un investissement suit une règle
légèrement différente : seuls les frais « Porte-monnaie » et « Compte courant » y sont ajoutés comme sortie
de trésorerie. Un frais « Déduit du versement lors de la souscription » n'y est pas rajouté séparément car
il est déjà reflété dans le montant investi (net du frais dès la souscription) qui sert de point de départ
au calcul — l'ajouter une 2ᵉ fois fausserait le TRI.
</p>
</FaqItem>
<FaqItem question="Comment mettre en place un réinvestissement automatique des intérêts ?" openQuestion={openQuestion} setOpenQuestion={setOpenQuestion}>
<p style={{ marginTop: 0 }}>
Le réinvestissement automatique permet de capitaliser les intérêts perçus après chaque remboursement,
sans aucune saisie manuelle. Les intérêts sont automatiquement réinjectés dans le capital du prêt,
ce qui augmente progressivement le montant investi et les intérêts futurs.
</p>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Activation</h4>
<ol style={{ margin: '0 0 12px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
<li>Ouvrez la fiche d'un investissement.</li>
<li>Cliquez sur le bouton <strong style={{ color: 'var(--text)' }}>⋮</strong> en haut à droite du bloc <em>Informations du projet</em>, puis choisissez <strong style={{ color: 'var(--text)' }}>Réinvestir</strong>.</li>
<li>Dans la modale, sélectionnez l'onglet <strong style={{ color: 'var(--text)' }}>Automatique</strong>.</li>
<li>Cliquez sur <strong style={{ color: 'var(--text)' }}>Activer</strong>.</li>
</ol>
<p>
Une fois activé, le bloc <em>Réinvestissements complémentaires</em> apparaît sur la fiche avec
un badge <strong style={{ color: 'var(--primary)' }}>auto</strong>, même si aucun remboursement
n'a encore eu lieu.
</p>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Quel montant est réinvesti ?</h4>
<p>Le montant réinvesti après chaque remboursement dépend de la fiscalité de la plateforme :</p>
<ul style={{ margin: '8px 0 12px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
<li>
<strong style={{ color: 'var(--text)' }}>Plateforme française (Flat Tax)</strong> — les <em>intérêts nets</em> sont réinvestis
(après déduction du PFU prélevé à la source). C'est le montant réellement reçu sur votre porte-monnaie.
</li>
<li>
<strong style={{ color: 'var(--text)' }}>Plateforme hors France</strong> — les <em>intérêts bruts</em> sont réinvestis,
car aucune retenue n'est effectuée à la source. Pensez à provisionner la fiscalité due lors de votre déclaration annuelle.
</li>
</ul>
<p>Si les intérêts d'un remboursement sont nuls (remboursement de capital seul), aucun réinvestissement n'est créé.</p>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Désactivation</h4>
<p style={{ marginBottom: 0 }}>
Pour désactiver le réinvestissement automatique, cliquez sur <strong style={{ color: 'var(--text)' }}>⋮</strong> dans le bloc
<em> Informations du projet</em> et choisissez <strong style={{ color: 'var(--text)' }}>Désactiver le réinvestissement auto</strong>.
Les réinvestissements déjà créés sont conservés ; les prochains remboursements n'en génèreront plus.
</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 cinq 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 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 }}>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>
<tr>
<td style={{ padding: '8px 10px 8px 0', verticalAlign: 'top', whiteSpace: 'nowrap' }}>
<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2042-C PRO</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}><strong style={{ color: 'var(--text)' }}>Intérêts d'emprunts sur une plateforme d'actif numérique (crowdlending sur RWA).</strong> Visible dès qu'une ou plusieurs de vos plateformes sont de type « Prestataire de Services sur Actifs Numériques » (PSAN) — des bénéfices non commerciaux (BNC), en régime micro (case 5HQ), indépendants des trois formulaires ci-dessus.</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>
<li>La <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2042-C PRO</code> fonctionne à part : les intérêts de plateformes PSAN sont des bénéfices non commerciaux (BNC), pas des revenus de capitaux mobiliers — ils ne transitent ni par la 2047, ni par un crédit d'impôt, et ne sont pas repris dans les cases 2TT/2TR/2BH/2CK/2TY de la 2042. Ils se déclarent séparément, directement sur l'imprimé 2042-C PRO (voir la question dédiée ci-dessous).</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 sont imposés les intérêts perçus via une plateforme d'actifs numériques (PSAN) ?" openQuestion={openQuestion} setOpenQuestion={setOpenQuestion}>
<p style={{ marginTop: 0 }}>
Certaines plateformes de crowdlending prêtent sur des actifs numériques plutôt que sur des projets
classiques (crowdlending sur RWA — <em>Real World Asset</em> tokenisés). Dans le référentiel
Crowdlending, elles sont identifiées par la catégorie <strong style={{ color: 'var(--text)' }}>« Prestataire
de Services sur Actifs Numériques »</strong> (PSAN), par opposition aux plateformes de financement
participatif classiques (« Prestataire de Services de Financement Participatif », PSFP).
</p>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Un régime fiscal différent</h4>
<p>
Les intérêts perçus via une plateforme PSAN ne sont pas des revenus de capitaux mobiliers (RCM)
comme les autres — ce sont des <strong style={{ color: 'var(--text)' }}>bénéfices non commerciaux
(BNC)</strong>, quelle que soit la localisation de la plateforme (française ou étrangère).
Conséquence directe :
</p>
<ul style={{ margin: '8px 0 12px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
<li>Pas de déclaration <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2047</code>, même si la plateforme est domiciliée hors de France.</li>
<li>Pas de crédit d'impôt conventionnel possible.</li>
<li>Déclaration directe sur l'imprimé <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2042-C PRO</code>, en régime micro-BNC, case <strong style={{ color: 'var(--text)' }}>5HQ</strong>.</li>
</ul>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Le régime micro-BNC</h4>
<p>
Vous déclarez le montant brut des intérêts reçus (valorisés en euros au jour de leur réception) ;
l'abattement forfaitaire de <strong style={{ color: 'var(--text)' }}>34 %</strong> est appliqué
automatiquement par l'administration fiscale, vous n'avez rien à calculer vous-même. Si vos
recettes brutes dépassent <strong style={{ color: 'var(--text)' }}>77 700 €</strong> sur l'année —
ou si vous souhaitez déduire des charges réelles (matériel, électricité…) — vous basculez en
régime réel (cases 5KO / 5KP), non géré par l'application.
</p>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Dans l'application</h4>
<p style={{ marginBottom: 0 }}>
Dès qu'au moins une de vos plateformes est catégorisée PSAN, un onglet <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>CERFA
2042 C PRO</code> apparaît sur la page Fiscalité, avec un suivi mensuel des intérêts bruts et la
case 5HQ à reporter — ainsi qu'un bloc dédié « Cases fiscales 2042 C PRO » affiché à côté du
récapitulatif principal. Vous pouvez vérifier ou modifier la catégorie d'une plateforme (PSFP /
PSAN) depuis <strong style={{ color: 'var(--text)' }}>Paramétrage → Plateformes</strong>.
</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). Exception : l'onglet <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>2042-C PRO</code> ne
propose que les deux premières vues (Suivi mensuel / Données 2042 C PRO) — il n'existe pas de
report vers la 2042 classique, les BNC se déclarant sur un imprimé séparé (voir la question dédiée
aux plateformes PSAN ci-dessus).
</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-C PRO</code> si vous avez des plateformes de type PSAN (crowdlending sur actifs numériques) — indépendant des onglets précédents, la case 5HQ se reporte séparément sur l'imprimé 2042-C PRO.</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}>
<p style={{ marginTop: 0 }}>
Le serveur MCP tourne déjà en continu (service Docker <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>crowdlending-mcp</code>,
exposé via Traefik) — vous n'avez rien à installer ni à laisser tourner sur votre machine. Il
suffit de connecter Claude Desktop à l'URL publique.
</p>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Étapes</h4>
<ol style={{ margin: '0 0 12px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
<li>Générez une clé API : <strong style={{ color: 'var(--text)' }}>Mon compte → Clés API → Nouvelle clé</strong> (par exemple nommée « Claude Desktop »).</li>
<li>Allez dans <strong style={{ color: 'var(--text)' }}>Mon compte → Serveur MCP</strong> et renseignez l'URL publique
(<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>{mcpConfigUrl}</code>{appInfo.mcpUrl ? ' — déjà pré-rempli si vous ne l\'avez pas modifié' : ''}) ainsi que la clé générée.</li>
<li>Copiez la configuration générée dans <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>claude_desktop_config.json</code> (Réglages
→ Développeur → Serveurs MCP locaux → Modifier la config) — le mécanisme passe par
<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}> mcp-remote</code> (avec le wrapper <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>cmd /c</code> sous
Windows), généré automatiquement pour vous.</li>
<li>Redémarrez complètement Claude Desktop.</li>
</ol>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Sécurité</h4>
<ul style={{ margin: '0 0 12px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
<li>Le point d'entrée public n'a <strong style={{ color: 'var(--text)' }}>volontairement aucune restriction d'IP</strong> (accessible
depuis n'importe où, pour un usage nomade) : la clé API est donc la seule barrière d'accès. Traitez-la comme un
mot de passe, et révoquez-la immédiatement en cas de doute (Mon compte → Clés API).</li>
<li>Limite de 60 requêtes/minute par IP (au-delà, erreur 429) et fermeture automatique des sessions inactives
depuis plus de 30 minutes — aucune donnée de session n'est conservée entre deux connexions.</li>
<li>L'outil <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>crowdlending_fetch_url</code> (lecture d'une page web arbitraire) reste
<strong style={{ color: 'var(--text)' }}> désactivé</strong> sur ce serveur.</li>
</ul>
<p style={{ marginBottom: 0 }}>
Si la connexion reste bloquée sans erreur visible, la cause est presque toujours la même :
<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}> npx</code> qui échoue silencieusement à joindre le registre npm — voir la
section Dépannage de <strong style={{ color: 'var(--text)' }}>Mon compte → Serveur MCP</strong>.
</p>
</FaqItem>
<FaqItem question="Comment utiliser le serveur MCP au quotidien ? (fonctions disponibles et exemples)" openQuestion={openQuestion} setOpenQuestion={setOpenQuestion}>
<p style={{ marginTop: 0 }}>
Une fois connecté, Claude (Desktop ou tout autre client MCP) peut consulter votre portefeuille en langage
naturel — il choisit lui-même le bon outil selon votre question. Le serveur est <strong style={{ color: 'var(--text)' }}>strictement
en lecture seule</strong> : aucune donnée n'est jamais créée, modifiée ou supprimée depuis une conversation. Toute
saisie (nouvel investissement, remboursement…) reste manuelle dans l'application.
</p>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Fonctions disponibles</h4>
<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 }}>crowdlending_get_investisseur</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>Profil de l'investisseur lié à la clé API (nom, type famille/entreprise, régime fiscal). Avec une clé « Famille et entreprises », renvoie la liste des profils du foyer.</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 }}>crowdlending_get_dashboard</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>KPIs du portefeuille : capital investi, capital en risque, montant remboursé, intérêts bruts/nets, dépôts/retraits, gain net depuis le début (frais hors remboursement déduits) — filtrable par année.</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 }}>crowdlending_list_investissements</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>Liste des investissements (projet, émetteur, plateforme, montant, taux, durée, statut), filtrable par statut.</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 }}>crowdlending_get_investissement</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>Détail complet d'un investissement (par id), y compris ses remboursements réels perçus et les frais qui lui sont liés (souscription, gestion, distribution).</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 }}>crowdlending_list_remboursements</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>Historique des remboursements perçus (toutes plateformes), avec le frais éventuellement retenu sur chacun, filtrable par période.</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 }}>crowdlending_list_frais_operations</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>Frais liés aux investissements — souscription, gestion, distribution — toutes plateformes, filtrable par période.</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 }}>crowdlending_list_depots_retraits</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>Historique des mouvements de cash (dépôts et retraits), du plus récent au plus ancien.</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 }}>crowdlending_list_objectifs</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>Objectifs de versement et de capital investi (Paramétrage → Fixation des objectifs), à comparer au réalisé. Filtrable par type et par année.</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 }}>crowdlending_get_taxreport</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>
Synthèse fiscale annuelle (CERFA 2042/2561/2047/2778-SD) — aide à la préparation de la déclaration,
calculée avec les mêmes règles que la page Fiscalité. Ce n'est pas une source officielle : à vérifier
avant tout report sur votre déclaration.
</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 }}>crowdlending_fetch_url</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>
<em>Développement local uniquement</em>, désactivé par défaut. Lit une page web (ex. annonce de projet sur
une plateforme) et en extrait le texte propre — c'est à vous d'en reprendre les informations utiles pour
créer l'investissement manuellement, l'outil ne saisit rien lui-même.
</td>
</tr>
</tbody>
</table>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Exemples de questions</h4>
<ul style={{ margin: '0 0 12px 16px', paddingLeft: 0, lineHeight: 2 }}>
<li>« Quel est mon encours de crowdlending actuellement ? »</li>
<li>« Quel a été mon rendement (intérêts nets) en 2026 ? »</li>
<li>« Liste-moi les investissements en retard ou en procédure. »</li>
<li>« Donne-moi le détail de l'investissement 42, avec ses remboursements. »</li>
<li>« Quels remboursements ai-je reçus entre le 1er et le 30 juin 2026 ? »</li>
<li>« Quels ont été mes derniers dépôts et retraits ? »</li>
<li>« Combien ai-je payé de frais cette année, et sur quels investissements ? »</li>
<li>« Regarde cette annonce de projet et propose-moi les infos pour créer l'investissement : [URL] »
(développement local, avec <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>crowdlending_fetch_url</code> activé)</li>
<li>« Ai-je atteint mon objectif de versement pour {new Date().getFullYear()} ? »</li>
<li>« Prépare-moi une synthèse fiscale pour l'année {new Date().getFullYear() - 1}. »</li>
</ul>
<p style={{ marginBottom: 0 }}>
Ces exemples fonctionnent aussi bien en dev qu'en prod dès lors que le serveur correspondant est connecté
(voir les FAQ de configuration ci-dessus) — les neuf premiers outils sont identiques dans les deux environnements.
</p>
</FaqItem>
<FaqItem question="Comment interroger mon portefeuille depuis Excel avec Power Query ?" openQuestion={openQuestion} setOpenQuestion={setOpenQuestion}>
<p style={{ marginTop: 0 }}>
Comme le serveur MCP, Power Query s'appuie sur l'API v1 (lecture seule) via une clé API.
Power Query est intégré nativement à Excel (365 / 2016 et plus, Windows ou Mac) — rien à
installer. Chaque étape ci-dessous correspond à un écran que vous pouvez capturer pour
illustrer votre propre guide.
</p>
<div>
<DownloadLink href="/powerquery/crowdlending-powerquery-exemple.xlsb">
Télécharger le classeur complet (.xlsb)
</DownloadLink>
</div>
<p style={{ marginTop: 0, marginBottom: 16 }}>
Le classeur ci-dessus contient déjà les 7 requêtes ci-dessous, prêtes à charger — il ne
reste qu'à renseigner votre clé (étape 1). Vous pouvez aussi tout reconstruire à la main
avec les fichiers <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>.pq</code> individuels
proposés à chaque étape ci-dessous.
</p>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Étape 1 — Générer une clé API</h4>
<ol style={{ margin: '0 0 12px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
<li><strong style={{ color: 'var(--text)' }}>Mon compte → Clés API → Nouvelle clé</strong>.</li>
<li>Donnez-lui un nom explicite (ex. « Excel »), validez, puis copiez immédiatement la clé
affichée — elle ne sera plus visible en clair ensuite.</li>
</ol>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Étape 2 — Créer les deux requêtes de connexion</h4>
<p>
Dans Excel : <strong style={{ color: 'var(--text)' }}>Données → Obtenir des données → À partir d'autres
sources → Requête vide</strong>. Une nouvelle requête « Requête1 » apparaît dans l'éditeur Power Query.
</p>
<ol style={{ margin: '0 0 8px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
<li>Renommez-la <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>ApiBaseUrl</code> (double-clic
sur son nom dans le volet Requêtes), puis <strong style={{ color: 'var(--text)' }}>Affichage → Éditeur avancé</strong>,
effacez le contenu par défaut et collez :</li>
</ol>
<pre style={{ margin: '0 0 12px', padding: '12px 16px', background: 'var(--surface-2)', borderRadius: 8, fontFamily: 'monospace', fontSize: 'var(--fs-sm)', color: 'var(--text)', whiteSpace: 'pre-wrap', overflowX: 'auto' }}>
{`"http://localhost:4000/api/v1"`}
</pre>
<DownloadLink href="/powerquery/queries/ApiBaseUrl.pq">Télécharger ApiBaseUrl.pq</DownloadLink>
<img
src="/powerquery/apibaseurl-editeur-avance.png"
alt="Éditeur avancé Power Query — requête ApiBaseUrl avec l'URL de l'API en production"
style={{ width: '100%', maxWidth: 600, borderRadius: 8, border: '1px solid var(--border)', display: 'block', margin: '0 0 16px' }}
onError={(e) => { e.currentTarget.style.display = 'none'; }}
/>
<ol start={2} style={{ margin: '0 0 8px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
<li>Cliquez <strong style={{ color: 'var(--text)' }}>Terminé</strong>, puis <strong style={{ color: 'var(--text)' }}>Accueil → Fermer et charger dans… → Uniquement créer la connexion</strong> (pas besoin d'un tableau pour celle-ci).</li>
<li>Répétez l'opération pour une seconde requête vide nommée <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>ApiKey</code> :</li>
</ol>
<pre style={{ margin: '0 0 12px', padding: '12px 16px', background: 'var(--surface-2)', borderRadius: 8, fontFamily: 'monospace', fontSize: 'var(--fs-sm)', color: 'var(--text)', whiteSpace: 'pre-wrap', overflowX: 'auto' }}>
{`"clk_live_VOTRE_CLE_API"`}
</pre>
<DownloadLink href="/powerquery/queries/ApiKey.pq">Télécharger ApiKey.pq</DownloadLink>
<img
src="/powerquery/apikey-editeur-avance.png"
alt="Éditeur avancé Power Query — requête ApiKey avec la clé API renseignée"
style={{ width: '100%', maxWidth: 600, borderRadius: 8, border: '1px solid var(--border)', display: 'block', margin: '0 0 16px' }}
onError={(e) => { e.currentTarget.style.display = 'none'; }}
/>
<p>Remplacez par la clé copiée à l'étape 1, entre guillemets. Même chose : Terminé → Uniquement créer la connexion.</p>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Étape 3 — La fonction technique fnApiGet</h4>
<p>
Une troisième requête vide, nommée <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>fnApiGet</code>,
fait l'appel HTTP et transmet la clé dans l'en-tête <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>X-API-Key</code> —
les requêtes suivantes la réutiliseront par son nom, sans la récrire :
</p>
<pre style={{ margin: '0 0 12px', padding: '12px 16px', background: 'var(--surface-2)', borderRadius: 8, fontFamily: 'monospace', fontSize: 'var(--fs-sm)', color: 'var(--text)', whiteSpace: 'pre-wrap', overflowX: 'auto' }}>
{`(chemin as text, optional parametres as nullable record) as any =>
let
parametresBruts = if parametres = null then [] else parametres,
champsUtiles = List.Select(
Record.FieldNames(parametresBruts),
each Record.Field(parametresBruts, _) <> null and Record.Field(parametresBruts, _) <> ""
),
parametresNettoyes = Record.SelectFields(parametresBruts, champsUtiles),
reponse = Web.Contents(
ApiBaseUrl,
[
RelativePath = chemin,
Headers = [#"X-API-Key" = ApiKey, #"Accept" = "application/json"],
Query = parametresNettoyes
]
),
resultat = Json.Document(reponse)
in
resultat`}
</pre>
<DownloadLink href="/powerquery/queries/fnApiGet.pq">Télécharger fnApiGet.pq</DownloadLink>
<img
src="/powerquery/fnapiget-editeur-avance.png"
alt="Éditeur avancé Power Query — fonction fnApiGet"
style={{ width: '100%', maxWidth: 700, borderRadius: 8, border: '1px solid var(--border)', display: 'block', margin: '0 0 16px' }}
onError={(e) => { e.currentTarget.style.display = 'none'; }}
/>
<p>Terminé → Fermer et charger dans… → Uniquement créer la connexion (ce n'est pas une donnée à afficher).</p>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Étape 4 — Première requête de données : Investissements</h4>
<p>Une nouvelle requête vide, nommée <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>Investissements</code> :</p>
<pre style={{ margin: '0 0 12px', padding: '12px 16px', background: 'var(--surface-2)', borderRadius: 8, fontFamily: 'monospace', fontSize: 'var(--fs-sm)', color: 'var(--text)', whiteSpace: 'pre-wrap', overflowX: 'auto' }}>
{`let
// Pour filtrer par statut, remplacez null par [statut = "rembourse"]
// Statuts possibles : en_cours, rembourse, en_retard, procedure, cloture
Source = fnApiGet("/investissements", null),
Resultat = Table.FromRecords(Source)
in
Resultat`}
</pre>
<DownloadLink href="/powerquery/queries/Investissements.pq">Télécharger Investissements.pq</DownloadLink>
<img
src="/powerquery/investissements-editeur-avance.png"
alt="Éditeur avancé Power Query — requête Investissements avec aperçu des données chargées"
style={{ width: '100%', maxWidth: 700, borderRadius: 8, border: '1px solid var(--border)', display: 'block', margin: '0 0 16px' }}
onError={(e) => { e.currentTarget.style.display = 'none'; }}
/>
<p>
Cette fois-ci, <strong style={{ color: 'var(--text)' }}>Terminé → Fermer et charger</strong> (le bouton
simple, pas « … ») : la requête s'ajoute comme tableau dans une nouvelle feuille, avec vos
investissements en ligne et en colonnes.
</p>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Étape 5 — Actualiser</h4>
<p style={{ marginBottom: 0 }}>
<strong style={{ color: 'var(--text)' }}>Données → Actualiser tout</strong> (ou Ctrl+Alt+F5) à tout moment
pour recharger les données depuis l'API — utile après un nouvel investissement ou remboursement
saisi dans l'app.
</p>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Aller plus loin — les autres requêtes</h4>
<p>Même principe (requête vide → nom exact → éditeur avancé → coller → charger) pour :</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 }}>Investisseur</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>Profil investisseur (ou liste des membres si clé « Famille et entreprises »).</td>
<td style={{ padding: '8px 0 8px 10px', verticalAlign: 'top', whiteSpace: 'nowrap' }}>
<DownloadLink href="/powerquery/queries/Investisseur.pq">.pq</DownloadLink>
</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 }}>Dashboard</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>KPIs du portefeuille, au format « Indicateur / Valeur » (facile à croiser dans un TCD).</td>
<td style={{ padding: '8px 0 8px 10px', verticalAlign: 'top', whiteSpace: 'nowrap' }}>
<DownloadLink href="/powerquery/queries/Dashboard.pq">.pq</DownloadLink>
</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 }}>Remboursements</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>Historique des remboursements, filtrable par période.</td>
<td style={{ padding: '8px 0 8px 10px', verticalAlign: 'top', whiteSpace: 'nowrap' }}>
<DownloadLink href="/powerquery/queries/Remboursements.pq">.pq</DownloadLink>
</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 }}>DépôtsRetraits</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>Historique des mouvements de cash.</td>
<td style={{ padding: '8px 0 8px 10px', verticalAlign: 'top', whiteSpace: 'nowrap' }}>
<DownloadLink href="/powerquery/queries/DepotsRetraits.pq">.pq</DownloadLink>
</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 }}>fnDetailInvestissement</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>
Fonction avancée : sur la table <em>Investissements</em>, colonne personnalisée{' '}
<code style={{ fontFamily: 'monospace' }}>= fnDetailInvestissement([id])</code>{' '}
pour ramener le détail + les remboursements de chaque prêt.
</td>
<td style={{ padding: '8px 0 8px 10px', verticalAlign: 'top', whiteSpace: 'nowrap' }}>
<DownloadLink href="/powerquery/queries/fnDetailInvestissement.pq">.pq</DownloadLink>
</td>
</tr>
</tbody>
</table>
<p>
Toutes ces requêtes sont déjà incluses dans le classeur téléchargeable en haut de cette fiche —
les fichiers <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>.pq</code> ci-dessus
ne sont utiles que si vous préférez les coller vous-même dans un classeur existant.
</p>
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Dépannage</h4>
<ul style={{ margin: '0 0 12px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
<li><strong style={{ color: 'var(--text)' }}>« Impossible de se connecter au service distant »</strong> — le
backend n'est pas démarré, ou <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>ApiBaseUrl</code> est
incorrecte (vérifiez le port et le suffixe <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>/api/v1</code>).</li>
<li><strong style={{ color: 'var(--text)' }}>Erreur 401 / clé invalide</strong> — régénérez une clé dans
Mon compte → Clés API et mettez à jour la requête <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>ApiKey</code>.</li>
<li><strong style={{ color: 'var(--text)' }}>Avertissement de confidentialité au premier chargement</strong> —
normal pour toute nouvelle source Web dans Power Query ; choisissez « Organisationnel » ou « Public »
selon votre contexte.</li>
</ul>
<p style={{ marginBottom: 0 }}>
Comme le serveur MCP, ces requêtes sont strictement en lecture seule : aucune saisie n'est possible
depuis Excel, toute modification reste à faire dans l'application.
</p>
</FaqItem>
</div>
)}
</div>
</div>
);
}