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.
-
- setModalOpen(false)}>
- Annuler
-
- setModalOpen(false)}>
- Enregistrer
-
-
+ 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