Compare commits
3
Commits
e741065e14
...
270283e96d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
270283e96d | ||
|
|
d27ff11ea2 | ||
|
|
25a31feec4 |
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user