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
This commit is contained in:
2026-08-15 13:35:50 +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');