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.
400 lines
12 KiB
JavaScript
400 lines
12 KiB
JavaScript
/* Service centralisé pour tous les appels API vers le backend FastAPI */
|
|
|
|
/* 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é.
|
|
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';
|
|
export const USER_KEY = 'ds_user';
|
|
|
|
/* Helpers de gestion du token JWT dans le localStorage */
|
|
export function getToken() {
|
|
return localStorage.getItem(TOKEN_KEY);
|
|
}
|
|
|
|
export function setToken(token) {
|
|
localStorage.setItem(TOKEN_KEY, token);
|
|
}
|
|
|
|
export function clearToken() {
|
|
localStorage.removeItem(TOKEN_KEY);
|
|
}
|
|
|
|
/* Construit les en-têtes d'authentification si un token est présent */
|
|
function authHeaders(extra = {}) {
|
|
const token = getToken();
|
|
const headers = { ...extra };
|
|
if (token) {
|
|
headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
/* Gestion d'une réponse 401 : purge de la session et redirection vers /login */
|
|
function handleUnauthorized() {
|
|
clearToken();
|
|
localStorage.removeItem(USER_KEY);
|
|
/* Évite une boucle de redirection si on est déjà sur la page de connexion */
|
|
if (window.location.pathname !== '/login') {
|
|
window.location.href = '/login';
|
|
}
|
|
}
|
|
|
|
/* Fonction utilitaire pour les requêtes fetch (GET) avec gestion d'erreur et auth */
|
|
async function fetchApi(path, params = {}) {
|
|
const url = new URL(`${BASE_URL}${path}`);
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value !== undefined && value !== null && value !== '') {
|
|
url.searchParams.append(key, value);
|
|
}
|
|
});
|
|
|
|
try {
|
|
const response = await fetch(url.toString(), { headers: authHeaders() });
|
|
if (response.status === 401) {
|
|
handleUnauthorized();
|
|
throw new Error('Session expirée, veuillez vous reconnecter.');
|
|
}
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error(`Erreur API [${response.status}] ${path}: ${errorText}`);
|
|
throw new Error(`Erreur ${response.status}: ${errorText}`);
|
|
}
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error(`Erreur lors de l'appel API ${path}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/* Helper générique pour les requêtes JSON (POST / PUT / DELETE) avec auth */
|
|
async function sendJson(path, method, body) {
|
|
const url = `${BASE_URL}${path}`;
|
|
const options = {
|
|
method,
|
|
headers: authHeaders(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
};
|
|
if (body !== undefined) {
|
|
options.body = JSON.stringify(body);
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, options);
|
|
if (response.status === 401) {
|
|
handleUnauthorized();
|
|
throw new Error('Session expirée, veuillez vous reconnecter.');
|
|
}
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error(`Erreur API [${response.status}] ${path}: ${errorText}`);
|
|
throw new Error(`Erreur ${response.status}: ${errorText}`);
|
|
}
|
|
/* Certaines réponses (204, DELETE) peuvent ne pas contenir de corps JSON */
|
|
const text = await response.text();
|
|
return text ? JSON.parse(text) : null;
|
|
} catch (error) {
|
|
console.error(`Erreur lors de l'appel API ${path}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/* Authentification : POST /auth/login en x-www-form-urlencoded (OAuth2) */
|
|
export async function login(username, password) {
|
|
const body = new URLSearchParams();
|
|
body.append('username', username);
|
|
body.append('password', password);
|
|
|
|
const response = await fetch(`${BASE_URL}/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: body.toString(),
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
throw new Error('Identifiants invalides');
|
|
}
|
|
if (response.status === 429) {
|
|
throw new Error('Trop de tentatives. Veuillez réessayer dans une minute.');
|
|
}
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(`Erreur ${response.status}: ${errorText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
setToken(data.access_token);
|
|
return data.user;
|
|
}
|
|
|
|
/* Récupère le profil de l'utilisateur connecté */
|
|
export async function getMe() {
|
|
return fetchApi('/auth/me');
|
|
}
|
|
|
|
/* ---------- Administration des utilisateurs ---------- */
|
|
|
|
export async function getUsers() {
|
|
return fetchApi('/admin/users');
|
|
}
|
|
|
|
export async function createUser({ username, email, password, role }) {
|
|
return sendJson('/admin/users', 'POST', { username, email, password, role });
|
|
}
|
|
|
|
export async function updateUser(id_user, changes) {
|
|
return sendJson(`/admin/users/${id_user}`, 'PUT', changes);
|
|
}
|
|
|
|
export async function deleteUser(id_user) {
|
|
return sendJson(`/admin/users/${id_user}`, 'DELETE');
|
|
}
|
|
|
|
export async function resetUserPassword(id_user, password) {
|
|
return sendJson(`/admin/users/${id_user}/reset-password`, 'POST', { password });
|
|
}
|
|
|
|
/* Journal d'audit */
|
|
export async function getJournal(limit = 200) {
|
|
return fetchApi('/admin/journal', { limit });
|
|
}
|
|
|
|
/* ---------- RGPD : données personnelles ---------- */
|
|
|
|
export async function exportMyData() {
|
|
return fetchApi('/me/data-export');
|
|
}
|
|
|
|
export async function deleteMyAccount() {
|
|
return sendJson('/me', 'DELETE');
|
|
}
|
|
|
|
/* Santé de l'API */
|
|
export async function getHealth() {
|
|
return fetchApi('/health');
|
|
}
|
|
|
|
/* Récupère la liste des catégories */
|
|
export async function getCategories() {
|
|
return fetchApi('/categories');
|
|
}
|
|
|
|
/* Récupère la liste des services */
|
|
export async function getServices() {
|
|
return fetchApi('/services');
|
|
}
|
|
|
|
/* Récupère les contacts, filtrable par service */
|
|
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 });
|
|
return Array.isArray(data) ? data.map(m => ({
|
|
id: m.id_monito,
|
|
id_monito: m.id_monito,
|
|
nom: m.monito_intitule,
|
|
monito_intitule: m.monito_intitule,
|
|
...m
|
|
})) : data;
|
|
}
|
|
|
|
/* Récupère un monitoring par son identifiant */
|
|
export async function getMonitoringById(id) {
|
|
const data = await fetchApi(`/monitorings/${id}`);
|
|
return data ? {
|
|
id: data.id_monito,
|
|
id_monito: data.id_monito,
|
|
nom: data.monito_intitule,
|
|
monito_intitule: data.monito_intitule,
|
|
...data
|
|
} : data;
|
|
}
|
|
|
|
/* Récupère les lignes d'erreur d'un monitoring avec pagination et filtres */
|
|
export async function getMonitoringDetails(id, { search, limit = 500, offset = 0 } = {}) {
|
|
return fetchApi(`/monitorings/${id}/details`, { search, limit, offset });
|
|
}
|
|
|
|
/* Récupère le nombre d'erreurs d'un monitoring */
|
|
export async function getMonitoringCount(id) {
|
|
const data = await fetchApi(`/monitorings/${id}/count`);
|
|
return {
|
|
id_monito: data.id_monito,
|
|
count: data.nb_erreurs,
|
|
nb_erreurs: data.nb_erreurs,
|
|
...data
|
|
};
|
|
}
|
|
|
|
/* Récupère les colonnes dynamiques d'un monitoring */
|
|
export async function getMonitoringColumns(id) {
|
|
return fetchApi(`/monitorings/${id}/columns`);
|
|
}
|
|
|
|
/* Récupère les données du dashboard (VUE_CONSO), filtrable par service et catégorie */
|
|
export async function getDashboard(service, categorie) {
|
|
const data = await fetchApi('/dashboard', { service, categorie });
|
|
return Array.isArray(data) ? data.map(m => ({
|
|
id: m.id_monito,
|
|
id_monito: m.id_monito,
|
|
nom: m.nom_monito,
|
|
nb_erreurs: m.nb_erreurs,
|
|
...m
|
|
})) : 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');
|
|
return {
|
|
total: data.nb_monitorings ?? 0,
|
|
ok: data.monitorings_ok ?? 0,
|
|
warning: 0, // À calculer ou ajouter à l'API
|
|
critical: data.monitorings_en_erreur ?? 0,
|
|
nb_monitorings: data.nb_monitorings,
|
|
total_erreurs: data.total_erreurs,
|
|
max_erreurs: data.max_erreurs,
|
|
monitoring_critique: data.monitoring_critique,
|
|
...data
|
|
};
|
|
}
|
|
|
|
/* Récupère l'historique, filtrable par monitoring, dates et service */
|
|
export async function getHistorique(id_monito, date_debut, date_fin, service) {
|
|
const data = await fetchApi('/historique', { id_monito, date_debut, date_fin, service });
|
|
return Array.isArray(data) ? data.map(h => ({
|
|
date: h.date_sauvegarde,
|
|
date_sauvegarde: h.date_sauvegarde,
|
|
...h
|
|
})) : data;
|
|
}
|
|
|
|
/* Récupère les données d'évolution pour un graphique */
|
|
export async function getHistoriqueEvolution(id, date_debut, date_fin) {
|
|
const data = await fetchApi(`/historique/${id}/evolution`, { date_debut, date_fin });
|
|
if (data?.evolution) {
|
|
return data.evolution.map(point => ({
|
|
date: point.date,
|
|
erreurs: point.nb_erreurs,
|
|
nb_erreurs: point.nb_erreurs
|
|
}));
|
|
}
|
|
return [];
|
|
}
|
|
|
|
export async function getEvolutionGlobal(date_debut, date_fin) {
|
|
const data = await fetchApi('/evolution/global', { date_debut, date_fin });
|
|
if (data?.series) {
|
|
return data.series.map(point => ({
|
|
date: point.date,
|
|
erreurs: point.nb_erreurs,
|
|
nb_erreurs: point.nb_erreurs
|
|
}));
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/* Export CSV utilitaire */
|
|
export function exportToCSV(data, filename = 'export.csv') {
|
|
if (!data || data.length === 0) {
|
|
console.warn('Aucune donnée à exporter');
|
|
return;
|
|
}
|
|
|
|
// Récupère les clés du premier objet
|
|
const headers = Object.keys(data[0]);
|
|
|
|
// Crée le CSV : en-têtes + lignes
|
|
const csvLines = [
|
|
headers.join(','),
|
|
...data.map(row =>
|
|
headers.map(header => {
|
|
const value = row[header];
|
|
// Échappe les valeurs qui contiennent des virgules ou des guillemets
|
|
if (value === null || value === undefined) return '';
|
|
const str = String(value);
|
|
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
return `"${str.replace(/"/g, '""')}"`;
|
|
}
|
|
return str;
|
|
}).join(',')
|
|
)
|
|
].join('\n');
|
|
|
|
// Crée un blob et télécharge
|
|
const blob = new Blob([csvLines], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement('a');
|
|
link.href = URL.createObjectURL(blob);
|
|
link.download = filename;
|
|
link.click();
|
|
URL.revokeObjectURL(link.href);
|
|
}
|