Améliorations diverses
This commit is contained in:
1 parent
441da1975a
commit
41c464e6a3
24 files changed
+907
-55
No files matched your search
+400
-43
@@ -1,21 +1,107 @@
|
||||
import { useState } from 'react';
|
||||
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 ───────────────────────────────────────────── */
|
||||
function FaqItem({ question, children }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
/* `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={{
|
||||
borderBottom: '1px solid var(--border)',
|
||||
padding: '0',
|
||||
background: 'var(--surface)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 10,
|
||||
boxShadow: 'var(--shadow)',
|
||||
padding: '0 20px',
|
||||
marginBottom: 12,
|
||||
}}>
|
||||
<button
|
||||
onClick={() => setOpen(o => !o)}
|
||||
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: 'var(--fs-base)', fontWeight: 500,
|
||||
color: 'var(--text)', fontSize: '1.05rem', fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<span>{question}</span>
|
||||
@@ -29,16 +115,81 @@ function FaqItem({ question, children }) {
|
||||
</button>
|
||||
{open && (
|
||||
<div style={{
|
||||
paddingBottom: 16, color: 'var(--text-muted)',
|
||||
paddingBottom: 20, color: 'var(--text-muted)',
|
||||
fontSize: 'var(--fs-sm)', lineHeight: 1.7,
|
||||
}}>
|
||||
{children}
|
||||
<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 = [
|
||||
{
|
||||
@@ -59,6 +210,17 @@ const NAV = [
|
||||
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 });
|
||||
@@ -86,9 +248,11 @@ export default function Aide() {
|
||||
|
||||
{section === 'faq' && (
|
||||
<div>
|
||||
<h2 style={{ marginTop: 0, marginBottom: 24 }}>Questions fréquentes</h2>
|
||||
<h2 style={{ marginTop: 0, marginBottom: 24, fontSize: '1.6rem', fontWeight: 700 }}>Questions fréquentes</h2>
|
||||
|
||||
<FaqItem question="Comment est calculé le solde du porte-monnaie d'une plateforme ?">
|
||||
<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 :
|
||||
@@ -137,7 +301,7 @@ export default function Aide() {
|
||||
permettant de réconcilier de micro-écarts de calcul (par exemple un arrondi de centimes sur la fiscalité).</p>
|
||||
</FaqItem>
|
||||
|
||||
<FaqItem question="Comment mettre en place un réinvestissement automatique des intérêts ?">
|
||||
<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,
|
||||
@@ -179,41 +343,26 @@ export default function Aide() {
|
||||
</p>
|
||||
</FaqItem>
|
||||
|
||||
<FaqItem question="Comment configurer le serveur MCP en production (sans passer par le développement local) ?">
|
||||
<p style={{ marginTop: 0 }}>
|
||||
Le serveur MCP tourne déjà en continu en production (service Docker <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>crowdlending-mcp</code>,
|
||||
exposé via Traefik) — contrairement au développement local, vous n'avez rien à démarrer ni
|
||||
à laisser tourner sur votre machine. Il suffit de connecter Claude Desktop à l'URL publique.
|
||||
</p>
|
||||
<FaqSectionTitle>API et Serveur MCP pour l'IA</FaqSectionTitle>
|
||||
|
||||
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Prérequis</h4>
|
||||
<ul style={{ margin: '0 0 12px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
|
||||
<li>Le service <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>crowdlending-mcp</code> du
|
||||
<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}> docker-compose.yml</code> doit être déployé, avec un enregistrement
|
||||
DNS pour <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>mcp.<votre domaine></code> pointant vers la même IP que l'app
|
||||
(certificat TLS automatique via Traefik).</li>
|
||||
<li>Une clé API <strong style={{ color: 'var(--text)' }}>dédiée à la production</strong>, distincte de celle utilisée en développement
|
||||
local si vous en avez une — cela permet de révoquer l'une sans affecter l'autre.</li>
|
||||
</ul>
|
||||
<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 dédiée : <strong style={{ color: 'var(--text)' }}>Mon compte → Clés API → Nouvelle clé</strong> (par exemple nommée « MCP Prod »).</li>
|
||||
<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 }}>https://mcp.<votre domaine>/mcp</code>) ainsi que la clé générée. L'environnement
|
||||
se détecte automatiquement sur « PROD » dès que l'URL ne contient ni <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>localhost</code> ni
|
||||
<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}> dev</code> — cochez « Forcer manuellement » si votre domaine de test prête à confusion.</li>
|
||||
(<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), exactement comme en développement — seule l'URL change,
|
||||
le mécanisme <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>mcp-remote</code> (et le wrapper <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>cmd /c</code> sous
|
||||
Windows) reste identique.</li>
|
||||
→ 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>
|
||||
<p>
|
||||
Vous pouvez connecter dev et prod <strong style={{ color: 'var(--text)' }}>simultanément</strong> : répétez ces étapes une seconde fois
|
||||
avec l'URL locale et une clé distincte, les deux entrées de config (<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>crowdlending-dev</code> /
|
||||
<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}> crowdlending-prod</code>) coexistent sans collision.
|
||||
</p>
|
||||
|
||||
<h4 style={{ margin: '16px 0 8px', color: 'var(--text)' }}>Sécurité</h4>
|
||||
<ul style={{ margin: '0 0 12px 16px', paddingLeft: 0, lineHeight: 1.8 }}>
|
||||
@@ -223,16 +372,16 @@ export default function Aide() {
|
||||
<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é en production</strong>, même s'il est activé chez vous en développement local.</li>
|
||||
<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 qu'en développement local
|
||||
(<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>npx</code> qui échoue silencieusement à joindre le registre npm) — voir la
|
||||
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)">
|
||||
<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
|
||||
@@ -309,6 +458,214 @@ export default function Aide() {
|
||||
</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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user