personnal/ecole/public/js/global.js

444 lines
14 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, 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 `<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, '&lt;').replace(/>/g, '&gt;');
// 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>
`;
}
/**
* 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|&nbsp;/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 + ' €';
}