diff --git a/Bootstrap_new/js/ux-manager.js b/Bootstrap_new/js/ux-manager.js index 0740453..e746ecb 100644 --- a/Bootstrap_new/js/ux-manager.js +++ b/Bootstrap_new/js/ux-manager.js @@ -1,6 +1,25 @@ -// UX Manager - Gestionnaire d'expérience utilisateur modulaire +/** + * UX Manager - Gestionnaire d'expérience utilisateur modulaire + * Version: 2.0.0 + * Description: Gère tous les aspects de l'UX : navigation, panels contextuels, notifications, accessibilité + * Auteur: Portail RH Inter Santé + * Date: 2025.12.22 + */ + +// ============================================================================ +// 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(); @@ -8,11 +27,18 @@ class UXManager { 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...'); + 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(); @@ -20,82 +46,200 @@ class UXManager { this.performance.init(); this.language.init(); - // Vérifier la session + // Gestion de session utilisateur this.checkSession(); - // Initialiser le Service Worker + // 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(); } + /** + * Basculer l'état de la sidebar (menu latéral) + * @public - Méthode accessible globalement + */ toggleSidebar() { const sidebar = document.getElementById('sidebar'); - sidebar.classList.toggle('show'); + if (sidebar) { + sidebar.classList.toggle('show'); + // Annoncer le changement pour l'accessibilité + this.accessibility.announce( + sidebar.classList.contains('show') + ? 'Menu latéral ouvert' + : 'Menu latéral fermé' + ); + } } + /** + * Vérifier et gérer l'expiration de session utilisateur + * @private - Méthode interne de gestion de session + */ checkSession() { - const dureeSession = parseInt(document.getElementById('dureeSession').value) || 30; - const dureeMinutes = dureeSession * 60 * 1000; + // 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érifier toutes les minutes + }, 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; - const message = isAnglophone - ? 'Your session will expire soon. Do you want to extend it?' - : 'Votre session va bientôt expirer. Souhaitez-vous la prolonger?'; - - Swal.fire({ + const isAnglophone = window.appConfig?.isAnglophone || false; + const messages = { title: isAnglophone ? 'Session Warning' : 'Avertissement de session', - text: message, + 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: isAnglophone ? 'Extend' : 'Prolonger', - cancelButtonText: isAnglophone ? 'Logout' : 'Déconnexion' + 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 { - window.location.href = window.appConfig.racineWeb + 'Connexion/deconnecter'; + // 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 prêt:', registration.scope); + console.log('[UX Manager] Service Worker actif:', registration.scope); + }).catch(error => { + console.warn('[UX Manager] Service Worker non disponible:', error); }); } } } -// Navigation Manager - Gestion des menus (VERSION CORRIGÉE) +// ============================================================================ +// 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; - this.activeMenuId = null; // Menu actif basé sur la page courante + 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(); + + 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() { - // Déterminer le menu actif basé sur la page courante + // Récupérer la configuration de la page active const activeParentId = window.appConfig?.activeParentId; const activeLink = window.appConfig?.activeLink; @@ -103,32 +247,36 @@ class NavigationManager { if (activeParentId !== null && activeParentId !== '') { this.activeMenuId = `submenu${activeParentId}`; - console.log('[Navigation] Menu actif détecté:', this.activeMenuId); + console.log('[Navigation] Menu actif configuré:', this.activeMenuId); - // Ouvrir seulement le menu actif + // Ouvrir seulement le menu actif (avec délai pour le DOM) setTimeout(() => { this.openMenu(this.activeMenuId); this.currentOpenMenu = this.activeMenuId; }, 100); } else { console.log('[Navigation] Aucun menu actif détecté'); - // Fermer tous les menus par défaut this.closeAllMenus(); } } + /** + * Configurer les comportements interactifs des menus + * @private - Méthode d'attachement des événements + */ setupMenuBehavior() { - // Pour chaque lien de menu avec sous-menu + // 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); - console.log('[Navigation] Clic sur menu:', targetId); + const targetId = link.getAttribute('href').substring(1); // Enlever le # + console.log('[Navigation] Clic détecté sur menu:', targetId); - // Si on clique sur le menu déjà ouvert, on le ferme + // 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 { @@ -144,7 +292,7 @@ class NavigationManager { }); }); - // Fermer les menus en cliquant ailleurs + // 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 @@ -154,7 +302,7 @@ class NavigationManager { } }); - // Empêcher la fermeture du menu actif + // Empêcher la fermeture accidentelle en cliquant dans le sous-menu document.querySelectorAll('.nav-submenu').forEach(menu => { menu.addEventListener('click', (e) => { e.stopPropagation(); @@ -162,12 +310,19 @@ class NavigationManager { }); } + /** + * 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) { - // Retirer 'show' de tous les autres menus + 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'); @@ -181,61 +336,90 @@ class NavigationManager { } }); - // Ouvrir le menu sélectionné + // 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)'; - console.log('[Navigation] Menu ouvert:', menuId); + // 4. Annoncer pour l'accessibilité + const menuName = link.querySelector('.nav-text')?.textContent || 'Menu'; + window.appUX?.accessibility.announce(`${menuName} ouvert`); } } - closeMenu(menuId) { + /** + * 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 si c'est celui de la page courante - if (menuId === this.activeMenuId) { - console.log('[Navigation] Menu actif, ne pas fermer:', menuId); + // 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)'; - - console.log('[Navigation] Menu fermé:', menuId); } } + /** + * Fermer tous les menus (sauf le menu actif) + * @public - Méthode accessible globalement + */ closeAllMenus() { - // Ne fermer que les menus qui ne sont pas actifs + 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); } }); - // Si aucun menu actif, réinitialiser + // 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(); } - // Navigation par flèches + // Flèches: navigation dans les menus if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { const focused = document.activeElement; if (focused.classList.contains('nav-link')) { @@ -246,6 +430,12 @@ class NavigationManager { }); } + /** + * 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); @@ -257,35 +447,60 @@ class NavigationManager { } } - // Méthode publique pour forcer l'ouverture d'un menu + /** + * 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; } - - // Méthode publique pour forcer la fermeture - closeAllExceptActive() { - this.closeAllMenus(); - } } -// Context Panel Manager - Gestion du panneau de contexte +// ============================================================================ +// 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) return; + 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)'; @@ -298,23 +513,34 @@ class ContextPanelManager { } }); - // Initial opacity + // 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(); @@ -323,18 +549,30 @@ class ContextPanelManager { } } + /** + * 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 + // Focus sur le bouton de fermeture pour l'accessibilité setTimeout(() => { - this.panel.querySelector('.context-close').focus(); + 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'); @@ -343,228 +581,757 @@ class ContextPanelManager { // Retourner le focus au bouton toggle this.toggleButton.focus(); + + console.log('[Context Panel] Fermé'); + window.appUX?.accessibility.announce('Panneau contextuel fermé'); } } -// Notification Manager +// ============================================================================ +// 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() { - // Simuler le chargement des notifications - this.updateCount(3); // Exemple: 3 notifications non lues + // 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 - setInterval(() => { - // À implémenter: appel AJAX pour vérifier les nouvelles notifications - // this.checkNewNotifications(); + 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 modal = new bootstrap.Modal(document.getElementById('messagesModal')); + 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; - // Simuler le chargement des messages - container.innerHTML = ` -
Une nouvelle prescription a été créée pour le bénéficiaire.
+${msg.content}
Votre demande de dérogation a été approuvée.
-