394 lines
12 KiB
JavaScript
394 lines
12 KiB
JavaScript
// Événement de vérification d'authentification
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Vérification de la page sur laquelle nous sommes
|
|
if (window.location.pathname !== '/auth') {
|
|
// Récupération du token JWT du localStorage
|
|
const token = localStorage.getItem('jwtToken');
|
|
|
|
// Si pas de token, redirige vers la page d'authentification
|
|
if (!token) {
|
|
window.location.replace('/auth');
|
|
} else {
|
|
// Si un token existe, vérification de sa validité
|
|
fetch('/auth/verifyToken', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Bearer ' + token
|
|
}
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
|
|
// Si le token n'est pas valide, redirige vers la page d'authentification
|
|
if (!data.valid) {
|
|
localStorage.removeItem('jwtToken');
|
|
window.location.replace('/auth');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Événement du bouton déconnexion
|
|
document.addEventListener('DOMContentLoaded', function (event) {
|
|
// Empêche le comportement par défaut du formulaire
|
|
event.preventDefault();
|
|
|
|
const logoutBtn = document.getElementById('logoutBtn');
|
|
|
|
if (window.location.pathname === '/auth') {
|
|
logoutBtn.style.display = 'none';
|
|
} else {
|
|
logoutBtn.addEventListener('click', function () {
|
|
// Suppression du token JWT du localStorage et redirection
|
|
localStorage.removeItem('jwtToken');
|
|
window.location.replace('/auth');
|
|
});
|
|
}
|
|
});
|
|
|
|
// Événement d'initialisation des Materialize content
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Init dropdown
|
|
var sidenav = document.querySelectorAll('.sidenav');
|
|
M.Sidenav.init(sidenav);
|
|
|
|
// Init Modal
|
|
var modals = document.querySelectorAll('.modal');
|
|
M.Modal.init(modals);
|
|
});
|
|
|
|
// Événement condition de groupe pour les menus
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const token = localStorage.getItem('jwtToken');
|
|
|
|
if (token) {
|
|
const decoded = jwt_decode(token);
|
|
const userAuthGroupe = decoded.userAuthGroupe;
|
|
|
|
const reportingItem = document.getElementById("reportingSidenavSelect");
|
|
const adminItem = document.getElementById("adminSidenavSelect");
|
|
|
|
// Cache par défaut
|
|
reportingItem.style.display = 'none';
|
|
adminItem.style.display = 'none';
|
|
|
|
if (userAuthGroupe === 'MANAGER' || userAuthGroupe === 'ADMIN') {
|
|
reportingItem.style.display = 'block';
|
|
}
|
|
|
|
if (userAuthGroupe === 'ADMIN') {
|
|
adminItem.style.display = 'block';
|
|
}
|
|
}
|
|
});
|
|
|
|
// Fonction pour extraire le numéro de parcours de l'URL
|
|
function getNumParcoursFromURL() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
return urlParams.get('numParcours');
|
|
}
|
|
|
|
|
|
// Fonction pour extraire le sous-menu de l'URL
|
|
function getSubmenuFromURL() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
return urlParams.get('submenu');
|
|
}
|
|
|
|
// Fonction pour vérifier si une valeur est nulle ou non définie
|
|
function isNullOrUndefined(value) {
|
|
return value === null || value === undefined;
|
|
}
|
|
|
|
// Fonction pour éviter la duplication du code fetch
|
|
async function fetchWithJson(url, method, body = null) {
|
|
const options = {
|
|
method: method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
};
|
|
if (body) options.body = JSON.stringify(body);
|
|
|
|
const response = await fetch(url, options);
|
|
if (!response.ok) throw new Error('Réseau ou erreur serveur');
|
|
|
|
return await response.json();
|
|
}
|
|
|
|
// Fonction de load du parcours en session storage
|
|
async function loadParcours(numParcours) {
|
|
try {
|
|
const data = await fetchWithJson(`/parcours/read/${numParcours}`, 'GET');
|
|
|
|
if (data.valid) {
|
|
sessionStorage.setItem('parcours', JSON.stringify(data.parcours));
|
|
} else {
|
|
sessionStorage.setItem('parcours', null);
|
|
sessionStorage.setItem('admins', JSON.stringify(data.admins));
|
|
console.log("Parcours introuvable");
|
|
}
|
|
} catch (error) {
|
|
console.error("Erreur lors de la récupération des informations parcours :", error);
|
|
}
|
|
}
|
|
|
|
// Fonction générique de toggle
|
|
function toggler(btn, div) {
|
|
if (document.getElementById(btn).checked) {
|
|
document.getElementById(div).style.display = 'block';
|
|
} else {
|
|
document.getElementById(div).style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Fonction de load du parcours en session storage
|
|
async function loadContrat(idContrat) {
|
|
try {
|
|
const data = await fetchWithJson(`/contrat/read/id/${idContrat}`, 'GET');
|
|
|
|
if (data.valid) {
|
|
sessionStorage.setItem('contrat', JSON.stringify(data.contrat));
|
|
} else {
|
|
sessionStorage.setItem('contrat', null);
|
|
console.log("Contrat introuvable");
|
|
}
|
|
} catch (error) {
|
|
console.error("Erreur lors de la récupération des informations contrat :", error);
|
|
}
|
|
}
|
|
|
|
// ========== Fonctions utilitaires génériques ==========
|
|
|
|
/**
|
|
* Formatage des dates (ISO vers format français)
|
|
* @param {string} iso - Date au format ISO
|
|
* @param {boolean} withTime - Inclure l'heure (défaut: true)
|
|
* @returns {string} Date formatée (dd/mm/yyyy ou dd/mm/yyyy hh:mm)
|
|
*/
|
|
function fmtDate(iso, withTime = true) {
|
|
// Si la valeur est null, undefined, ou vide
|
|
if (!iso || (typeof iso === 'string' && iso.trim() === "")) return "NC";
|
|
|
|
// Convertir en string si ce n'est pas déjà le cas
|
|
const dateStr = String(iso).trim();
|
|
|
|
// Vérifier les valeurs invalides connues
|
|
if (dateStr === "00/00/0000" || dateStr === "00/00" || dateStr === "null" || dateStr === "undefined") {
|
|
return "NC";
|
|
}
|
|
|
|
let d;
|
|
|
|
// Si c'est déjà au format jj/mm/aaaa (format français)
|
|
if (dateStr.includes("/") && dateStr.split("/").length === 3) {
|
|
const parts = dateStr.split("/");
|
|
const day = parseInt(parts[0], 10);
|
|
const month = parseInt(parts[1], 10);
|
|
const year = parseInt(parts[2], 10);
|
|
|
|
// Vérifier si les valeurs sont valides
|
|
if (isNaN(day) || isNaN(month) || isNaN(year)) {
|
|
return "NC";
|
|
}
|
|
|
|
// Si le jour ou le mois est 00, considérer comme invalide
|
|
if (day === 0 || month === 0) {
|
|
return "NC";
|
|
}
|
|
|
|
// Si le mois est invalide
|
|
if (month < 1 || month > 12) {
|
|
return "NC";
|
|
}
|
|
|
|
// Si l'année est 0000, afficher juste jj/mm (sans l'année)
|
|
if (year === 0) {
|
|
return `${String(day).padStart(2, "0")}/${String(month).padStart(2, "0")}`;
|
|
}
|
|
|
|
// Si l'année est valide, créer une vraie date et valider
|
|
const monthIndex = month - 1; // Les mois commencent à 0
|
|
d = new Date(year, monthIndex, day);
|
|
// Vérifier si la date est valide (ex: 31/02/2000 serait invalide)
|
|
if (d.getDate() !== day || d.getMonth() !== monthIndex || d.getFullYear() !== year) {
|
|
return "NC";
|
|
}
|
|
|
|
// Formater la date
|
|
const dd = String(d.getDate()).padStart(2, "0");
|
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
const yyyy = d.getFullYear();
|
|
if (!withTime) return `${dd}/${mm}/${yyyy}`;
|
|
const hh = String(d.getHours()).padStart(2, "0");
|
|
const mi = String(d.getMinutes()).padStart(2, "0");
|
|
return `${dd}/${mm}/${yyyy} ${hh}:${mi}`;
|
|
}
|
|
// Si c'est au format jj/mm (pour date d'échéance)
|
|
else if (dateStr.includes("/") && dateStr.split("/").length === 2) {
|
|
const parts = dateStr.split("/");
|
|
const day = parseInt(parts[0], 10);
|
|
const month = parseInt(parts[1], 10);
|
|
|
|
// Pour l'échéance, on retourne juste jj/mm
|
|
if (isNaN(day) || isNaN(month) || day === 0 || month === 0 || month > 12) {
|
|
return "NC";
|
|
}
|
|
return `${String(day).padStart(2, "0")}/${String(month).padStart(2, "0")}`;
|
|
}
|
|
// Sinon, essayer de parser comme date ISO
|
|
else {
|
|
d = new Date(dateStr);
|
|
if (isNaN(d.getTime())) return "NC";
|
|
|
|
const dd = String(d.getDate()).padStart(2, "0");
|
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
const yyyy = d.getFullYear();
|
|
if (!withTime) return `${dd}/${mm}/${yyyy}`;
|
|
const hh = String(d.getHours()).padStart(2, "0");
|
|
const mi = String(d.getMinutes()).padStart(2, "0");
|
|
return `${dd}/${mm}/${yyyy} ${hh}:${mi}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Crée un élément key-value pour l'affichage de détails
|
|
* @param {string} label - Libellé
|
|
* @param {*} value - Valeur (booléen converti en Oui/Non)
|
|
* @returns {string} HTML formaté
|
|
*/
|
|
function kv(label, value) {
|
|
const v = (value === true) ? "Oui" : (value === false) ? "Non" : (value ?? "NC");
|
|
return `<div style="display:flex; gap:8px; margin:2px 0;">
|
|
<div style="min-width:220px;"><strong>${label}</strong> :</div>
|
|
<div>${v}</div>
|
|
</div>`;
|
|
}
|
|
|
|
/**
|
|
* Crée une grille à 2 colonnes pour l'affichage de détails
|
|
* @param {string} innerLeft - Contenu colonne gauche
|
|
* @param {string} innerRight - Contenu colonne droite
|
|
* @returns {string} HTML formaté
|
|
*/
|
|
function gridWrap2cols(innerLeft, innerRight) {
|
|
return `<div style="display:grid;grid-template-columns:repeat(2,minmax(280px,1fr));gap:14px;">${innerLeft}${innerRight}</div>`;
|
|
}
|
|
|
|
/**
|
|
* Fonction debounce pour limiter la fréquence d'exécution d'une fonction
|
|
* Utile pour les recherches en temps réel et éviter les appels API excessifs
|
|
* @param {Function} fn - Fonction à débouncer
|
|
* @param {number} delay - Délai en millisecondes (défaut: 300ms)
|
|
* @returns {Function} Fonction débouncée avec méthode cancel()
|
|
*/
|
|
function debounce(fn, delay = 300) {
|
|
let t;
|
|
function wrapped(...args) {
|
|
clearTimeout(t);
|
|
t = setTimeout(() => fn(...args), delay);
|
|
}
|
|
wrapped.cancel = () => clearTimeout(t);
|
|
return wrapped;
|
|
}
|
|
|
|
/**
|
|
* Décode un token JWT et retourne le payload
|
|
* @param {string} token - Token JWT à décoder
|
|
* @returns {Object|null} Payload décodé ou null en cas d'erreur
|
|
*/
|
|
function parseJwt(token) {
|
|
try {
|
|
const base64Url = token.split(".")[1];
|
|
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
const jsonPayload = decodeURIComponent(
|
|
atob(base64)
|
|
.split("")
|
|
.map(function (c) {
|
|
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
|
|
})
|
|
.join("")
|
|
);
|
|
return JSON.parse(jsonPayload);
|
|
} catch (error) {
|
|
console.error("Erreur lors du décodage du token:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Affiche un message d'erreur dans l'élément avec l'ID "error"
|
|
* @param {string} message - Message d'erreur à afficher
|
|
*/
|
|
function displayError(message) {
|
|
const errorElement = document.getElementById("error");
|
|
if (errorElement) {
|
|
errorElement.textContent = message;
|
|
errorElement.style.display = "block";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Crée un message formaté avec différents types (info, warn, error, dev)
|
|
* @param {string} type - Type de message : 'info', 'warn', 'error', 'dev'
|
|
* @param {string} title - Titre du message
|
|
* @param {string} description - Description du message (peut contenir du HTML)
|
|
* @returns {string} HTML du message formaté
|
|
*/
|
|
function createMessageBox(type, title, description) {
|
|
// Protection contre les paramètres invalides
|
|
if (!type || typeof type !== 'string') type = 'info';
|
|
if (!title || typeof title !== 'string') title = 'Message';
|
|
if (!description || typeof description !== 'string') description = '';
|
|
|
|
const configs = {
|
|
info: {
|
|
icon: 'fa-info-circle',
|
|
bgColor: '#e3f2fd',
|
|
borderColor: '#2196f3',
|
|
textColor: '#1565c0'
|
|
},
|
|
warn: {
|
|
icon: 'fa-exclamation-triangle',
|
|
bgColor: '#fff3e0',
|
|
borderColor: '#ff9800',
|
|
textColor: '#e65100'
|
|
},
|
|
error: {
|
|
icon: 'fa-times-circle',
|
|
bgColor: '#ffebee',
|
|
borderColor: '#f44336',
|
|
textColor: '#c62828'
|
|
},
|
|
dev: {
|
|
icon: 'fa-tools',
|
|
bgColor: '#fff3cd',
|
|
borderColor: '#ffc107',
|
|
textColor: '#856404'
|
|
}
|
|
};
|
|
|
|
const config = configs[type] || configs.info;
|
|
|
|
// Échappement basique pour éviter les problèmes (description peut contenir du HTML valide)
|
|
const safeTitle = String(title).replace(/</g, '<').replace(/>/g, '>');
|
|
// Description peut contenir du HTML (comme <br>), donc on ne l'échappe pas complètement
|
|
// mais on s'assure qu'elle est une string
|
|
const safeDescription = String(description);
|
|
|
|
return `
|
|
<div style="padding: 20px; text-align: center; background-color: ${config.bgColor}; border: 1px solid ${config.borderColor}; border-radius: 4px; margin: 10px 0; width: 100%; box-sizing: border-box;">
|
|
<p style="color: ${config.textColor}; font-weight: 600; margin: 0 0 10px 0;">
|
|
<i class="fas ${config.icon}" style="margin-right: 8px;"></i>
|
|
${safeTitle}
|
|
</p>
|
|
<p style="color: ${config.textColor}; margin: 0; font-size: 0.9rem;">
|
|
${safeDescription}
|
|
</p>
|
|
</div>
|
|
`;
|
|
} |