3 Commits
Author SHA1 Message Date
Antho 270283e96d merge: intègre feature/dashboard-filtres-et-historique dans dev
Build & Deploy / build (push) Successful in 36s
- feat(dashboard): filtres services/catégories restreints aux résultats
- fix(historique): période par défaut du 1er janvier à aujourd'hui
2026-08-15 13:35:50 +02:00
Antho d27ff11ea2 fix(historique): période par défaut du 1er janvier à aujourd'hui
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.
2026-08-15 13:28:43 +02:00
Antho 25a31feec4 feat(dashboard): filtres services/catégories restreints aux résultats
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.
2026-08-15 13:28:36 +02:00
3 changed files with 129 additions and 44 deletions
+35 -10
View File
@@ -1,8 +1,8 @@
/* Page principale du dashboard : KPI cards + grille de monitorings */ /* 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 { 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 MonitoringCard from '../../components/common/MonitoringCard';
import LoadingSpinner from '../../components/common/LoadingSpinner'; import LoadingSpinner from '../../components/common/LoadingSpinner';
import ErrorMessage from '../../components/common/ErrorMessage'; import ErrorMessage from '../../components/common/ErrorMessage';
@@ -22,19 +22,22 @@ export default function Dashboard() {
const [summary, setSummary] = useState(null); const [summary, setSummary] = useState(null);
const [monitorings, setMonitorings] = useState([]); const [monitorings, setMonitorings] = useState([]);
const [services, setServices] = useState([]); const [services, setServices] = useState([]);
const [categories, setCategories] = useState([]); const [combinaisons, setCombinaisons] = useState([]);
const [filtreService, setFiltreService] = useState(''); const [filtreService, setFiltreService] = useState('');
const [filtreCategorie, setFiltreCategorie] = useState(''); const [filtreCategorie, setFiltreCategorie] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [erreur, setErreur] = useState(''); 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(() => { useEffect(() => {
async function loadFiltres() { async function loadFiltres() {
try { try {
const [svcs, cats] = await Promise.all([getServices(), getCategories()]); const filtres = await getDashboardFiltres();
setServices(svcs); setServices(filtres.services);
setCategories(cats); setCombinaisons(filtres.combinaisons);
} catch { } catch {
/* Non bloquant, les filtres restent vides */ /* Non bloquant, les filtres restent vides */
} }
@@ -42,6 +45,28 @@ export default function Dashboard() {
loadFiltres(); 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 */ /* Chargement des données dashboard */
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
setLoading(true); setLoading(true);
@@ -105,11 +130,11 @@ export default function Dashboard() {
<select <select
className={styles.select} className={styles.select}
value={filtreService} value={filtreService}
onChange={e => setFiltreService(e.target.value)} onChange={e => handleServiceChange(e.target.value)}
> >
<option value="">Tous les services</option> <option value="">Tous les services</option>
{services.map(s => ( {services.map(s => (
<option key={s.id_service} value={s.nom_service}>{s.nom_service}</option> <option key={s} value={s}>{s}</option>
))} ))}
</select> </select>
<select <select
@@ -119,7 +144,7 @@ export default function Dashboard() {
> >
<option value="">Toutes les catégories</option> <option value="">Toutes les catégories</option>
{categories.map(c => ( {categories.map(c => (
<option key={c.id_categorie} value={c.intitule_categorie}>{c.intitule_categorie}</option> <option key={c} value={c}>{c}</option>
))} ))}
</select> </select>
</div> </div>
@@ -2,7 +2,7 @@
// Page Historique : filtres + graphique ligne + tableau résumé // Page Historique : filtres + graphique ligne + tableau résumé
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom'; 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 { getMonitorings, getHistorique, getHistoriqueEvolution, getEvolutionGlobal, exportToCSV } from '../../services/api';
import DataTable from '../../components/common/DataTable'; import DataTable from '../../components/common/DataTable';
import LoadingSpinner from '../../components/common/LoadingSpinner'; import LoadingSpinner from '../../components/common/LoadingSpinner';
@@ -10,25 +10,66 @@ import ErrorMessage from '../../components/common/ErrorMessage';
import Button from '../../components/common/Button'; import Button from '../../components/common/Button';
import styles from './Historique.module.css'; import styles from './Historique.module.css';
/* Formate une date au format attendu par <input type="date"> (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() { export default function Historique() {
const [searchParams] = useSearchParams(); 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 [monitorings, setMonitorings] = useState([]);
const [selectedMonitoring, setSelectedMonitoring] = useState(searchParams.get('id') || ''); const [selectedMonitoring, setSelectedMonitoring] = useState(valeursInitiales.monitoring);
const [dateDebut, setDateDebut] = useState(''); const [dateDebut, setDateDebut] = useState(valeursInitiales.debut);
const [dateFin, setDateFin] = useState(''); const [dateFin, setDateFin] = useState(valeursInitiales.fin);
const [historiqueData, setHistoriqueData] = useState([]); const [historiqueData, setHistoriqueData] = useState([]);
const [evolutionData, setEvolutionData] = 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(''); 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(() => { useEffect(() => {
async function loadMonitorings() { async function loadMonitorings() {
try { try {
@@ -41,6 +82,31 @@ export default function Historique() {
loadMonitorings(); 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() { async function handleAfficher() {
if (!dateDebut || !dateFin) { if (!dateDebut || !dateFin) {
setErreur('Veuillez sélectionner des dates.'); setErreur('Veuillez sélectionner des dates.');
@@ -50,25 +116,7 @@ export default function Historique() {
setLoading(true); setLoading(true);
setErreur(''); setErreur('');
try { try {
let hist = []; const { hist, evol } = await recupererHistorique(selectedMonitoring, dateDebut, dateFin);
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 : [];
}
setHistoriqueData(hist); setHistoriqueData(hist);
setEvolutionData(evol); setEvolutionData(evol);
} catch (e) { } catch (e) {
@@ -189,9 +237,9 @@ export default function Historique() {
emptyMessage="Aucune donnée historique." emptyMessage="Aucune donnée historique."
/> />
</> </>
) : selectedMonitoring && dateDebut && dateFin && !loading ? ( ) : (
<p className={styles.empty}>Aucune donnée trouvée pour cette période.</p> <p className={styles.empty}>Aucune donnée trouvée pour cette période.</p>
) : null} )}
</div> </div>
); );
} }
+12
View File
@@ -245,6 +245,18 @@ export async function getDashboard(service, categorie) {
})) : data; })) : 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 */ /* Récupère le résumé global KPI du dashboard */
export async function getDashboardSummary() { export async function getDashboardSummary() {
const data = await fetchApi('/dashboard/summary'); const data = await fetchApi('/dashboard/summary');