// É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, useLoader = true) { let loaderShown = false; if (useLoader && typeof showLoader === 'function') { showLoader(); loaderShown = true; } try { 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'); const data = await response.json(); if (loaderShown && typeof hideLoader === 'function') { hideLoader(); } return data; } catch (error) { // Toujours cacher le loader en cas d'erreur if (loaderShown && typeof hideLoader === 'function') { hideLoader(); } throw error; } } // 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 `
${label} :
${v}
`; } /** * 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 `
${innerLeft}${innerRight}
`; } /** * 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, '>'); // Description peut contenir du HTML (comme
), donc on ne l'échappe pas complètement // mais on s'assure qu'elle est une string const safeDescription = String(description); return `

${safeTitle}

${safeDescription}

`; } /** * Formate une valeur en euros : * - espace insécable fine entre milliers, * - virgule pour les décimales, * - 0 à 2 décimales max, * - symbole € à la fin. * Si la valeur est invalide => "NC". */ function formatEuro(value, options) { const opts = Object.assign({ minimumFractionDigits: 0, maximumFractionDigits: 2 }, options || {}); if (value === null || value === undefined || value === '' || value === 'NC') return 'NC'; // Accepte string avec virgule ou espaces const normalized = (typeof value === 'string') ? value.replace(/\s|\u00A0|\u202F| /g, '').replace(',', '.') : value; const num = Number(normalized); if (!isFinite(num)) return 'NC'; const formatted = new Intl.NumberFormat('fr-FR', { minimumFractionDigits: opts.minimumFractionDigits, maximumFractionDigits: opts.maximumFractionDigits }).format(num); return formatted + ' €'; }