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/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 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');