/** * UX Manager - Gestionnaire d'expérience utilisateur modulaire * Version: 3.0.0 - Corrigée et Optimisée * Description: Gère tous les aspects de l'UX : navigation, panels contextuels, notifications, accessibilité * Auteur: Portail RH Inter Santé * Date: 2025.12.22 - Version finale */ // ============================================================================ // CLASSE PRINCIPALE - UXManager // ============================================================================ /** * Classe principale qui orchestre tous les gestionnaires d'expérience utilisateur * Elle initialise et coordonne les différents sous-managers */ class UXManager { /** * Constructeur principal - Initialise tous les sous-managers */ constructor() { // Initialisation des sous-managers this.navigation = new NavigationManager(); this.contextPanel = new ContextPanelManager(); this.notifications = new NotificationManager(); this.accessibility = new AccessibilityManager(); this.performance = new PerformanceManager(); this.language = new LanguageManager(); // Lancement de l'initialisation this.init(); } /** * Méthode d'initialisation principale * Appelée automatiquement par le constructeur */ init() { //console.log('[UX Manager] Initialisation du système d\'expérience utilisateur...'); // Initialisation séquentielle des sous-managers this.navigation.init(); this.contextPanel.init(); this.notifications.init(); this.accessibility.init(); this.performance.init(); this.language.init(); // Gestion de session utilisateur this.checkSession(); // Initialisation du Service Worker this.initServiceWorker(); //console.log('[UX Manager] Initialisation terminée avec succès'); } /** * Basculer l'état du panneau contextuel * @public - Méthode accessible globalement */ toggleContextPanel() { this.contextPanel.toggle(); } /** * Vérifier et gérer l'expiration de session utilisateur * @private - Méthode interne de gestion de session */ checkSession() { // Récupérer la durée de session depuis le DOM const dureeSession = parseInt(document.getElementById('dureeSession')?.value) || 30; const dureeMinutes = dureeSession * 60 * 1000; // Conversion en millisecondes // Vérifier régulièrement l'activité de l'utilisateur setInterval(() => { const derniereAction = sessionStorage.getItem('derniere_action'); const maintenant = Date.now(); // Si inactivité dépassant la durée autorisée if (derniereAction && (maintenant - derniereAction > dureeMinutes)) { this.showSessionWarning(); } }, 60000); // Vérification toutes les minutes } /** * Afficher un avertissement d'expiration de session * @private - Méthode d'affichage de notification */ showSessionWarning() { const isAnglophone = window.appConfig?.isAnglophone || false; const messages = { title: isAnglophone ? 'Session Warning' : 'Avertissement de session', text: isAnglophone ? 'Your session will expire soon. Do you want to extend it?' : 'Votre session va bientôt expirer. Souhaitez-vous la prolonger?', confirm: isAnglophone ? 'Extend' : 'Prolonger', cancel: isAnglophone ? 'Logout' : 'Déconnexion' }; // Utiliser SweetAlert pour une notification élégante Swal.fire({ title: messages.title, text: messages.text, icon: 'warning', showCancelButton: true, confirmButtonText: messages.confirm, cancelButtonText: messages.cancel, allowOutsideClick: false, allowEscapeKey: false }).then((result) => { if (result.isConfirmed) { // Prolonger la session sessionStorage.setItem('derniere_action', Date.now()); this.accessibility.announce( isAnglophone ? 'Session extended' : 'Session prolongée' ); } else { // Déconnexion window.location.href = window.appConfig?.racineWeb + 'Connexion/deconnecter'; } }); } /** * Initialiser le Service Worker pour le mode hors-ligne * @private - Méthode d'initialisation PWA */ initServiceWorker() { if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then(registration => { //console.log('[UX Manager] Service Worker actif:', registration.scope); }).catch(error => { //console.warn('[UX Manager] Service Worker non disponible:', error); }); } } } // ============================================================================ // SOUS-CLASSE - NavigationManager (GESTION DES MENUS) // ============================================================================ /** * Gestionnaire spécialisé pour la navigation et les menus * Contrôle l'ouverture/fermeture des menus dépliables */ class NavigationManager { /** * Constructeur - Initialise l'état de navigation */ constructor() { this.currentOpenMenu = null; // Menu actuellement ouvert this.activeMenuId = null; // Menu actif basé sur la page courante this.isInitialized = false; // État d'initialisation } /** * Initialiser le système de navigation * @public - Méthode appelée par UXManager */ init() { if (this.isInitialized) return; // console.log('[Navigation] Initialisation du système de menus...'); // Appliquer le correctif d'urgence pour les menus multiples ouverts this.applyEmergencyFix(); // Configuration des comportements this.setupActiveMenu(); this.setupMenuBehavior(); this.setupKeyboardNavigation(); // Vérification finale après initialisation setTimeout(() => { this.validateMenuConsistency(); }, 300); this.isInitialized = true; //console.log('[Navigation] Système de menus initialisé'); } /** * Correctif d'urgence - Force un seul menu ouvert au chargement * @private - Méthode de correction initiale */ applyEmergencyFix() { // Détecter le menu actif depuis la configuration const activeParentId = window.appConfig?.activeParentId; if (activeParentId !== null && activeParentId !== '') { this.activeMenuId = `submenu${activeParentId}`; //console.log('[Navigation] Correctif: Menu actif détecté:', this.activeMenuId); // Forcer la fermeture de tous les autres menus this.forceSingleMenu(); } } /** * Forcer l'ouverture d'un seul menu (celui actif) * @private - Méthode de correction forcée */ forceSingleMenu() { if (!this.activeMenuId) return; //console.log('[Navigation] Forçage: Un seul menu ouvert ->', this.activeMenuId); // Étape 1: Fermer TOUS les menus document.querySelectorAll('.nav-submenu').forEach(menu => { if (menu.id !== this.activeMenuId) { this.closeMenu(menu.id, true); // true = force close } }); // Étape 2: Ouvrir le menu actif setTimeout(() => { this.openMenu(this.activeMenuId); this.currentOpenMenu = this.activeMenuId; }, 50); } /** * Configurer le menu actif basé sur la page courante * @private - Méthode de configuration initiale */ setupActiveMenu() { const activeParentId = window.appConfig?.activeParentId; const activeLink = window.appConfig?.activeLink; //console.log('[Navigation] Configuration:', { activeParentId, activeLink }); // CORRECTION : Si "Accueil" mais activeParentId != 0, corriger if (activeLink === 'Accueil' && activeParentId !== '0') { //console.warn('[Navigation] Correction: Accueil devrait être menu 0, pas', activeParentId); this.activeMenuId = 'submenu0'; // Mettre à jour la config if (window.appConfig) { window.appConfig.activeParentId = '0'; } } // Cas normal else if (activeParentId !== null && activeParentId !== '') { this.activeMenuId = `submenu${activeParentId}`; } else { this.activeMenuId = null; } //console.log('[Navigation] Menu actif final:', this.activeMenuId); // Appliquer if (this.activeMenuId) { setTimeout(() => { this.openMenu(this.activeMenuId); this.currentOpenMenu = this.activeMenuId; }, 100); } } /** * Configurer les comportements interactifs des menus * @private - Méthode d'attachement des événements */ setupMenuBehavior() { // Pour chaque lien de menu avec sous-menu (dépliable) document.querySelectorAll('.nav-link[data-bs-toggle="collapse"]').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const targetId = link.getAttribute('href').substring(1); // Enlever le # //console.log('[Navigation] Clic détecté sur menu:', targetId); // Logique de basculement intelligent if (this.currentOpenMenu === targetId) { // Si on clique sur le menu déjà ouvert, on le ferme this.closeMenu(targetId); this.currentOpenMenu = null; } else { // Fermer le menu précédent si différent if (this.currentOpenMenu && this.currentOpenMenu !== targetId) { this.closeMenu(this.currentOpenMenu); } // Ouvrir le nouveau menu this.openMenu(targetId); this.currentOpenMenu = targetId; } }); }); // Fermer les menus en cliquant en dehors de la sidebar document.addEventListener('click', (e) => { if (!e.target.closest('.app-sidebar')) { // Ne pas fermer le menu actif s'il correspond à la page courante if (this.currentOpenMenu !== this.activeMenuId) { this.closeAllMenus(); } } }); // Empêcher la fermeture accidentelle en cliquant dans le sous-menu document.querySelectorAll('.nav-submenu').forEach(menu => { menu.addEventListener('click', (e) => { e.stopPropagation(); }); }); } /** * Ouvrir un menu spécifique * @param {string} menuId - ID du menu à ouvrir * @private - Méthode d'ouverture de menu */ openMenu(menuId) { const menu = document.getElementById(menuId); const link = document.querySelector(`[href="#${menuId}"]`); if (menu && link) { // Vérifier si le menu est déjà ouvert if (menu.classList.contains('show')) { //console.log('[Navigation] Menu déjà ouvert:', menuId); return; } //console.log('[Navigation] Ouverture du menu:', menuId); // 1. Fermer tous les autres menus document.querySelectorAll('.nav-submenu.show').forEach(otherMenu => { if (otherMenu.id !== menuId) { otherMenu.classList.remove('show'); const otherLink = document.querySelector(`[href="#${otherMenu.id}"]`); if (otherLink) { otherLink.setAttribute('aria-expanded', 'false'); otherLink.classList.remove('active'); const arrow = otherLink.querySelector('.nav-arrow'); if (arrow) arrow.style.transform = 'rotate(0deg)'; } } }); // 2. Ouvrir le menu sélectionné avec animations menu.classList.add('show'); link.setAttribute('aria-expanded', 'true'); link.classList.add('active'); // 3. Animer la flèche const arrow = link.querySelector('.nav-arrow'); if (arrow) arrow.style.transform = 'rotate(90deg)'; // 4. Annoncer pour l'accessibilité const menuName = link.querySelector('.nav-text')?.textContent || 'Menu'; window.appUX?.accessibility.announce(`${menuName} ouvert`); } } /** * Fermer un menu spécifique * @param {string} menuId - ID du menu à fermer * @param {boolean} force - Forcer la fermeture même pour le menu actif * @private - Méthode de fermeture de menu */ closeMenu(menuId, force = false) { const menu = document.getElementById(menuId); const link = document.querySelector(`[href="#${menuId}"]`); if (menu && link) { // Ne pas fermer le menu actif s'il correspond à la page courante (sauf si forcé) if (!force && menuId === this.activeMenuId) { //console.log('[Navigation] Menu actif, fermeture bloquée:', menuId); return; } //console.log('[Navigation] Fermeture du menu:', menuId); // Appliquer les changements visuels menu.classList.remove('show'); link.setAttribute('aria-expanded', 'false'); link.classList.remove('active'); // Réinitialiser la flèche const arrow = link.querySelector('.nav-arrow'); if (arrow) arrow.style.transform = 'rotate(0deg)'; } } /** * Fermer tous les menus (sauf le menu actif) * @public - Méthode accessible globalement */ closeAllMenus() { //console.log('[Navigation] Fermeture de tous les menus (sauf actif)'); document.querySelectorAll('.nav-submenu.show').forEach(menu => { if (menu.id !== this.activeMenuId) { this.closeMenu(menu.id); } }); // Réinitialiser l'état si aucun menu actif if (!this.activeMenuId) { this.currentOpenMenu = null; } } /** * Fermer tous les menus sauf celui actif * @public - Alias pour closeAllMenus avec nom plus explicite */ closeAllExceptActive() { this.closeAllMenus(); } /** * Configurer la navigation au clavier * @private - Méthode d'accessibilité clavier */ setupKeyboardNavigation() { document.addEventListener('keydown', (e) => { // Échap: fermer tous les menus if (e.key === 'Escape') { this.closeAllMenus(); } // Flèches: navigation dans les menus if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { const focused = document.activeElement; if (focused.classList.contains('nav-link')) { e.preventDefault(); this.navigateMenu(focused, e.key); } } }); } /** * Naviguer dans les menus avec les flèches * @param {HTMLElement} currentElement - Élément actuellement focus * @param {string} direction - Direction de navigation ('ArrowDown' ou 'ArrowUp') * @private - Méthode de navigation clavier */ navigateMenu(currentElement, direction) { const allLinks = Array.from(document.querySelectorAll('.nav-link')); const currentIndex = allLinks.indexOf(currentElement); if (direction === 'ArrowDown' && currentIndex < allLinks.length - 1) { allLinks[currentIndex + 1].focus(); } else if (direction === 'ArrowUp' && currentIndex > 0) { allLinks[currentIndex - 1].focus(); } } /** * Vérifier la cohérence des menus * @private - Méthode de validation */ validateMenuConsistency() { //console.log('[Navigation] Vérification de cohérence...'); // 1. Vérifier que le menu actif est ouvert if (this.activeMenuId) { const activeMenu = document.getElementById(this.activeMenuId); if (activeMenu && !activeMenu.classList.contains('show')) { //console.warn('[Navigation] Menu actif non ouvert, correction...'); this.openMenu(this.activeMenuId); } } // 2. Vérifier qu'un seul menu est ouvert const openMenus = document.querySelectorAll('.nav-submenu.show'); if (openMenus.length > 1) { //console.warn(`[Navigation] ${openMenus.length} menus ouverts, correction...`); this.forceSingleMenu(); } // 3. Vérifier la correspondance page/menu this.validatePageMenuMatch(); } /** * Valider la correspondance entre la page et le menu ouvert * @private - Méthode de validation avancée */ validatePageMenuMatch() { const activeLink = window.appConfig?.activeLink; if (!activeLink) return; // Chercher si la page active est dans le menu ouvert const openMenu = document.querySelector('.nav-submenu.show'); if (openMenu) { const hasActiveLink = openMenu.querySelector(`a[href*="${activeLink}"]`); if (!hasActiveLink) { //console.warn(`[Navigation] Page "${activeLink}" non trouvée dans le menu ouvert ${openMenu.id}`); // Essayer de trouver le bon menu this.autoDetectCorrectMenu(); } } } /** * Détecter automatiquement le menu correct * @private - Méthode d'auto-détection */ autoDetectCorrectMenu() { const activeLink = window.appConfig?.activeLink; if (!activeLink) return; //console.log(`[Navigation] Auto-détection du menu pour "${activeLink}"...`); // Chercher dans tous les menus document.querySelectorAll('.nav-submenu').forEach(menu => { const link = menu.querySelector(`a[href*="${activeLink}"]`); if (link) { const correctMenuId = menu.id; //console.log(`[Navigation] Trouvé dans ${correctMenuId}`); // Mettre à jour this.activeMenuId = correctMenuId; if (window.appConfig) { const menuNum = correctMenuId.replace('submenu', ''); window.appConfig.activeParentId = menuNum; } // Ouvrir le bon menu setTimeout(() => { this.openMenu(correctMenuId); this.currentOpenMenu = correctMenuId; }, 100); } }); } /** * Ouvrir un menu par son ID (méthode publique) * @param {string} menuId - ID du menu à ouvrir * @public - Méthode accessible globalement */ openMenuById(menuId) { this.openMenu(menuId); this.currentOpenMenu = menuId; } } // ============================================================================ // SOUS-CLASSE - ContextPanelManager (PANEL CONTEXTUEL) // ============================================================================ /** * Gestionnaire du panneau contextuel latéral * Contrôle l'affichage et les interactions du panel d'outils */ class ContextPanelManager { /** * Constructeur - Initialise les références DOM */ constructor() { this.panel = document.getElementById('contextPanel'); this.toggleButton = document.querySelector('.context-toggle'); this.proximityArea = document.querySelector('.proximity-hover-area'); this.isOpen = false; this.isInitialized = false; } /** * Initialiser le panel contextuel * @public - Méthode appelée par UXManager */ init() { if (this.isInitialized || !this.panel) return; //console.log('[Context Panel] Initialisation...'); this.setupProximityDetection(); this.setupKeyboardControls(); this.isInitialized = true; } /** * Configurer la détection de proximité (hover) * @private - Méthode d'interaction au survol */ setupProximityDetection() { if (!this.proximityArea || !this.toggleButton) return; // Animation au survol de la zone de proximité this.proximityArea.addEventListener('mouseenter', () => { this.toggleButton.style.opacity = '1'; this.toggleButton.style.transform = 'scale(1.1)'; }); this.proximityArea.addEventListener('mouseleave', () => { if (!this.isOpen) { this.toggleButton.style.opacity = '0.2'; this.toggleButton.style.transform = 'scale(1)'; } }); // Opacité initiale this.toggleButton.style.opacity = '0.2'; this.toggleButton.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; } /** * Configurer les contrôles clavier * @private - Méthode d'accessibilité clavier */ setupKeyboardControls() { document.addEventListener('keydown', (e) => { // Ctrl+Shift+C : basculer le panel if (e.ctrlKey && e.shiftKey && e.key === 'C') { e.preventDefault(); this.toggle(); } // Échap : fermer le panel si ouvert if (e.key === 'Escape' && this.isOpen) { this.close(); } }); } /** * Basculer l'état du panel (ouvrir/fermer) * @public - Méthode accessible globalement */ toggle() { if (this.isOpen) { this.close(); } else { this.open(); } } /** * Ouvrir le panel contextuel * @private - Méthode d'ouverture */ open() { this.panel.classList.add('open'); this.toggleButton.setAttribute('aria-expanded', 'true'); this.toggleButton.style.opacity = '1'; this.isOpen = true; // Focus sur le bouton de fermeture pour l'accessibilité setTimeout(() => { const closeBtn = this.panel.querySelector('.context-close'); if (closeBtn) closeBtn.focus(); }, 300); //console.log('[Context Panel] Ouvert'); window.appUX?.accessibility.announce('Panneau contextuel ouvert'); } /** * Fermer le panel contextuel * @private - Méthode de fermeture */ close() { this.panel.classList.remove('open'); this.toggleButton.setAttribute('aria-expanded', 'false'); this.toggleButton.style.opacity = '0.2'; this.isOpen = false; // Retourner le focus au bouton toggle this.toggleButton.focus(); //console.log('[Context Panel] Fermé'); window.appUX?.accessibility.announce('Panneau contextuel fermé'); } } // ============================================================================ // SOUS-CLASSE - NotificationManager (GESTION DES NOTIFICATIONS) // ============================================================================ /** * Gestionnaire des notifications et messages système */ class NotificationManager { /** * Constructeur - Initialise le compteur de notifications */ constructor() { this.countElement = document.getElementById('notificationCount'); this.unreadCount = 0; this.pollingInterval = null; } /** * Initialiser le système de notifications * @public - Méthode appelée par UXManager */ init() { //console.log('[Notifications] Initialisation...'); this.loadNotifications(); this.setupPolling(); } /** * Charger les notifications depuis le serveur * @private - Méthode de chargement initial */ loadNotifications() { // Simulation - À remplacer par un appel API réel const mockCount = 3; // Exemple : 3 notifications non lues this.updateCount(mockCount); //console.log('[Notifications] Chargement simulé:', mockCount, 'notification(s)'); } /** * Mettre à jour le compteur de notifications * @param {number} count - Nouveau nombre de notifications * @private - Méthode de mise à jour d'affichage */ updateCount(count) { this.unreadCount = count; if (this.countElement) { // Mettre à jour l'élément visuel this.countElement.textContent = count; this.countElement.style.display = count > 0 ? 'flex' : 'none'; // Mettre à jour le titre de la page const baseTitle = document.title.replace(/^\(\d+\)\s*/, ''); document.title = count > 0 ? `(${count}) ${baseTitle}` : baseTitle; // Annoncer pour l'accessibilité si nouveau message if (count > 0) { const message = window.appConfig?.isAnglophone ? `${count} new notification${count > 1 ? 's' : ''}` : `${count} nouvelle${count > 1 ? 's' : ''} notification${count > 1 ? 's' : ''}`; window.appUX?.accessibility.announce(message); } } } /** * Configurer le polling pour les nouvelles notifications * @private - Méthode de vérification périodique */ setupPolling() { // Vérifier les nouvelles notifications toutes les 30 secondes this.pollingInterval = setInterval(() => { this.checkNewNotifications(); }, 30000); } /** * Vérifier les nouvelles notifications (méthode à implémenter) * @private - Méthode d'appel API */ checkNewNotifications() { // À implémenter : appel AJAX pour vérifier les nouvelles notifications // console.log('[Notifications] Vérification des nouvelles notifications...'); } /** * Afficher la modale des messages * @public - Méthode accessible globalement */ showMessagesModal() { const modalElement = document.getElementById('messagesModal'); if (!modalElement) { //console.error('[Notifications] Modale des messages non trouvée'); return; } const modal = new bootstrap.Modal(modalElement); modal.show(); // Charger les messages this.loadMessages(); //console.log('[Notifications] Affichage de la modale des messages'); } /** * Charger les messages dans la modale * @private - Méthode de chargement de contenu */ loadMessages() { const container = document.getElementById('div_messagerie'); if (!container) return; // Simulation de chargement - À remplacer par un appel API réel const messages = window.appConfig?.isAnglophone ? [ { title: 'New prescription', time: '5 minutes ago', content: 'A new prescription has been created for the beneficiary.' }, { title: 'Exemption approved', time: '2 hours ago', content: 'Your exemption request has been approved.' } ] : [ { title: 'Nouvelle prescription', time: 'Il y a 5 minutes', content: 'Une nouvelle prescription a été créée pour le bénéficiaire.' }, { title: 'Dérogation approuvée', time: 'Il y a 2 heures', content: 'Votre demande de dérogation a été approuvée.' } ]; // Générer le HTML des messages let messagesHTML = '
'; messages.forEach(msg => { messagesHTML += `
${msg.title}
${msg.time}

${msg.content}

`; }); messagesHTML += '
'; container.innerHTML = messagesHTML; } } // ============================================================================ // SOUS-CLASSE - AccessibilityManager (ACCESSIBILITÉ) // ============================================================================ /** * Gestionnaire d'accessibilité (WCAG, ARIA, etc.) */ class AccessibilityManager { /** * Constructeur - Initialise les fonctionnalités d'accessibilité */ constructor() { this.liveRegion = null; this.reducedMotion = false; } /** * Initialiser les fonctionnalités d'accessibilité * @public - Méthode appelée par UXManager */ init() { //console.log('[Accessibility] Initialisation des fonctionnalités d\'accessibilités...'); this.setupFocusManagement(); this.setupAriaLiveRegions(); this.detectReducedMotion(); this.setupSkipLinks(); //console.log('[Accessibility] Fonctionnalités d\'accessibilité activées'); } /** * Configurer la gestion du focus (modales, etc.) * @private - Méthode de gestion du focus */ setupFocusManagement() { // Gérer le focus pour les modales Bootstrap document.addEventListener('shown.bs.modal', (e) => { const modal = e.target; const focusable = modal.querySelector( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); if (focusable) focusable.focus(); }); // Piéger le focus dans les modales document.addEventListener('keydown', (e) => { if (e.key === 'Tab' && document.querySelector('.modal.show')) { this.trapFocusInModal(e); } }); } /** * Piéger le focus dans une modale * @param {KeyboardEvent} e - Événement clavier * @private - Méthode de piégeage de focus */ trapFocusInModal(e) { const modal = document.querySelector('.modal.show'); if (!modal) return; const focusable = modal.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const firstFocusable = focusable[0]; const lastFocusable = focusable[focusable.length - 1]; if (e.shiftKey) { // Shift + Tab if (document.activeElement === firstFocusable) { e.preventDefault(); lastFocusable.focus(); } } else { // Tab seul if (document.activeElement === lastFocusable) { e.preventDefault(); firstFocusable.focus(); } } } /** * Configurer les régions ARIA live (annonces) * @private - Méthode de création de régions ARIA */ setupAriaLiveRegions() { // Créer une région ARIA live pour les annonces this.liveRegion = document.createElement('div'); this.liveRegion.setAttribute('aria-live', 'polite'); this.liveRegion.setAttribute('aria-atomic', 'true'); this.liveRegion.className = 'visually-hidden'; this.liveRegion.setAttribute('role', 'status'); document.body.appendChild(this.liveRegion); //console.log('[Accessibility] Région ARIA live créée'); } /** * Annoncer un message pour les lecteurs d'écran * @param {string} message - Message à annoncer * @public - Méthode accessible globalement */ announce(message) { if (!this.liveRegion || !message) return; // Effacer l'ancien message this.liveRegion.textContent = ''; // Ajouter le nouveau message après un délai setTimeout(() => { this.liveRegion.textContent = message; //console.log('[Accessibility] Annonce:', message); }, 100); } /** * Détecter la préférence de réduction des animations * @private - Méthode de détection de préférence utilisateur */ detectReducedMotion() { const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); this.reducedMotion = mediaQuery.matches; if (this.reducedMotion) { document.documentElement.classList.add('reduced-motion'); //console.log('[Accessibility] Réduction des mouvements activée'); } // Écouter les changements de préférence mediaQuery.addEventListener('change', (e) => { this.reducedMotion = e.matches; if (this.reducedMotion) { document.documentElement.classList.add('reduced-motion'); } else { document.documentElement.classList.remove('reduced-motion'); } //console.log('[Accessibility] Préférence de mouvement mise à jour:', this.reducedMotion); }); } /** * Configurer les liens d'évitement (skip links) * @private - Méthode d'accessibilité navigation */ setupSkipLinks() { // Vérifier si les skip links existent déjà const existingSkipLinks = document.querySelector('.skip-links'); if (existingSkipLinks) return; // Créer les skip links const skipLinks = document.createElement('nav'); skipLinks.className = 'skip-links'; skipLinks.setAttribute('aria-label', 'Liens d\'évitement'); skipLinks.innerHTML = ` `; // Insérer au début du body document.body.insertBefore(skipLinks, document.body.firstChild); //console.log('[Accessibility] Liens d\'évitement créés'); } } // ============================================================================ // SOUS-CLASSE - PerformanceManager (PERFORMANCES) // ============================================================================ /** * Gestionnaire des performances et optimisation */ class PerformanceManager { /** * Constructeur - Initialise le monitoring de performance */ constructor() { this.observers = []; this.metrics = {}; } /** * Initialiser le monitoring de performance * @public - Méthode appelée par UXManager */ init() { //console.log('[Performance] Initialisation du monitoring...'); this.monitorPerformance(); this.setupLazyLoading(); this.setupIntersectionObserver(); this.setupIdleCallback(); //console.log('[Performance] Monitoring activé'); } /** * Monitorer les métriques de performance web vitales * @private - Méthode de monitoring Core Web Vitals */ monitorPerformance() { // Monitoring LCP (Largest Contentful Paint) if ('PerformanceObserver' in window) { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { switch (entry.entryType) { case 'largest-contentful-paint': this.metrics.lcp = entry.startTime; //console.log('[Performance] LCP:', entry.startTime.toFixed(2), 'ms'); break; case 'first-input': this.metrics.fid = entry.processingStart - entry.startTime; //console.log('[Performance] FID:', this.metrics.fid.toFixed(2), 'ms'); break; case 'layout-shift': if (!entry.hadRecentInput) { this.metrics.cls = (this.metrics.cls || 0) + entry.value; //console.log('[Performance] CLS cumulé:', this.metrics.cls.toFixed(4)); } break; } } }); observer.observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift'] }); this.observers.push(observer); } } /** * Configurer le lazy loading pour les images * @private - Méthode d'optimisation de chargement */ setupLazyLoading() { // Utiliser le lazy loading natif si disponible if ('loading' in HTMLImageElement.prototype) { document.querySelectorAll('img[loading="lazy"]').forEach(img => { img.addEventListener('load', () => { img.classList.add('loaded'); //console.log('[Performance] Image chargée:', img.src); }); img.addEventListener('error', () => { //console.warn('[Performance] Erreur de chargement image:', img.src); }); }); } else { // Fallback pour les navigateurs plus anciens this.setupIntersectionObserverForImages(); } } /** * Configurer l'Intersection Observer pour les images (fallback) * @private - Méthode de lazy loading alternatif */ setupIntersectionObserverForImages() { const lazyImages = document.querySelectorAll('img[data-src]'); const imageObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.classList.add('loaded'); imageObserver.unobserve(img); } }); }); lazyImages.forEach(img => imageObserver.observe(img)); this.observers.push(imageObserver); } /** * Configurer l'Intersection Observer pour les éléments * @private - Méthode d'animation au défilement */ setupIntersectionObserver() { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('in-view'); observer.unobserve(entry.target); } }); }, { threshold: 0.1, rootMargin: '50px' }); // Observer les cartes de contenu document.querySelectorAll('.content-card').forEach(card => { observer.observe(card); }); this.observers.push(observer); } /** * Configurer les callbacks idle (traitements en arrière-plan) * @private - Méthode d'optimisation des ressources */ setupIdleCallback() { if ('requestIdleCallback' in window) { requestIdleCallback(() => { // Tâches de basse priorité à exécuter pendant les périodes d'inactivité this.cleanupOldData(); this.prefetchImportantLinks(); }, { timeout: 2000 }); } } /** * Nettoyer les données anciennes * @private - Méthode de nettoyage */ cleanupOldData() { // Exemple : nettoyer le cache local si trop volumineux // À implémenter selon les besoins } /** * Précharger les liens importants * @private - Méthode de préchargement */ prefetchImportantLinks() { // Exemple : précharger les pages fréquemment visitées // À implémenter selon l'analyse d'usage } /** * Nettoyer les observers (pour la mémoire) * @public - Méthode de nettoyage */ cleanup() { this.observers.forEach(observer => { if (observer && typeof observer.disconnect === 'function') { observer.disconnect(); } }); this.observers = []; } } // ============================================================================ // SOUS-CLASSE - LanguageManager (GESTION DES LANGUES) // ============================================================================ /** * Gestionnaire du changement de langue */ class LanguageManager { /** * Constructeur - Initialise les textes selon la langue */ constructor() { this.currentLang = null; } /** * Initialiser la gestion des langues * @public - Méthode appelée par UXManager */ init() { this.detectCurrentLanguage(); this.updateLanguageTexts(); //console.log('[Language] Gestionnaire de langue initialisé:', this.currentLang); } /** * Détecter la langue actuelle depuis la configuration * @private - Méthode de détection */ detectCurrentLanguage() { this.currentLang = window.appConfig?.isAnglophone ? 'en_US' : 'fr_FR'; } /** * Changer la langue de l'interface * @public - Méthode accessible globalement */ changeLanguage() { const newLang = this.currentLang === 'en_US' ? 'fr_FR' : 'en_US'; const messages = window.appConfig?.isAnglophone ? { title: 'Change Language', text: 'Switch to French?', confirm: 'Switch', cancel: 'Cancel' } : { title: 'Changer de langue', text: 'Passer en Anglais?', confirm: 'Changer', cancel: 'Annuler' }; // Demander confirmation à l'utilisateur Swal.fire({ title: messages.title, text: messages.text, icon: 'question', showCancelButton: true, confirmButtonText: messages.confirm, cancelButtonText: messages.cancel, allowOutsideClick: true, allowEscapeKey: true }).then((result) => { if (result.isConfirmed) { // Rediriger vers la page de connexion avec le nouveau paramètre de langue const redirectUrl = window.appConfig?.racineWeb + 'Connexion/index?langue=' + newLang; //console.log('[Language] Changement de langue vers:', newLang); window.location.href = redirectUrl; } }); } /** * Mettre à jour les textes dynamiques selon la langue * @private - Méthode de mise à jour des textes */ updateLanguageTexts() { // À implémenter: mise à jour des textes dynamiques non gérés par le serveur // Cette méthode peut être étendue pour gérer les textes côté client // Exemple: Mettre à jour le titre de la page en fonction de la langue const pageTitle = document.title; if (this.currentLang === 'en_US' && !pageTitle.includes('RH Portal')) { // Traduire vers l'anglais si nécessaire } } /** * Obtenir la langue courante * @returns {string} Code de langue (fr_FR ou en_US) * @public - Méthode d'accès à la langue */ getCurrentLanguage() { return this.currentLang; } } // ============================================================================ // INITIALISATION ET EXPOSITION GLOBALE // ============================================================================ /** * Initialiser l'application après le chargement du DOM * Point d'entrée principal de l'application */ document.addEventListener('DOMContentLoaded', () => { //console.log('[App] DOM chargé, initialisation de l\'application...'); // Vérifier que la configuration est disponible if (!window.appConfig) { //console.warn('[App] Configuration non trouvée, création par défaut...'); window.appConfig = { activeParentId: '', activeChildId: '', activeLink: '', racineWeb: '/', baseUrl: '/', isAnglophone: false, debugMode: false }; } // Créer l'instance principale window.appUX = new UXManager(); // Marquer le chargement comme terminé setTimeout(() => { document.body.classList.add('loaded'); //console.log('[App] Portail RH Inter Santé initialisé avec succès'); // Émettre un événement personnalisé document.dispatchEvent(new CustomEvent('app:initialized')); }, 100); }); // ============================================================================ // FONCTIONS GLOBALES D'ACCÈS RAPIDE // ============================================================================ /** * Obtenir le gestionnaire de notifications * @returns {NotificationManager} Instance du gestionnaire de notifications */ function appNotifications() { return window.appUX?.notifications; } /** * Obtenir le gestionnaire de navigation * @returns {NavigationManager} Instance du gestionnaire de navigation */ function appNavigation() { return window.appUX?.navigation; } /** * Obtenir le gestionnaire de langues * @returns {LanguageManager} Instance du gestionnaire de langues */ function appLanguage() { return window.appUX?.language; } /** * Obtenir le gestionnaire d'accessibilité * @returns {AccessibilityManager} Instance du gestionnaire d'accessibilité */ function appAccessibility() { return window.appUX?.accessibility; } // ============================================================================ // API PUBLIQUE GLOBALE // ============================================================================ /** * API publique globale pour l'interaction avec le UX Manager * Permet un accès simple aux fonctionnalités principales */ window.uxManager = { /** * Basculer le panneau contextuel */ toggleContextPanel: () => window.appUX?.toggleContextPanel(), /** * Afficher les notifications */ showNotifications: () => window.appUX?.notifications?.showMessagesModal(), /** * Changer la langue */ changeLanguage: () => window.appUX?.language?.changeLanguage(), /** * Annoncer un message pour l'accessibilité * @param {string} message - Message à annoncer */ announce: (message) => window.appUX?.accessibility?.announce(message), /** * Ouvrir un menu spécifique * @param {string} menuId - ID du menu à ouvrir */ openMenu: (menuId) => window.appUX?.navigation?.openMenuById(menuId), /** * Fermer tous les menus */ closeAllMenus: () => window.appUX?.navigation?.closeAllMenus(), /** * Tester le système (mode debug) */ testSystem: () => { if (window.appConfig?.debugMode) { /*console.group('[UX Manager] Test du système'); console.log('App UX:', window.appUX); console.log('Navigation:', window.appUX?.navigation); console.log('Config:', window.appConfig); console.groupEnd();*/ } } }; // ============================================================================ // EXPORT POUR LES MODULES ES6 (si utilisé avec bundler) // ============================================================================ // Note: Décommentez si vous utilisez un bundler moderne /* export { UXManager, NavigationManager, ContextPanelManager, NotificationManager, AccessibilityManager, PerformanceManager, LanguageManager }; */ //console.log('[UX Manager] Module chargé et prêt à l\'emploi');