merge: intègre feature/dashboard-filtres-et-historique dans dev
Build & Deploy / build (push) Successful in 36s
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user