From 9058bc7d22af23b9f4082a1650827316774d4d58 Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 16 Jun 2026 08:38:23 +0200 Subject: [PATCH] =?UTF-8?q?Message=20=20lorsqu'aucun=20enregistrement=20pr?= =?UTF-8?q?=C3=A9sent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MEMORY.md | 45 ++++++++++++++++- frontend/src/components/EmptyState.jsx | 67 ++++++++++++++++++++++++++ frontend/src/pages/Dashboard.jsx | 10 +++- frontend/src/pages/DepotsRetraits.jsx | 7 +++ frontend/src/pages/Investissements.jsx | 7 +++ frontend/src/pages/Plateformes.jsx | 5 +- frontend/src/pages/Remboursements.jsx | 10 +++- frontend/src/pages/TaxReport.jsx | 11 +++++ 8 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/EmptyState.jsx diff --git a/MEMORY.md b/MEMORY.md index ec81d12..1a3738f 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -1,5 +1,5 @@ # MEMORY.md — Crowdlending Tracker -*Dernière mise à jour: 2026-05-09 (session 6)* +*Dernière mise à jour: 2026-06-15 (session 7)* --- @@ -382,3 +382,46 @@ const isBonus = BONUS_VALUES.includes(form.investissement_id); 7. **Modal + dropdown** : ne jamais utiliser `position: absolute` pour un dropdown dans une modale — utiliser `position: fixed` + `getBoundingClientRect()` (cf. `CategorySelect.jsx`). 8. **Suppression dans modale** : ne pas chaîner `.then(() => closeModal())` sur une fonction qui appelle `confirm()` — inliner la logique avec early return si `!confirm(...)` pour éviter la fermeture sur "Annuler". 9. **Affichage investisseur** : toujours `memberLabel(inv)` — jamais `inv.prenom + ' ' + inv.nom` car `nom` contient déjà le nom complet. + +--- + +## Session 7 — Auth, sécurité, section Admin Général (2026-06-15) + +### Système d'audit logs +- Table `audit_logs` (id, actor_id, target_user_id, action, category, details JSON, ip_address, user_agent, created_at) +- Util `backend/src/utils/audit.js` : `audit(req, { action, category, actorId, targetUserId, details })` — silencieux, ne throw jamais +- Route `GET /api/admin/audit-logs` (+ `/categories`) avec filtres page/limit/category/search/dateFrom/dateTo/userId, purge auto 30 jours +- Instrumenté dans : `auth.js` (register, login_failed, login_success, 2fa), `admin.js` (user_created, status_changed, role_changed, user_deleted), `invitations.js` (invitation_sent, invitation_accepted) +- Frontend : `AuditLogsSection.jsx` — badges catégories cliquables, recherche 350ms debounce, plage dates, pagination 50/page, `DetailTooltip` JSON au survol + +### Bug focus-loss sur InvitationRegister +- **Cause** : composant `Wrap` défini à l'intérieur du corps de `InvitationRegister()` → new component type à chaque render → React démonte/remonte tous les enfants → perte de focus à chaque frappe +- **Fix** : déplacer `Wrap` en dehors de la fonction, passer `appInfo` en prop +- **Règle** : ne jamais définir un composant React à l'intérieur d'un autre composant + +### Indicateur de complexité mot de passe (PasswordStrength.jsx) +- Composant `frontend/src/components/PasswordStrength.jsx` +- Props : `password: string`, `minLength: number = 8` +- 5 règles : len (≥ minLength), upper, lower, digit, special +- 6 niveaux visuels : Très faible → Très fort (couleurs fixes) +- Règle `len` dynamique via `buildRules(minLength)` appelé dans le composant +- Déployé sur 5 écrans : Register, ResetPassword, InvitationRegister, MonCompte (SecurityForm), UsersSection (CreateUserModal) + +### Section Admin "Général" — paramètres globaux +- Route backend `GET/PATCH /api/admin/general` → `backend/src/routes/general.js` +- Colonnes DB ajoutées sur `smtp_config` : `allow_registration INTEGER DEFAULT 1`, `min_password_length INTEGER DEFAULT 8` +- Route publique `/api/app-info` enrichie : expose `allowRegistration` et `minPasswordLength` (sans auth) +- `SmtpSection.jsx` : champs appName/appUrl **retirés** (déplacés dans Général) +- `GeneralSection.jsx` : 3 blocs — Identité (appName, appUrl), Accès (toggle auto-inscription), Sécurité (longueur min MDP) +- `Admin.jsx` : section "Général" ajoutée en tête du groupe "Administration de la plateforme" + +### Contrôle auto-inscription +- `App.jsx` : route `/register` → `` si `allowRegistration=false` +- `Login.jsx` : lien "Créer un compte" conditionnel sur `appInfo.allowRegistration !== false` +- Fetch `/api/app-info` au montage dans App.jsx, état initial `allowRegistration: true` (jamais bloquant par défaut) + +### minPasswordLength — propagation +- Chaque écran password fetch `/api/app-info` au montage et extrait `minPasswordLength` +- `useState(8)` comme valeur initiale sur tous les écrans → jamais bloquant si l'API est lente +- Register/ResetPassword/InvitationRegister : via `appInfo.minPasswordLength || 8` (appInfo déjà fetché) +- MonCompte (SecurityForm) et UsersSection (CreateUserModal) : state local `minPasswordLength` + useEffect fetch dédié diff --git a/frontend/src/components/EmptyState.jsx b/frontend/src/components/EmptyState.jsx new file mode 100644 index 0000000..ba355b5 --- /dev/null +++ b/frontend/src/components/EmptyState.jsx @@ -0,0 +1,67 @@ +import { useNavigate } from 'react-router-dom'; + +/** + * État vide — affiché quand aucune plateforme n'est configurée. + * Props : + * icon — emoji ou caractère affiché en grand (défaut : '🏦') + * title — titre du message (défaut : 'Aucune plateforme configurée') + * message — texte explicatif (défaut : message générique plateformes) + * cta — libellé du bouton (défaut : 'Configurer mes plateformes') + * to — route cible du bouton (défaut : '/settings?section=plateformes') + */ +export default function EmptyState({ + icon = '🏦', + title = 'Aucune plateforme configurée', + message = "Vous n'avez pas encore configuré de plateforme de crowdfunding sur votre compte. Associez au moins une plateforme pour commencer à suivre vos investissements.", + cta = 'Configurer mes plateformes', + to = '/settings?section=plateformes', +}) { + const navigate = useNavigate(); + + return ( +
+
{icon}
+

+ {title} +

+

+ {message} +

+ +
+ ); +} diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 2a1bd08..d34b6f0 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -10,6 +10,7 @@ import InteretsDonutChart from '../components/InteretsDonutChart.jsx'; import { InteretsChartProvider, useInteretsChart } from '../context/InteretsChartContext.jsx'; import TableauInteretsPlateforme from '../components/TableauInteretsPlateforme.jsx'; import DrillCellPanel from '../components/DrillCellPanel.jsx'; +import EmptyState from '../components/EmptyState.jsx'; /* ── Sélecteur d'année — doit être enfant de InteretsChartProvider ── */ function YearSelectorKpi() { @@ -422,6 +423,7 @@ export default function Dashboard() { const [loading, setLoading] = useState(false); const [capitalMensuelData, setCapitalMensuelData] = useState([]); const [plateformes, setPlateformes] = useState([]); + const [platLoaded, setPlatLoaded] = useState(false); /* ── drillCell : cellule sélectionnée dans le TIP — par défaut mois courant toutes plateformes ── */ const _now = new Date(); @@ -461,7 +463,7 @@ export default function Dashboard() { useEffect(() => { api.get('/pfu').then(setPfuRates).catch(() => {}); - api.get('/plateformes').then(setPlateformes).catch(() => {}); + api.get('/plateformes').then(d => { setPlateformes(d); setPlatLoaded(true); }).catch(() => { setPlatLoaded(true); }); }, []); useEffect(() => { @@ -475,6 +477,12 @@ export default function Dashboard() { const ready = activeView === 'all' ? investisseurs.length > 0 : !!activeId; if (!ready) return
Sélectionnez un compte investisseur.
; if (loading || !data) return
Chargement…
; + if (platLoaded && plateformes.length === 0) return ( + <> +

Tableau de bord

+ + + ); const { cash, cashByPlatform, portfolio, interets, interetsParAnnee } = data; const viewTitle = activeView === 'all' diff --git a/frontend/src/pages/DepotsRetraits.jsx b/frontend/src/pages/DepotsRetraits.jsx index c2ab82e..9f87b19 100644 --- a/frontend/src/pages/DepotsRetraits.jsx +++ b/frontend/src/pages/DepotsRetraits.jsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { usePagination } from '../hooks/usePagination.js'; import Pagination from '../components/Pagination.jsx'; import PageIcon from '../components/PageIcon.jsx'; +import EmptyState from '../components/EmptyState.jsx'; import { useLocation, useNavigate } from 'react-router-dom'; import { api } from '../api.js'; import { useInvestisseur } from '../context/InvestisseurContext.jsx'; @@ -882,6 +883,12 @@ export default function DepotsRetraits() { }); }; + if (!loading && plats.length === 0) return ( + <> +

Dépôts / Retraits

+ + + ); return ( <>
diff --git a/frontend/src/pages/Investissements.jsx b/frontend/src/pages/Investissements.jsx index 47c2f66..c4edea8 100644 --- a/frontend/src/pages/Investissements.jsx +++ b/frontend/src/pages/Investissements.jsx @@ -15,6 +15,7 @@ import DistributionChart from '../components/DistributionChart.jsx'; import CapitalMensuelTable from '../components/CapitalMensuelTable.jsx'; import { fmtEUR, fmtPct, fmtDate, fmtStatut, today } from '../utils/format.js'; import * as XLSX from 'xlsx'; +import EmptyState from '../components/EmptyState.jsx'; /* ── Constantes ──────────────────────────────────────────────── */ const MOIS_FR = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre']; @@ -742,6 +743,12 @@ export default function Investissements() { const clearFilter = () => setFilter({ statut: '', plateforme_id: '', categorie_inv_id: '', secteur_inv_id: '', year: '', month: '' }); const multiDetenteur = new Set(plats.map(p => p.investisseur_id)).size > 1; + if (!loading && plats.length === 0) return ( + <> +

Investissements

+ + + ); return ( <>
diff --git a/frontend/src/pages/Plateformes.jsx b/frontend/src/pages/Plateformes.jsx index 2faefa5..b287d3f 100644 --- a/frontend/src/pages/Plateformes.jsx +++ b/frontend/src/pages/Plateformes.jsx @@ -4,6 +4,7 @@ import { api } from '../api.js'; import { useInvestisseur } from '../context/InvestisseurContext.jsx'; import { useUi } from '../context/UiContext.jsx'; import PageIcon from '../components/PageIcon.jsx'; +import EmptyState from '../components/EmptyState.jsx'; import InvChart from '../components/InvChart.jsx'; import InvMensuelTable from '../components/InvMensuelTable.jsx'; import { fmtEUR, fmtDate, fmtStatut, memberLabel } from '../utils/format.js'; @@ -567,9 +568,7 @@ export default function Plateformes() {

Plateformes

-
- Aucun investissement trouvé. -
+ ); } diff --git a/frontend/src/pages/Remboursements.jsx b/frontend/src/pages/Remboursements.jsx index 60aadb1..645c3c2 100644 --- a/frontend/src/pages/Remboursements.jsx +++ b/frontend/src/pages/Remboursements.jsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { usePagination } from '../hooks/usePagination.js'; import Pagination from '../components/Pagination.jsx'; import PageIcon from '../components/PageIcon.jsx'; +import EmptyState from '../components/EmptyState.jsx'; import { useLocation, useNavigate } from 'react-router-dom'; import { api } from '../api.js'; import { useInvestisseur } from '../context/InvestisseurContext.jsx'; @@ -177,6 +178,7 @@ export default function Remboursements() { const [corrections, setCorrections] = useState([]); const [investissements, setInvestissements] = useState([]); const [plateformes, setPlateformes] = useState([]); + const [platLoaded, setPlatLoaded] = useState(false); const [pfuRates, setPfuRates] = useState([]); const [loading, setLoading] = useState(false); @@ -263,7 +265,7 @@ export default function Remboursements() { useEffect(() => { api.get('/pfu').then(setPfuRates).catch(() => {}); - api.get('/plateformes').then(setPlateformes).catch(() => {}); + api.get('/plateformes').then(d => { setPlateformes(d); setPlatLoaded(true); }).catch(() => { setPlatLoaded(true); }); api.get('/icons').then(rows => { const m = {}; rows.forEach(r => { m[r.name] = r.filename; }); setLibIcons(m); }).catch(() => {}); }, []); @@ -760,6 +762,12 @@ export default function Remboursements() { const multiDetenteur = new Set(plateformes.map(p => p.investisseur_id)).size > 1; /* ── Rendu ── */ + if (platLoaded && !loading && plateformes.length === 0) return ( + <> +

Remboursements

+ + + ); return ( <>
diff --git a/frontend/src/pages/TaxReport.jsx b/frontend/src/pages/TaxReport.jsx index 8e4d570..4cede2d 100644 --- a/frontend/src/pages/TaxReport.jsx +++ b/frontend/src/pages/TaxReport.jsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import PageIcon from '../components/PageIcon.jsx'; +import EmptyState from '../components/EmptyState.jsx'; import Pagination from '../components/Pagination.jsx'; import { usePagination } from '../hooks/usePagination.js'; import { api } from '../api.js'; @@ -249,6 +250,16 @@ export default function TaxReport() { dlBlob(JSON.stringify(payload, null, 2), `2778-SD-${annee}.json`, 'application/json'); }; + if (!loading && data && availableYears.length === 0) return ( + <> +

Fiscalité

+ + + ); return ( <>