From a59c0183b312da7e8e919188ba15469cf67d151a Mon Sep 17 00:00:00 2001 From: KONE SOREL Date: Mon, 22 Dec 2025 09:43:39 +0000 Subject: [PATCH] Valide --- Bootstrap_new/js/ux-manager.js | 1007 ++++++++++++++++++++++++++++---- Vue/gabarit.php | 226 +++---- 2 files changed, 1014 insertions(+), 219 deletions(-) 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 = ` -
+ // 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 += `
-
Nouvelle prescription
- Il y a 5 minutes +
${msg.title}
+ ${msg.time}
-

Une nouvelle prescription a été créée pour le bénéficiaire.

+

${msg.content}

-
-
-
Dérogation approuvée
- Il y a 2 heures -
-

Votre demande de dérogation a été approuvée.

-
-
- `; + `; + }); + messagesHTML += '
'; + + container.innerHTML = messagesHTML; } } -// Accessibility Manager +// ============================================================================ +// 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 + // 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"])'); + 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); + } + }); } - setupAriaLiveRegions() { - // Créer une région ARIA live pour les notifications - const liveRegion = document.createElement('div'); - liveRegion.setAttribute('aria-live', 'polite'); - liveRegion.setAttribute('aria-atomic', 'true'); - liveRegion.className = 'visually-hidden'; - document.body.appendChild(liveRegion); + /** + * 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)'); - if (mediaQuery.matches) { + 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'); } } -// Performance Manager +// ============================================================================ +// 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()) { - if (entry.entryType === 'largest-contentful-paint') { - console.log('[Performance] LCP:', entry.startTime); + 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'] }); + 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() { - // Lazy loading pour les images + // 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 + 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 = []; } } -// Language Manager +// ============================================================================ +// 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 currentLang = window.appConfig.isAnglophone ? 'en_US' : 'fr_FR'; - const newLang = currentLang === 'en_US' ? 'fr_FR' : 'en_US'; - - // Demander confirmation - const message = window.appConfig.isAnglophone - ? 'Switch to French?' - : 'Passer en Anglais?'; + 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: window.appConfig.isAnglophone ? 'Change Language' : 'Changer de langue', - text: message, + title: messages.title, + text: messages.text, icon: 'question', showCancelButton: true, - confirmButtonText: window.appConfig.isAnglophone ? 'Switch' : 'Changer', - cancelButtonText: window.appConfig.isAnglophone ? 'Cancel' : 'Annuler' + 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 - window.location.href = window.appConfig.racineWeb + + const redirectUrl = window.appConfig?.racineWeb + 'Connexion/index?langue=' + newLang; + console.log('[Language] Changement de langue vers:', newLang); + window.location.href = redirectUrl; } }); } - init() { - // Initialiser les textes selon la langue - this.updateLanguageTexts(); + /** + * 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 + } } - updateLanguageTexts() { - // À implémenter: mise à jour des textes dynamiques + /** + * 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 de l'application +// ============================================================================ +// INITIALISATION ET EXPOSITION GLOBALE +// ============================================================================ + +/** + * Initialiser l'application après le chargement du DOM + * Point d'entrée principal de l'application + */ document.addEventListener('DOMContentLoaded', () => { - window.appUX = new UXManager(); - console.log('[App] Portail RH Inter Santé initialisé'); + console.log('[App] DOM chargé, initialisation de l\'application...'); - // Déclencher les animations après le chargement + // 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 +// ============================================================================ +// 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; } -// Export pour les scripts externes +/** + * 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(), + + /** + * Basculer la sidebar (menu latéral) + */ toggleSidebar: () => window.appUX?.toggleSidebar(), - showNotifications: () => window.appUX?.notifications.showMessagesModal() -}; \ No newline at end of file + + /** + * 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'); \ No newline at end of file diff --git a/Vue/gabarit.php b/Vue/gabarit.php index 837d2a5..f60b4fb 100755 --- a/Vue/gabarit.php +++ b/Vue/gabarit.php @@ -555,14 +555,64 @@ foreach ($menus as $key0 => $menuParent) { + + +