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 */
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() {
<select
className={styles.select}
value={filtreService}
onChange={e => setFiltreService(e.target.value)}
onChange={e => handleServiceChange(e.target.value)}
>
<option value="">Tous les services</option>
{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
@@ -119,7 +144,7 @@ export default function Dashboard() {
>
<option value="">Toutes les catégories</option>
{categories.map(c => (
<option key={c.id_categorie} value={c.intitule_categorie}>{c.intitule_categorie}</option>
<option key={c} value={c}>{c}</option>
))}
</select>
</div>
@@ -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 <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() {
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 ? (
) : (
<p className={styles.empty}>Aucune donnée trouvée pour cette période.</p>
) : null}
)}
</div>
);
}
+12
View File
@@ -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');