From 25a31feec491e2b8d4e89b14e6e2da2e8b849004 Mon Sep 17 00:00:00 2001 From: Anthony Coyaud Date: Sat, 15 Aug 2026 13:28:36 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(dashboard):=20filtres=20services/cat?= =?UTF-8?q?=C3=A9gories=20restreints=20aux=20r=C3=A9sultats?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les select de service et catégorie utilisaient getServices()/getCategories(), soit les référentiels complets — dont certaines entrées (ex. "Business Intelligence") n'ont aucun monitoring rattaché et menaient sur un dashboard vide. Bascule vers le nouvel endpoint GET /dashboard/filtres (VUE_CONSO), avec filtrage en cascade : sélectionner un service restreint la liste des catégories proposées à celles qui ont réellement des résultats pour ce service. --- .../src/pages/Dashboard/Dashboard.jsx | 45 ++++++++++++++----- DataSentinel/src/services/api.js | 12 +++++ 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/DataSentinel/src/pages/Dashboard/Dashboard.jsx b/DataSentinel/src/pages/Dashboard/Dashboard.jsx index 7797dbf..79c30e2 100644 --- a/DataSentinel/src/pages/Dashboard/Dashboard.jsx +++ b/DataSentinel/src/pages/Dashboard/Dashboard.jsx @@ -1,8 +1,8 @@ /* Page principale du dashboard : KPI cards + grille de monitorings */ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useMemo } from 'react'; import { RefreshCw } from 'lucide-react'; -import { getDashboard, getDashboardSummary, getServices, getCategories } from '../../services/api'; +import { getDashboard, getDashboardSummary, getDashboardFiltres } from '../../services/api'; import MonitoringCard from '../../components/common/MonitoringCard'; import LoadingSpinner from '../../components/common/LoadingSpinner'; import ErrorMessage from '../../components/common/ErrorMessage'; @@ -22,19 +22,22 @@ export default function Dashboard() { const [summary, setSummary] = useState(null); const [monitorings, setMonitorings] = useState([]); const [services, setServices] = useState([]); - const [categories, setCategories] = useState([]); + const [combinaisons, setCombinaisons] = useState([]); const [filtreService, setFiltreService] = useState(''); const [filtreCategorie, setFiltreCategorie] = useState(''); const [loading, setLoading] = useState(true); const [erreur, setErreur] = useState(''); - /* Chargement initial des filtres */ + /* Chargement initial des filtres. + On n'utilise pas les référentiels SERVICE / CATEGORIE complets : ils + contiennent des entrées sans aucun monitoring rattaché (ex. « Business + Intelligence »), qui mèneraient sur un dashboard vide. */ useEffect(() => { async function loadFiltres() { try { - const [svcs, cats] = await Promise.all([getServices(), getCategories()]); - setServices(svcs); - setCategories(cats); + const filtres = await getDashboardFiltres(); + setServices(filtres.services); + setCombinaisons(filtres.combinaisons); } catch { /* Non bloquant, les filtres restent vides */ } @@ -42,6 +45,28 @@ export default function Dashboard() { loadFiltres(); }, []); + /* Catégories proposées : restreintes au service sélectionné, le cas échéant */ + const categoriesDisponibles = useCallback(service => { + const pertinentes = service + ? combinaisons.filter(c => c.service === service) + : combinaisons; + return [...new Set(pertinentes.map(c => c.categorie))].sort(); + }, [combinaisons]); + + const categories = useMemo( + () => categoriesDisponibles(filtreService), + [categoriesDisponibles, filtreService], + ); + + /* Changer de service peut rendre la catégorie sélectionnée sans résultat : + on la remet à zéro dans le même rendu, pour ne déclencher qu'un seul appel. */ + function handleServiceChange(service) { + setFiltreService(service); + if (filtreCategorie && !categoriesDisponibles(service).includes(filtreCategorie)) { + setFiltreCategorie(''); + } + } + /* Chargement des données dashboard */ const loadData = useCallback(async () => { setLoading(true); @@ -105,11 +130,11 @@ export default function Dashboard() { diff --git a/DataSentinel/src/services/api.js b/DataSentinel/src/services/api.js index 7c6f0c9..bbd1116 100644 --- a/DataSentinel/src/services/api.js +++ b/DataSentinel/src/services/api.js @@ -245,6 +245,18 @@ export async function getDashboard(service, categorie) { })) : data; } +/* Récupère les valeurs de filtre qui ramènent au moins un monitoring (VUE_CONSO). + Évite de proposer un service ou une catégorie du référentiel qui n'a aucun + monitoring rattaché, et donc de mener l'utilisateur sur un écran vide. */ +export async function getDashboardFiltres() { + const data = await fetchApi('/dashboard/filtres'); + return { + services: Array.isArray(data?.services) ? data.services : [], + categories: Array.isArray(data?.categories) ? data.categories : [], + combinaisons: Array.isArray(data?.combinaisons) ? data.combinaisons : [], + }; +} + /* Récupère le résumé global KPI du dashboard */ export async function getDashboardSummary() { const data = await fetchApi('/dashboard/summary'); From d27ff11ea200cc7dc181898e338ba7e6edce4808 Mon Sep 17 00:00:00 2001 From: Anthony Coyaud Date: Sat, 15 Aug 2026 13:28:43 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(historique):=20p=C3=A9riode=20par=20d?= =?UTF-8?q?=C3=A9faut=20du=201er=20janvier=20=C3=A0=20aujourd'hui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La page se limitait aux 5 derniers jours par défaut, alors que l'historique disponible remonte au 1er janvier. Corrige aussi un bug de fuseau horaire : les dates par défaut étaient calculées avec toISOString(), qui bascule en UTC (un 1er janvier saisi depuis Paris, UTC+1, devenait le 31 décembre de l'année précédente) — remplacé par un formatage en heure locale. La page charge maintenant directement cette période au montage, au lieu d'attendre un clic sur "Afficher" pour afficher autre chose qu'un écran vide sous les filtres. --- .../src/pages/Historique/Historique.jsx | 116 +++++++++++++----- 1 file changed, 82 insertions(+), 34 deletions(-) diff --git a/DataSentinel/src/pages/Historique/Historique.jsx b/DataSentinel/src/pages/Historique/Historique.jsx index 4849f91..6c07917 100644 --- a/DataSentinel/src/pages/Historique/Historique.jsx +++ b/DataSentinel/src/pages/Historique/Historique.jsx @@ -2,7 +2,7 @@ // Page Historique : filtres + graphique ligne + tableau résumé import { useState, useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, ComposedChart } from 'recharts'; +import { Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, ComposedChart } from 'recharts'; import { getMonitorings, getHistorique, getHistoriqueEvolution, getEvolutionGlobal, exportToCSV } from '../../services/api'; import DataTable from '../../components/common/DataTable'; import LoadingSpinner from '../../components/common/LoadingSpinner'; @@ -10,25 +10,66 @@ import ErrorMessage from '../../components/common/ErrorMessage'; import Button from '../../components/common/Button'; import styles from './Historique.module.css'; +/* Formate une date au format attendu par (YYYY-MM-DD). + On lit les composantes en heure locale plutôt que d'utiliser toISOString(), + qui bascule en UTC : un 1er janvier saisi depuis Paris (UTC+1) y devient le + 31 décembre de l'année précédente. */ +function formatDateISO(date) { + const mois = String(date.getMonth() + 1).padStart(2, '0'); + const jour = String(date.getDate()).padStart(2, '0'); + return `${date.getFullYear()}-${mois}-${jour}`; +} + +/* Période affichée par défaut : du 1er janvier de l'année courante à aujourd'hui. */ +function periodeParDefaut() { + const aujourdhui = new Date(); + return { + debut: formatDateISO(new Date(aujourdhui.getFullYear(), 0, 1)), + fin: formatDateISO(aujourdhui), + }; +} + +/* Récupère le tableau d'historique et la série du graphique pour les filtres + donnés. Sans monitoring sélectionné, on prend la courbe globale. + Fonction pure : elle ne touche à aucun état, ce qui permet de l'appeler + aussi bien depuis le bouton « Afficher » que depuis l'effet de montage. */ +async function recupererHistorique(monitoring, debut, fin) { + const [histResult, evolResult] = monitoring + ? await Promise.all([ + getHistorique(monitoring, debut, fin), + getHistoriqueEvolution(monitoring, debut, fin), + ]) + : await Promise.all([ + getHistorique(undefined, debut, fin), + getEvolutionGlobal(debut, fin), + ]); + + return { + hist: Array.isArray(histResult) ? histResult : [], + evol: Array.isArray(evolResult) ? evolResult : [], + }; +} + export default function Historique() { const [searchParams] = useSearchParams(); + + /* Figé au montage : sert à la fois de valeur initiale des filtres et de + paramètres du premier chargement, sans dépendance instable. */ + const [valeursInitiales] = useState(() => ({ + monitoring: searchParams.get('id') || '', + ...periodeParDefaut(), + })); + const [monitorings, setMonitorings] = useState([]); - const [selectedMonitoring, setSelectedMonitoring] = useState(searchParams.get('id') || ''); - const [dateDebut, setDateDebut] = useState(''); - const [dateFin, setDateFin] = useState(''); + const [selectedMonitoring, setSelectedMonitoring] = useState(valeursInitiales.monitoring); + const [dateDebut, setDateDebut] = useState(valeursInitiales.debut); + const [dateFin, setDateFin] = useState(valeursInitiales.fin); const [historiqueData, setHistoriqueData] = useState([]); const [evolutionData, setEvolutionData] = useState([]); - const [loading, setLoading] = useState(false); + /* Un chargement est lancé dès le montage : la page démarre donc en attente. */ + const [loading, setLoading] = useState(true); const [erreur, setErreur] = useState(''); - /* Set default dates to last 5 days */ - useEffect(() => { - const today = new Date(); - const fiveDaysAgo = new Date(today); - fiveDaysAgo.setDate(today.getDate() - 5); - setDateDebut(fiveDaysAgo.toISOString().split('T')[0]); - setDateFin(today.toISOString().split('T')[0]); - }, []); useEffect(() => { async function loadMonitorings() { try { @@ -41,6 +82,31 @@ export default function Historique() { loadMonitorings(); }, []); + /* Premier affichage : on charge la période par défaut, pour que la page ne + s'ouvre pas sur un écran vide en attente d'un clic. Le drapeau `annule` + évite d'écrire dans un composant déjà démonté si l'utilisateur quitte la + page pendant la requête. */ + useEffect(() => { + let annule = false; + + async function chargerAuMontage() { + const { monitoring, debut, fin } = valeursInitiales; + try { + const { hist, evol } = await recupererHistorique(monitoring, debut, fin); + if (annule) return; + setHistoriqueData(hist); + setEvolutionData(evol); + } catch (e) { + if (!annule) setErreur(e.message || 'Erreur lors du chargement de l\'historique.'); + } finally { + if (!annule) setLoading(false); + } + } + + chargerAuMontage(); + return () => { annule = true; }; + }, [valeursInitiales]); + async function handleAfficher() { if (!dateDebut || !dateFin) { setErreur('Veuillez sélectionner des dates.'); @@ -50,25 +116,7 @@ export default function Historique() { setLoading(true); setErreur(''); try { - let hist = []; - let evol = []; - - if (selectedMonitoring) { - const [histResult, evolResult] = await Promise.all([ - getHistorique(selectedMonitoring, dateDebut, dateFin), - getHistoriqueEvolution(selectedMonitoring, dateDebut, dateFin) - ]); - hist = Array.isArray(histResult) ? histResult : []; - evol = Array.isArray(evolResult) ? evolResult : []; - } else { - const [histResult, evolResult] = await Promise.all([ - getHistorique(undefined, dateDebut, dateFin), - getEvolutionGlobal(dateDebut, dateFin) - ]); - hist = Array.isArray(histResult) ? histResult : []; - evol = Array.isArray(evolResult) ? evolResult : []; - } - + const { hist, evol } = await recupererHistorique(selectedMonitoring, dateDebut, dateFin); setHistoriqueData(hist); setEvolutionData(evol); } catch (e) { @@ -189,9 +237,9 @@ export default function Historique() { emptyMessage="Aucune donnée historique." /> - ) : selectedMonitoring && dateDebut && dateFin && !loading ? ( + ) : (

Aucune donnée trouvée pour cette période.

- ) : null} + )} ); } \ No newline at end of file