From 8b8121ca1ca58f4752859d9bd7eeaf7f6dcdf207 Mon Sep 17 00:00:00 2001 From: Anthony Coyaud Date: Sat, 15 Aug 2026 14:29:09 +0200 Subject: [PATCH 1/4] =?UTF-8?q?refactor(front):=20centralise=20la=20r?= =?UTF-8?q?=C3=A8gle=20de=20statut=20et=20les=20colonnes=20dupliqu=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le seuil de 10 erreurs qui fait basculer un monitoring en « Critique » était réécrit dans trois fichiers (MonitoringCard, MonitoringDetail, VueConso), la version de VueConso ne renvoyant qu'une couleur. Faire évoluer ce seuil imposait de corriger trois endroits sans en oublier. Il vit maintenant dans utils/statutMonitoring.js, avec la constante SEUIL_ATTENTION nommée. Les colonnes du tableau de monitorings étaient également identiques entre VueConso et ServiceMonitorings : extraites dans utils/monitoringColumns.jsx. --- .../src/components/common/MonitoringCard.jsx | 10 ++----- .../MonitoringDetail/MonitoringDetail.jsx | 10 ++----- .../ServiceMonitorings/ServiceMonitorings.jsx | 18 ++---------- DataSentinel/src/pages/VueConso/VueConso.jsx | 29 +++---------------- DataSentinel/src/utils/monitoringColumns.jsx | 24 +++++++++++++++ DataSentinel/src/utils/statutMonitoring.js | 16 ++++++++++ 6 files changed, 50 insertions(+), 57 deletions(-) create mode 100644 DataSentinel/src/utils/monitoringColumns.jsx create mode 100644 DataSentinel/src/utils/statutMonitoring.js diff --git a/DataSentinel/src/components/common/MonitoringCard.jsx b/DataSentinel/src/components/common/MonitoringCard.jsx index 2c43a36..d322a7f 100644 --- a/DataSentinel/src/components/common/MonitoringCard.jsx +++ b/DataSentinel/src/components/common/MonitoringCard.jsx @@ -2,20 +2,14 @@ import { useNavigate } from 'react-router-dom'; import Badge from './Badge'; +import { getStatutMonitoring } from '../../utils/statutMonitoring'; import styles from './MonitoringCard.module.css'; -/* Détermine le statut visuel selon le nombre d'erreurs */ -function getStatut(nbErreurs) { - if (nbErreurs === 0) return { label: 'OK', variant: 'ok', color: 'var(--color-ok)' }; - if (nbErreurs < 10) return { label: 'Attention', variant: 'warning', color: 'var(--color-warning)' }; - return { label: 'Critique', variant: 'critical', color: 'var(--color-primary)' }; -} - export default function MonitoringCard({ monitoring }) { const navigate = useNavigate(); const nbErreurs = monitoring.nb_erreurs ?? monitoring.count ?? 0; const monitoringId = monitoring.id ?? monitoring.id_monito; - const statut = getStatut(nbErreurs); + const statut = getStatutMonitoring(nbErreurs); /* Calcul du pourcentage de la barre de progression (max 100%) */ const progressPct = Math.min((nbErreurs / 20) * 100, 100); diff --git a/DataSentinel/src/pages/MonitoringDetail/MonitoringDetail.jsx b/DataSentinel/src/pages/MonitoringDetail/MonitoringDetail.jsx index 7e66990..097f41e 100644 --- a/DataSentinel/src/pages/MonitoringDetail/MonitoringDetail.jsx +++ b/DataSentinel/src/pages/MonitoringDetail/MonitoringDetail.jsx @@ -9,15 +9,9 @@ import LoadingSpinner from '../../components/common/LoadingSpinner'; import ErrorMessage from '../../components/common/ErrorMessage'; import Badge from '../../components/common/Badge'; import Button from '../../components/common/Button'; +import { getStatutMonitoring } from '../../utils/statutMonitoring'; import styles from './MonitoringDetail.module.css'; -/* Détermine le statut selon le nombre d'erreurs */ -function getStatut(nbErreurs) { - if (nbErreurs === 0) return { label: 'OK', variant: 'ok' }; - if (nbErreurs < 10) return { label: 'Attention', variant: 'warning' }; - return { label: 'Critique', variant: 'critical' }; -} - export default function MonitoringDetail() { const { id } = useParams(); const navigate = useNavigate(); @@ -72,7 +66,7 @@ export default function MonitoringDetail() { if (loading) return ; if (erreur) return ; - const statut = getStatut(count); + const statut = getStatutMonitoring(count); return (
diff --git a/DataSentinel/src/pages/ServiceMonitorings/ServiceMonitorings.jsx b/DataSentinel/src/pages/ServiceMonitorings/ServiceMonitorings.jsx index 890a804..25030ae 100644 --- a/DataSentinel/src/pages/ServiceMonitorings/ServiceMonitorings.jsx +++ b/DataSentinel/src/pages/ServiceMonitorings/ServiceMonitorings.jsx @@ -7,6 +7,7 @@ import DataTable from '../../components/common/DataTable'; import LoadingSpinner from '../../components/common/LoadingSpinner'; import ErrorMessage from '../../components/common/ErrorMessage'; import Button from '../../components/common/Button'; +import { buildMonitoringColumns } from '../../utils/monitoringColumns'; import styles from './ServiceMonitorings.module.css'; export default function ServiceMonitorings({ serviceLabel, serviceName }) { @@ -34,22 +35,7 @@ export default function ServiceMonitorings({ serviceLabel, serviceName }) { if (loading) return ; if (erreur) return ; - const columns = [ - { key: 'nom', label: 'Monitoring' }, - { key: 'service', label: 'Service' }, - { key: 'categorie', label: 'Catégorie' }, - { key: 'bdd_source', label: 'Source' }, - { key: 'nb_erreurs', label: 'Erreurs', render: (val) => val ?? 0 }, - { - key: 'actions', - label: 'Actions', - render: (val, row) => ( - - ) - } - ]; + const columns = buildMonitoringColumns(navigate); return (
diff --git a/DataSentinel/src/pages/VueConso/VueConso.jsx b/DataSentinel/src/pages/VueConso/VueConso.jsx index da8253b..2e28186 100644 --- a/DataSentinel/src/pages/VueConso/VueConso.jsx +++ b/DataSentinel/src/pages/VueConso/VueConso.jsx @@ -9,15 +9,10 @@ import DataTable from '../../components/common/DataTable'; import LoadingSpinner from '../../components/common/LoadingSpinner'; import ErrorMessage from '../../components/common/ErrorMessage'; import Button from '../../components/common/Button'; +import { getStatutMonitoring } from '../../utils/statutMonitoring'; +import { buildMonitoringColumns } from '../../utils/monitoringColumns'; import styles from './VueConso.module.css'; -/* Détermine la couleur selon le statut */ -function getStatusColor(nbErreurs) { - if (nbErreurs === 0) return 'var(--color-ok)'; - if (nbErreurs < 10) return 'var(--color-warning)'; - return 'var(--color-primary)'; -} - export default function VueConso() { const navigate = useNavigate(); const [data, setData] = useState([]); @@ -47,26 +42,10 @@ export default function VueConso() { const chartData = data.map(item => ({ name: item.nom || item.nom_monito || item.libelle || 'Monitoring', erreurs: item.nb_erreurs ?? item.count ?? 0, - fill: getStatusColor(item.nb_erreurs ?? item.count ?? 0) + fill: getStatutMonitoring(item.nb_erreurs ?? item.count ?? 0).color })); - /* Colonnes du tableau */ - const columns = [ - { key: 'nom', label: 'Monitoring' }, - { key: 'service', label: 'Service' }, - { key: 'categorie', label: 'Catégorie' }, - { key: 'bdd_source', label: 'Source' }, - { key: 'nb_erreurs', label: 'Erreurs', render: (val) => val ?? 0 }, - { - key: 'actions', - label: 'Actions', - render: (val, row) => ( - - ) - } - ]; + const columns = buildMonitoringColumns(navigate); return (
diff --git a/DataSentinel/src/utils/monitoringColumns.jsx b/DataSentinel/src/utils/monitoringColumns.jsx new file mode 100644 index 0000000..82984bb --- /dev/null +++ b/DataSentinel/src/utils/monitoringColumns.jsx @@ -0,0 +1,24 @@ +import Button from '../components/common/Button'; + +/* Colonnes du tableau de monitorings partagées par VueConso et + ServiceMonitorings (Contrat/Fournisseur) : même jeu de données + (GET /dashboard), même tableau. Centralisé pour qu'une évolution des + colonnes (ajout, renommage) ne soit faite qu'à un seul endroit. */ +export function buildMonitoringColumns(navigate) { + return [ + { key: 'nom', label: 'Monitoring' }, + { key: 'service', label: 'Service' }, + { key: 'categorie', label: 'Catégorie' }, + { key: 'bdd_source', label: 'Source' }, + { key: 'nb_erreurs', label: 'Erreurs', render: (val) => val ?? 0 }, + { + key: 'actions', + label: 'Actions', + render: (val, row) => ( + + ) + } + ]; +} diff --git a/DataSentinel/src/utils/statutMonitoring.js b/DataSentinel/src/utils/statutMonitoring.js new file mode 100644 index 0000000..9c4de4c --- /dev/null +++ b/DataSentinel/src/utils/statutMonitoring.js @@ -0,0 +1,16 @@ +/* Règle de statut partagée : un monitoring est "OK" sans erreur, "Attention" + en-dessous du seuil, "Critique" au-delà. Centralisé ici pour que le seuil + ne soit corrigé qu'à un seul endroit (il était dupliqué dans MonitoringCard, + MonitoringDetail et VueConso, avec le risque que l'une des copies diverge). */ + +export const SEUIL_ATTENTION = 10; + +export function getStatutMonitoring(nbErreurs) { + if (nbErreurs === 0) { + return { label: 'OK', variant: 'ok', color: 'var(--color-ok)' }; + } + if (nbErreurs < SEUIL_ATTENTION) { + return { label: 'Attention', variant: 'warning', color: 'var(--color-warning)' }; + } + return { label: 'Critique', variant: 'critical', color: 'var(--color-primary)' }; +} From ddd0197130838d7df859bb4eddecb43c989b9636 Mon Sep 17 00:00:00 2001 From: Anthony Coyaud Date: Sat, 15 Aug 2026 14:29:18 +0200 Subject: [PATCH 2/4] =?UTF-8?q?chore(front):=20extrait=20KpiCard,=20unifie?= =?UTF-8?q?=20la=20cl=C3=A9=20de=20session,=20retire=20du=20code=20mort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KpiCard était déclaré dans Dashboard.jsx : sorti dans son propre fichier avec ses styles, les règles CSS devenues orphelines sont supprimées de Dashboard.module.css. - La clé localStorage 'ds_user' était définie séparément dans api.js (USER_KEY) et AuthContext.jsx (STORAGE_KEY). AuthContext importe désormais la constante exportée par api.js : un renommage ne peut plus casser silencieusement la restauration de session. - hooks/useApi.js n'était importé nulle part : supprimé. --- .../src/components/common/KpiCard.jsx | 10 ++++++ .../src/components/common/KpiCard.module.css | 28 ++++++++++++++++ DataSentinel/src/context/AuthContext.jsx | 9 +++-- DataSentinel/src/hooks/useApi.js | 33 ------------------- .../src/pages/Dashboard/Dashboard.jsx | 11 +------ .../src/pages/Dashboard/Dashboard.module.css | 27 --------------- 6 files changed, 43 insertions(+), 75 deletions(-) create mode 100644 DataSentinel/src/components/common/KpiCard.jsx create mode 100644 DataSentinel/src/components/common/KpiCard.module.css delete mode 100644 DataSentinel/src/hooks/useApi.js diff --git a/DataSentinel/src/components/common/KpiCard.jsx b/DataSentinel/src/components/common/KpiCard.jsx new file mode 100644 index 0000000..c12f4f1 --- /dev/null +++ b/DataSentinel/src/components/common/KpiCard.jsx @@ -0,0 +1,10 @@ +import styles from './KpiCard.module.css'; + +export default function KpiCard({ label, value, color }) { + return ( +
+ {value} + {label} +
+ ); +} diff --git a/DataSentinel/src/components/common/KpiCard.module.css b/DataSentinel/src/components/common/KpiCard.module.css new file mode 100644 index 0000000..cf8ef75 --- /dev/null +++ b/DataSentinel/src/components/common/KpiCard.module.css @@ -0,0 +1,28 @@ +.kpiCard { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + background: var(--color-white); + border-radius: var(--radius); + box-shadow: var(--shadow); +} + +.kpiValue { + font-size: 36px; + font-weight: 700; + line-height: 1; + margin-bottom: 6px; +} + +.kpiLabel { + font-size: 12px; + color: var(--color-muted, #888); + text-align: center; +} + +@media (max-width: 768px) { + .kpiValue { + font-size: 28px; + } +} diff --git a/DataSentinel/src/context/AuthContext.jsx b/DataSentinel/src/context/AuthContext.jsx index 931718e..aac8a26 100644 --- a/DataSentinel/src/context/AuthContext.jsx +++ b/DataSentinel/src/context/AuthContext.jsx @@ -2,16 +2,15 @@ import { createContext, useContext, useState } from 'react'; import * as api from '../services/api'; +import { USER_KEY } from '../services/api'; const AuthContext = createContext(null); -const STORAGE_KEY = 'ds_user'; - /* Fournisseur du contexte d'authentification */ export function AuthProvider({ children }) { const [user, setUser] = useState(() => { /* Restauration de la session depuis le localStorage au montage */ - const stored = localStorage.getItem(STORAGE_KEY); + const stored = localStorage.getItem(USER_KEY); return stored ? JSON.parse(stored) : null; }); @@ -19,7 +18,7 @@ export function AuthProvider({ children }) { async function login(username, password) { const connectedUser = await api.login(username, password); setUser(connectedUser); - localStorage.setItem(STORAGE_KEY, JSON.stringify(connectedUser)); + localStorage.setItem(USER_KEY, JSON.stringify(connectedUser)); return connectedUser; } @@ -27,7 +26,7 @@ export function AuthProvider({ children }) { function logout() { api.clearToken(); setUser(null); - localStorage.removeItem(STORAGE_KEY); + localStorage.removeItem(USER_KEY); } /* Booléen pratique pour savoir si l'utilisateur est connecté */ diff --git a/DataSentinel/src/hooks/useApi.js b/DataSentinel/src/hooks/useApi.js deleted file mode 100644 index 7239bb7..0000000 --- a/DataSentinel/src/hooks/useApi.js +++ /dev/null @@ -1,33 +0,0 @@ -/* Hook générique pour les appels API : retourne { data, loading, error, refetch } */ - -import { useState, useEffect, useCallback } from 'react'; - -/** - * @param {Function} fetchFn - Fonction async à appeler (depuis api.js) - * @param {Array} deps - Dépendances qui déclenchent un re-fetch (optionnel) - */ -export function useApi(fetchFn, deps = []) { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetch = useCallback(async () => { - setLoading(true); - setError(null); - try { - const result = await fetchFn(); - setData(result); - } catch (err) { - setError(err.message || 'Une erreur est survenue.'); - } finally { - setLoading(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, deps); - - useEffect(() => { - fetch(); - }, [fetch]); - - return { data, loading, error, refetch: fetch }; -} diff --git a/DataSentinel/src/pages/Dashboard/Dashboard.jsx b/DataSentinel/src/pages/Dashboard/Dashboard.jsx index 79c30e2..979484a 100644 --- a/DataSentinel/src/pages/Dashboard/Dashboard.jsx +++ b/DataSentinel/src/pages/Dashboard/Dashboard.jsx @@ -4,20 +4,11 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { RefreshCw } from 'lucide-react'; import { getDashboard, getDashboardSummary, getDashboardFiltres } from '../../services/api'; import MonitoringCard from '../../components/common/MonitoringCard'; +import KpiCard from '../../components/common/KpiCard'; import LoadingSpinner from '../../components/common/LoadingSpinner'; import ErrorMessage from '../../components/common/ErrorMessage'; import styles from './Dashboard.module.css'; -/* Carte KPI affichant un indicateur clé */ -function KpiCard({ label, value, color }) { - return ( -
- {value} - {label} -
- ); -} - export default function Dashboard() { const [summary, setSummary] = useState(null); const [monitorings, setMonitorings] = useState([]); diff --git a/DataSentinel/src/pages/Dashboard/Dashboard.module.css b/DataSentinel/src/pages/Dashboard/Dashboard.module.css index 4469834..7113e81 100644 --- a/DataSentinel/src/pages/Dashboard/Dashboard.module.css +++ b/DataSentinel/src/pages/Dashboard/Dashboard.module.css @@ -55,29 +55,6 @@ margin-bottom: 32px; } -.kpiCard { - display: flex; - flex-direction: column; - align-items: center; - padding: 20px; - background: var(--color-white); - border-radius: var(--radius); - box-shadow: var(--shadow); -} - -.kpiValue { - font-size: 36px; - font-weight: 700; - line-height: 1; - margin-bottom: 6px; -} - -.kpiLabel { - font-size: 12px; - color: var(--color-muted, #888); - text-align: center; -} - .filtres { display: flex; gap: 12px; @@ -131,10 +108,6 @@ gap: 10px; } - .kpiValue { - font-size: 28px; - } - .filtres { flex-direction: column; } From 82a1b93211faa01e656e19f41ca220fa9ff68a77 Mon Sep 17 00:00:00 2001 From: Anthony Coyaud Date: Sat, 15 Aug 2026 14:29:29 +0200 Subject: [PATCH 3/4] =?UTF-8?q?feat(front):=20rend=20l'=C3=A9cran=20d'admi?= =?UTF-8?q?nistration=20r=C3=A9ellement=20fonctionnel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les boutons Modifier et Désactiver de la page Administration n'appelaient rien : handleDelete se contentait d'un console.log et le bouton Enregistrer de la modale ne faisait que la refermer. L'écran donnait l'illusion d'un CRUD. Branche les 4 onglets sur les endpoints POST/PUT/DELETE ajoutés côté API, avec confirmation avant suppression, rechargement après enregistrement et remontée des erreurs de l'API (dont le refus 409 quand des enregistrements sont rattachés). Les 4 blocs de colonnes de tableau, quasi identiques d'un onglet à l'autre, laissent place à une description déclarative (referentielsConfig.js) : un seul tableau et un seul formulaire générique (ReferentielForm) servent les 4 référentiels. Les colonnes qui portaient un identifiant brut affichent maintenant le libellé. La modale, jusqu'ici déclarée dans Admin.jsx, devient un composant commun réutilisable. --- DataSentinel/src/components/common/Modal.jsx | 19 ++ .../src/components/common/Modal.module.css | 49 +++ DataSentinel/src/pages/Admin/Admin.jsx | 305 ++++++++---------- DataSentinel/src/pages/Admin/Admin.module.css | 62 ++-- .../src/pages/Admin/ReferentielForm.jsx | 91 ++++++ .../src/pages/Admin/referentielsConfig.js | 89 +++++ DataSentinel/src/services/api.js | 58 +++- 7 files changed, 455 insertions(+), 218 deletions(-) create mode 100644 DataSentinel/src/components/common/Modal.jsx create mode 100644 DataSentinel/src/components/common/Modal.module.css create mode 100644 DataSentinel/src/pages/Admin/ReferentielForm.jsx create mode 100644 DataSentinel/src/pages/Admin/referentielsConfig.js diff --git a/DataSentinel/src/components/common/Modal.jsx b/DataSentinel/src/components/common/Modal.jsx new file mode 100644 index 0000000..bb47a8a --- /dev/null +++ b/DataSentinel/src/components/common/Modal.jsx @@ -0,0 +1,19 @@ +import styles from './Modal.module.css'; + +export default function Modal({ isOpen, onClose, title, children }) { + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()}> +
+

{title}

+ +
+
+ {children} +
+
+
+ ); +} diff --git a/DataSentinel/src/components/common/Modal.module.css b/DataSentinel/src/components/common/Modal.module.css new file mode 100644 index 0000000..72d0cac --- /dev/null +++ b/DataSentinel/src/components/common/Modal.module.css @@ -0,0 +1,49 @@ +.modalOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal { + background-color: var(--color-white); + border-radius: var(--radius); + border-top: 4px solid var(--color-primary); + width: 90%; + max-width: 500px; + max-height: 80vh; + overflow-y: auto; +} + +.modalHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid var(--color-gray-border); +} + +.modalHeader h2 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: var(--color-text); +} + +.closeBtn { + background: none; + border: none; + font-size: 24px; + color: var(--color-text-secondary); + cursor: pointer; +} + +.modalBody { + padding: 24px; +} diff --git a/DataSentinel/src/pages/Admin/Admin.jsx b/DataSentinel/src/pages/Admin/Admin.jsx index 1f8ab4b..cd0d389 100644 --- a/DataSentinel/src/pages/Admin/Admin.jsx +++ b/DataSentinel/src/pages/Admin/Admin.jsx @@ -1,230 +1,187 @@ // src/pages/Admin/Admin.jsx -// Page Administration : onglets pour gérer monitorings, services, catégories, contacts -import { useState, useEffect } from 'react'; +// Page Administration : CRUD sur les référentiels (monitorings, services, +// catégories, contacts). Réservée au rôle Admin côté API. +import { useState, useEffect, useCallback } from 'react'; import { Plus, Edit, Trash2 } from 'lucide-react'; import { getMonitorings, getServices, getCategories, getContacts } from '../../services/api'; import DataTable from '../../components/common/DataTable'; import LoadingSpinner from '../../components/common/LoadingSpinner'; import ErrorMessage from '../../components/common/ErrorMessage'; import Button from '../../components/common/Button'; +import Modal from '../../components/common/Modal'; +import ReferentielForm from './ReferentielForm'; +import { REFERENTIELS, LIBELLES_REFERENTIEL } from './referentielsConfig'; import styles from './Admin.module.css'; -/* Modale générique pour ajouter/modifier */ -function Modal({ isOpen, onClose, title, children }) { - if (!isOpen) return null; - - return ( -
-
e.stopPropagation()}> -
-

{title}

- -
-
- {children} -
-
-
- ); +async function chargerReferentiels() { + const [monitorings, services, categories, contacts] = await Promise.all([ + getMonitorings(), + getServices(), + getCategories(), + getContacts(), + ]); + return { + monitorings: Array.isArray(monitorings) ? monitorings : [], + services: Array.isArray(services) ? services : [], + categories: Array.isArray(categories) ? categories : [], + contacts: Array.isArray(contacts) ? contacts : [], + }; } export default function Admin() { - const [activeTab, setActiveTab] = useState('monitorings'); - const [data, setData] = useState({}); + const [ongletActif, setOngletActif] = useState(REFERENTIELS[0].key); + const [donnees, setDonnees] = useState({}); const [loading, setLoading] = useState(true); const [erreur, setErreur] = useState(''); - const [modalOpen, setModalOpen] = useState(false); - const [editingItem, setEditingItem] = useState(null); + const [modaleOuverte, setModaleOuverte] = useState(false); + const [elementEnEdition, setElementEnEdition] = useState(null); - useEffect(() => { - async function loadData() { - setLoading(true); - setErreur(''); - try { - const [monitorings, services, categories, contacts] = await Promise.all([ - getMonitorings(), - getServices(), - getCategories(), - getContacts() - ]); - setData({ - monitorings: Array.isArray(monitorings) ? monitorings : [], - services: Array.isArray(services) ? services : [], - categories: Array.isArray(categories) ? categories : [], - contacts: Array.isArray(contacts) ? contacts : [] - }); - } catch (e) { - setErreur(e.message || 'Erreur lors du chargement des données administratives.'); - } finally { - setLoading(false); - } + const recharger = useCallback(async () => { + setErreur(''); + try { + setDonnees(await chargerReferentiels()); + } catch (e) { + setErreur(e.message || 'Erreur lors du chargement des données administratives.'); } - loadData(); }, []); - function handleAdd() { - setEditingItem(null); - setModalOpen(true); + useEffect(() => { + let annule = false; + async function chargerAuMontage() { + try { + const resultat = await chargerReferentiels(); + if (!annule) setDonnees(resultat); + } catch (e) { + if (!annule) setErreur(e.message || 'Erreur lors du chargement des données administratives.'); + } finally { + if (!annule) setLoading(false); + } + } + chargerAuMontage(); + return () => { annule = true; }; + }, []); + + const referentiel = REFERENTIELS.find(r => r.key === ongletActif); + + function ouvrirCreation() { + setElementEnEdition(null); + setModaleOuverte(true); } - function handleEdit(item) { - setEditingItem(item); - setModalOpen(true); + function ouvrirEdition(element) { + setElementEnEdition(element); + setModaleOuverte(true); } - function handleDelete(item) { - // Simulation de suppression (pas d'API pour ça) - console.log('Supprimer', item); + async function handleEnregistrer(corps) { + if (elementEnEdition) { + await referentiel.modifier(elementEnEdition[referentiel.idField], corps); + } else { + await referentiel.creer(corps); + } + setModaleOuverte(false); + await recharger(); } - const tabs = [ - { key: 'monitorings', label: 'Monitorings' }, - { key: 'services', label: 'Services' }, - { key: 'categories', label: 'Catégories' }, - { key: 'contacts', label: 'Contacts' } + async function handleSupprimer(element) { + const action = referentiel.suppressionEstDesactivation ? 'Désactiver' : 'Supprimer'; + const nom = element[referentiel.colonnes[0].key]; + if (!window.confirm(`${action} « ${nom} » ?`)) return; + + setErreur(''); + try { + await referentiel.supprimer(element[referentiel.idField]); + await recharger(); + } catch (e) { + setErreur(e.message || 'La suppression a échoué.'); + } + } + + /* Affiche le libellé d'un référentiel lié plutôt que son identifiant brut. */ + function libelleLie(nomReferentiel, id) { + const { idField, labelField } = LIBELLES_REFERENTIEL[nomReferentiel]; + const trouve = (donnees[nomReferentiel] || []).find(item => item[idField] === id); + return trouve ? trouve[labelField] : id; + } + + const colonnes = [ + ...referentiel.colonnes.map(colonne => ({ + key: colonne.key, + label: colonne.label, + render: colonne.referentiel + ? val => libelleLie(colonne.referentiel, val) + : undefined, + })), + { + key: 'actions', + label: 'Actions', + render: (val, row) => ( +
+ + +
+ ), + }, ]; - const columns = { - monitorings: [ - { key: 'monito_intitule', label: 'Nom' }, - { key: 'id_service', label: 'Service' }, - { key: 'id_categorie', label: 'Catégorie' }, - { key: 'bdd_source', label: 'Source' }, - { key: 'table_source', label: 'Table' }, - { - key: 'actions', - label: 'Actions', - render: (val, row) => ( -
- - -
- ) - } - ], - services: [ - { key: 'nom_service', label: 'Nom' }, - { - key: 'actions', - label: 'Actions', - render: (val, row) => ( -
- - -
- ) - } - ], - categories: [ - { key: 'intitule_categorie', label: 'Nom' }, - { - key: 'actions', - label: 'Actions', - render: (val, row) => ( -
- - -
- ) - } - ], - contacts: [ - { key: 'intitule_contact', label: 'Intitulé' }, - { key: 'nom', label: 'Nom' }, - { key: 'prenom', label: 'Prénom' }, - { key: 'mail', label: 'Email' }, - { key: 'id_service', label: 'Service' }, - { - key: 'actions', - label: 'Actions', - render: (val, row) => ( -
- - -
- ) - } - ] - }; - if (loading) return ; - if (erreur) return ; return (

Administration

- {/* Onglets */}
- {tabs.map(tab => ( + {REFERENTIELS.map(item => ( ))}
- {/* Contenu de l'onglet actif */} + {erreur && } +
-

{tabs.find(t => t.key === activeTab)?.label}

-
- {/* Modale */} setModalOpen(false)} - title={`${editingItem ? 'Modifier' : 'Ajouter'} ${tabs.find(t => t.key === activeTab)?.label.slice(0, -1)}`} + isOpen={modaleOuverte} + onClose={() => setModaleOuverte(false)} + title={`${elementEnEdition ? 'Modifier' : 'Ajouter'} ${referentiel.singulier}`} > -

Formulaire à implémenter selon les besoins.

-
- - -
+ setModaleOuverte(false)} + />
); -} \ No newline at end of file +} diff --git a/DataSentinel/src/pages/Admin/Admin.module.css b/DataSentinel/src/pages/Admin/Admin.module.css index e441f23..d63f429 100644 --- a/DataSentinel/src/pages/Admin/Admin.module.css +++ b/DataSentinel/src/pages/Admin/Admin.module.css @@ -83,55 +83,33 @@ color: var(--color-primary); } -/* Modale */ -.modalOverlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.6); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; +/* Formulaire de la modale (le conteneur vit dans Modal.module.css) */ +.field { + margin-bottom: 16px; } -.modal { - background-color: var(--color-white); - border-radius: var(--radius); - border-top: 4px solid var(--color-primary); - width: 90%; - max-width: 500px; - max-height: 80vh; - overflow-y: auto; -} - -.modalHeader { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px 24px; - border-bottom: 1px solid var(--color-gray-border); -} - -.modalHeader h2 { - margin: 0; - font-size: 18px; - font-weight: 600; +.label { + display: block; + margin-bottom: 6px; + font-size: 13px; + font-weight: 500; color: var(--color-text); } -.closeBtn { - background: none; - border: none; - font-size: 24px; - color: var(--color-text-secondary); - cursor: pointer; +.input { + width: 100%; + padding: 8px 12px; + border: 1px solid var(--color-gray-border); + border-radius: var(--radius); + font-size: 14px; + font-family: inherit; + color: var(--color-text); + background-color: var(--color-white); } -.modalBody { - padding: 24px; +.input:focus { + outline: 2px solid var(--color-primary); + outline-offset: -1px; } .modalActions { diff --git a/DataSentinel/src/pages/Admin/ReferentielForm.jsx b/DataSentinel/src/pages/Admin/ReferentielForm.jsx new file mode 100644 index 0000000..bc2c3fd --- /dev/null +++ b/DataSentinel/src/pages/Admin/ReferentielForm.jsx @@ -0,0 +1,91 @@ +import { useState } from 'react'; +import Button from '../../components/common/Button'; +import ErrorMessage from '../../components/common/ErrorMessage'; +import { LIBELLES_REFERENTIEL } from './referentielsConfig'; +import styles from './Admin.module.css'; + +/* Formulaire générique piloté par la description des champs du référentiel + (voir referentielsConfig.js) : les 4 onglets partagent ce composant. */ +export default function ReferentielForm({ referentiel, valeurInitiale, donnees, onEnregistrer, onAnnuler }) { + const [valeurs, setValeurs] = useState(() => + Object.fromEntries( + referentiel.champs.map(champ => [champ.name, valeurInitiale?.[champ.name] ?? '']) + ) + ); + const [enCours, setEnCours] = useState(false); + const [erreur, setErreur] = useState(''); + + function handleChange(name, valeur) { + setValeurs(precedentes => ({ ...precedentes, [name]: valeur })); + } + + async function handleSubmit(event) { + event.preventDefault(); + setErreur(''); + setEnCours(true); + try { + /* Les identifiants de référentiel arrivent en chaîne depuis handleChange(champ.name, e.target.value)} + required={champ.required} + > + + {(donnees[champ.options] || []).map(option => { + const { idField, labelField } = LIBELLES_REFERENTIEL[champ.options]; + return ( + + ); + })} + + ) : ( + handleChange(champ.name, e.target.value)} + required={champ.required} + /> + )} +
+ ))} + + {erreur && } + +
+ + +
+ + ); +} diff --git a/DataSentinel/src/pages/Admin/referentielsConfig.js b/DataSentinel/src/pages/Admin/referentielsConfig.js new file mode 100644 index 0000000..042e1af --- /dev/null +++ b/DataSentinel/src/pages/Admin/referentielsConfig.js @@ -0,0 +1,89 @@ +import { + createService, updateService, deleteService, + createCategorie, updateCategorie, deleteCategorie, + createContact, updateContact, deleteContact, + createMonitoring, updateMonitoring, desactiverMonitoring, +} from '../../services/api'; + +/* Description déclarative des 4 référentiels administrables. + Les colonnes du tableau, les champs du formulaire et les appels API sont + décrits ici plutôt que répétés onglet par onglet : les 4 onglets partagent + ainsi un seul tableau et un seul formulaire. */ +export const REFERENTIELS = [ + { + key: 'monitorings', + label: 'Monitorings', + singulier: 'monitoring', + idField: 'id_monito', + /* Désactivation logique : TABLE_FINAL référence le monitoring. */ + suppressionEstDesactivation: true, + colonnes: [ + { key: 'monito_intitule', label: 'Nom' }, + { key: 'id_service', label: 'Service', referentiel: 'services' }, + { key: 'id_categorie', label: 'Catégorie', referentiel: 'categories' }, + { key: 'bdd_source', label: 'Source' }, + { key: 'table_source', label: 'Table' }, + ], + champs: [ + { name: 'monito_intitule', label: 'Nom', type: 'text', required: true }, + { name: 'id_service', label: 'Service', type: 'select', options: 'services', required: true }, + { name: 'id_categorie', label: 'Catégorie', type: 'select', options: 'categories', required: true }, + { name: 'table_source', label: 'Table source', type: 'text', required: true }, + { name: 'bdd_source', label: 'Base source', type: 'text', required: true }, + ], + creer: createMonitoring, + modifier: updateMonitoring, + supprimer: desactiverMonitoring, + }, + { + key: 'services', + label: 'Services', + singulier: 'service', + idField: 'id_service', + colonnes: [{ key: 'nom_service', label: 'Nom' }], + champs: [{ name: 'nom_service', label: 'Nom du service', type: 'text', required: true }], + creer: createService, + modifier: updateService, + supprimer: deleteService, + }, + { + key: 'categories', + label: 'Catégories', + singulier: 'catégorie', + idField: 'id_categorie', + colonnes: [{ key: 'intitule_categorie', label: 'Nom' }], + champs: [{ name: 'intitule_categorie', label: 'Intitulé', type: 'text', required: true }], + creer: createCategorie, + modifier: updateCategorie, + supprimer: deleteCategorie, + }, + { + key: 'contacts', + label: 'Contacts', + singulier: 'contact', + idField: 'id_contact', + colonnes: [ + { key: 'intitule_contact', label: 'Intitulé' }, + { key: 'nom', label: 'Nom' }, + { key: 'prenom', label: 'Prénom' }, + { key: 'mail', label: 'Email' }, + { key: 'id_service', label: 'Service', referentiel: 'services' }, + ], + champs: [ + { name: 'intitule_contact', label: 'Intitulé', type: 'text', required: true }, + { name: 'nom', label: 'Nom', type: 'text', required: true }, + { name: 'prenom', label: 'Prénom', type: 'text', required: true }, + { name: 'mail', label: 'Email', type: 'email', required: true }, + { name: 'id_service', label: 'Service', type: 'select', options: 'services', required: true }, + ], + creer: createContact, + modifier: updateContact, + supprimer: deleteContact, + }, +]; + +/* Libellés des listes déroulantes et des colonnes qui affichent un identifiant. */ +export const LIBELLES_REFERENTIEL = { + services: { idField: 'id_service', labelField: 'nom_service' }, + categories: { idField: 'id_categorie', labelField: 'intitule_categorie' }, +}; diff --git a/DataSentinel/src/services/api.js b/DataSentinel/src/services/api.js index bbd1116..912a6ec 100644 --- a/DataSentinel/src/services/api.js +++ b/DataSentinel/src/services/api.js @@ -3,9 +3,11 @@ /* Utilise la variable d'environnement Vite si définie, sinon localhost par défaut */ const BASE_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8000'; -/* Clés de stockage local pour le token JWT et l'utilisateur connecté */ +/* Clés de stockage local pour le token JWT et l'utilisateur connecté. + USER_KEY est exporté : AuthContext lit/écrit la même entrée, et une clé + dupliquée des deux côtés divergerait silencieusement au premier renommage. */ const TOKEN_KEY = 'ds_token'; -const USER_KEY = 'ds_user'; +export const USER_KEY = 'ds_user'; /* Helpers de gestion du token JWT dans le localStorage */ export function getToken() { @@ -188,6 +190,58 @@ export async function getContacts(id_service) { return fetchApi('/contacts', { id_service }); } +/* ---------- Administration du référentiel (réservé Admin) ---------- */ + +export async function createService(body) { + return sendJson('/services', 'POST', body); +} + +export async function updateService(id_service, body) { + return sendJson(`/services/${id_service}`, 'PUT', body); +} + +export async function deleteService(id_service) { + return sendJson(`/services/${id_service}`, 'DELETE'); +} + +export async function createCategorie(body) { + return sendJson('/categories', 'POST', body); +} + +export async function updateCategorie(id_categorie, body) { + return sendJson(`/categories/${id_categorie}`, 'PUT', body); +} + +export async function deleteCategorie(id_categorie) { + return sendJson(`/categories/${id_categorie}`, 'DELETE'); +} + +export async function createContact(body) { + return sendJson('/contacts', 'POST', body); +} + +export async function updateContact(id_contact, body) { + return sendJson(`/contacts/${id_contact}`, 'PUT', body); +} + +export async function deleteContact(id_contact) { + return sendJson(`/contacts/${id_contact}`, 'DELETE'); +} + +export async function createMonitoring(body) { + return sendJson('/monitorings', 'POST', body); +} + +export async function updateMonitoring(id_monito, body) { + return sendJson(`/monitorings/${id_monito}`, 'PUT', body); +} + +/* Désactivation (actif = 0) : l'historique référence le monitoring, + il n'est jamais supprimé physiquement. */ +export async function desactiverMonitoring(id_monito) { + return sendJson(`/monitorings/${id_monito}`, 'DELETE'); +} + /* Récupère la nomenclature (monitorings), filtrable par service et catégorie */ export async function getMonitorings(id_service, id_categorie) { const data = await fetchApi('/monitorings', { id_service, id_categorie }); From 6ad76ca773d3e24ac104b4c4770649fb4188bf58 Mon Sep 17 00:00:00 2001 From: Anthony Coyaud Date: Sat, 15 Aug 2026 14:32:02 +0200 Subject: [PATCH 4/4] fix(front): corrige les 4 erreurs eslint restantes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le dépôt sortait 7 erreurs eslint au début de cette série de correctifs, dont certaines antérieures. Il n'en reste aucune. - Dashboard et Users appelaient setState de façon synchrone dans un effet, ce qui provoque des rendus en cascade. Le chargement initial passe par une fonction asynchrone qui n'écrit qu'après l'await, avec annulation au démontage. Sur le Dashboard, l'indicateur de chargement est allumé par les gestionnaires d'événement, et le bouton Actualiser relance l'effet via un compteur plutôt qu'en appelant le chargement. - L'import ExternalLink de Documentation.jsx n'était pas utilisé. - AuthContext.jsx exportait à la fois un composant, le contexte et un hook, ce qui désactivait le rafraîchissement à chaud de Vite pour ce fichier. Le contexte et le hook vivent désormais dans leurs propres modules ; les 5 fichiers concernés sont mis à jour. --- DataSentinel/src/App.jsx | 3 +- .../src/__tests__/AuthContext.test.jsx | 3 +- DataSentinel/src/components/Layout/Header.jsx | 2 +- .../src/components/Layout/Sidebar.jsx | 2 +- DataSentinel/src/context/AuthContext.jsx | 13 +--- DataSentinel/src/context/authContextObject.js | 7 +++ DataSentinel/src/context/useAuth.js | 15 +++++ .../src/pages/Dashboard/Dashboard.jsx | 61 ++++++++++++------- .../src/pages/Documentation/Documentation.jsx | 2 +- DataSentinel/src/pages/Login/Login.jsx | 2 +- DataSentinel/src/pages/Users/Users.jsx | 20 +++++- 11 files changed, 90 insertions(+), 40 deletions(-) create mode 100644 DataSentinel/src/context/authContextObject.js create mode 100644 DataSentinel/src/context/useAuth.js diff --git a/DataSentinel/src/App.jsx b/DataSentinel/src/App.jsx index 24306f8..94b658e 100644 --- a/DataSentinel/src/App.jsx +++ b/DataSentinel/src/App.jsx @@ -1,7 +1,8 @@ // src/App.jsx // Configuration du routing React Router v6 avec protection des routes import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; -import { AuthProvider, useAuth } from './context/AuthContext'; +import { AuthProvider } from './context/AuthContext'; +import { useAuth } from './context/useAuth'; import Layout from './components/Layout/Layout'; import Login from './pages/Login/Login'; import Dashboard from './pages/Dashboard/Dashboard'; diff --git a/DataSentinel/src/__tests__/AuthContext.test.jsx b/DataSentinel/src/__tests__/AuthContext.test.jsx index 439cba2..5c596f4 100644 --- a/DataSentinel/src/__tests__/AuthContext.test.jsx +++ b/DataSentinel/src/__tests__/AuthContext.test.jsx @@ -1,7 +1,8 @@ /* Tests du contexte d'authentification : login / logout */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { renderHook, act } from '@testing-library/react'; -import { AuthProvider, useAuth } from '../context/AuthContext'; +import { AuthProvider } from '../context/AuthContext'; +import { useAuth } from '../context/useAuth'; import * as api from '../services/api'; describe('AuthContext', () => { diff --git a/DataSentinel/src/components/Layout/Header.jsx b/DataSentinel/src/components/Layout/Header.jsx index 3692e77..72c538d 100644 --- a/DataSentinel/src/components/Layout/Header.jsx +++ b/DataSentinel/src/components/Layout/Header.jsx @@ -1,7 +1,7 @@ /* En-tête fixe de l'application Data Sentinel */ import { Bell, LogOut, Menu } from 'lucide-react'; -import { useAuth } from '../../context/AuthContext'; +import { useAuth } from '../../context/useAuth'; import { useNavigate } from 'react-router-dom'; import styles from './Header.module.css'; diff --git a/DataSentinel/src/components/Layout/Sidebar.jsx b/DataSentinel/src/components/Layout/Sidebar.jsx index 082bd2d..8b6fba9 100644 --- a/DataSentinel/src/components/Layout/Sidebar.jsx +++ b/DataSentinel/src/components/Layout/Sidebar.jsx @@ -13,7 +13,7 @@ import { FileText, ScrollText, } from 'lucide-react'; -import { useAuth } from '../../context/AuthContext'; +import { useAuth } from '../../context/useAuth'; import styles from './Sidebar.module.css'; /* Un lien de navigation avec icône */ diff --git a/DataSentinel/src/context/AuthContext.jsx b/DataSentinel/src/context/AuthContext.jsx index aac8a26..dc3b5aa 100644 --- a/DataSentinel/src/context/AuthContext.jsx +++ b/DataSentinel/src/context/AuthContext.jsx @@ -1,10 +1,9 @@ /* Contexte d'authentification global : gestion de l'utilisateur connecté */ -import { createContext, useContext, useState } from 'react'; +import { useState } from 'react'; import * as api from '../services/api'; import { USER_KEY } from '../services/api'; - -const AuthContext = createContext(null); +import { AuthContext } from './authContextObject'; /* Fournisseur du contexte d'authentification */ export function AuthProvider({ children }) { @@ -39,11 +38,3 @@ export function AuthProvider({ children }) { ); } -/* Hook personnalisé pour accéder au contexte d'authentification */ -export function useAuth() { - const context = useContext(AuthContext); - if (!context) { - throw new Error('useAuth doit être utilisé dans un AuthProvider'); - } - return context; -} diff --git a/DataSentinel/src/context/authContextObject.js b/DataSentinel/src/context/authContextObject.js new file mode 100644 index 0000000..b1cba8b --- /dev/null +++ b/DataSentinel/src/context/authContextObject.js @@ -0,0 +1,7 @@ +/* Objet de contexte isolé : un fichier qui exporte un composant ne doit + exporter que des composants, sans quoi le rafraîchissement à chaud de Vite + (react-refresh) cesse de fonctionner pour ce fichier. */ + +import { createContext } from 'react'; + +export const AuthContext = createContext(null); diff --git a/DataSentinel/src/context/useAuth.js b/DataSentinel/src/context/useAuth.js new file mode 100644 index 0000000..429359d --- /dev/null +++ b/DataSentinel/src/context/useAuth.js @@ -0,0 +1,15 @@ +/* Hook d'accès au contexte d'authentification. + + Séparé de AuthContext.jsx : un fichier qui exporte à la fois un composant + et autre chose casse le rafraîchissement à chaud de Vite (react-refresh). */ + +import { useContext } from 'react'; +import { AuthContext } from './authContextObject'; + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth doit être utilisé dans un AuthProvider'); + } + return context; +} diff --git a/DataSentinel/src/pages/Dashboard/Dashboard.jsx b/DataSentinel/src/pages/Dashboard/Dashboard.jsx index 979484a..ae98b6c 100644 --- a/DataSentinel/src/pages/Dashboard/Dashboard.jsx +++ b/DataSentinel/src/pages/Dashboard/Dashboard.jsx @@ -18,6 +18,7 @@ export default function Dashboard() { const [filtreCategorie, setFiltreCategorie] = useState(''); const [loading, setLoading] = useState(true); const [erreur, setErreur] = useState(''); + const [rafraichissement, setRafraichissement] = useState(0); /* Chargement initial des filtres. On n'utilise pas les référentiels SERVICE / CATEGORIE complets : ils @@ -52,39 +53,57 @@ export default function Dashboard() { /* 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) { + setLoading(true); setFiltreService(service); if (filtreCategorie && !categoriesDisponibles(service).includes(filtreCategorie)) { setFiltreCategorie(''); } } - /* Chargement des données dashboard */ - const loadData = useCallback(async () => { + function handleCategorieChange(categorie) { setLoading(true); - setErreur(''); - try { - const [sum, data] = await Promise.all([ - getDashboardSummary(), - getDashboard(filtreService, filtreCategorie), - ]); - setSummary(sum); - setMonitorings(Array.isArray(data) ? data : []); - } catch (e) { - setErreur(e.message || 'Erreur lors du chargement du dashboard.'); - } finally { - setLoading(false); - } - }, [filtreService, filtreCategorie]); + setFiltreCategorie(categorie); + } + /* Le bouton Actualiser relance l'effet en changeant cette valeur. */ + function handleActualiser() { + setLoading(true); + setRafraichissement(compteur => compteur + 1); + } + + /* Les données sont rechargées à chaque changement de filtre. L'indicateur de + chargement est allumé par les gestionnaires d'événement ci-dessus, jamais + dans l'effet : y appeler setState de façon synchrone provoquerait un rendu + en cascade. */ useEffect(() => { - loadData(); - }, [loadData]); + let annule = false; + + async function charger() { + try { + const [sum, data] = await Promise.all([ + getDashboardSummary(), + getDashboard(filtreService, filtreCategorie), + ]); + if (annule) return; + setSummary(sum); + setMonitorings(Array.isArray(data) ? data : []); + setErreur(''); + } catch (e) { + if (!annule) setErreur(e.message || 'Erreur lors du chargement du dashboard.'); + } finally { + if (!annule) setLoading(false); + } + } + + charger(); + return () => { annule = true; }; + }, [filtreService, filtreCategorie, rafraichissement]); return (

Dashboard

- @@ -131,7 +150,7 @@ export default function Dashboard() {