radiantrh/Bootstrap_new/js/ux-manager.js
2025-12-22 09:07:01 +00:00

570 lines
19 KiB
JavaScript

// UX Manager - Gestionnaire d'expérience utilisateur modulaire
class UXManager {
constructor() {
this.navigation = new NavigationManager();
this.contextPanel = new ContextPanelManager();
this.notifications = new NotificationManager();
this.accessibility = new AccessibilityManager();
this.performance = new PerformanceManager();
this.language = new LanguageManager();
this.init();
}
init() {
console.log('[UX Manager] Initialisation...');
this.navigation.init();
this.contextPanel.init();
this.notifications.init();
this.accessibility.init();
this.performance.init();
this.language.init();
// Vérifier la session
this.checkSession();
// Initialiser le Service Worker
this.initServiceWorker();
}
toggleContextPanel() {
this.contextPanel.toggle();
}
toggleSidebar() {
const sidebar = document.getElementById('sidebar');
sidebar.classList.toggle('show');
}
checkSession() {
const dureeSession = parseInt(document.getElementById('dureeSession').value) || 30;
const dureeMinutes = dureeSession * 60 * 1000;
setInterval(() => {
const derniereAction = sessionStorage.getItem('derniere_action');
const maintenant = Date.now();
if (derniereAction && (maintenant - derniereAction > dureeMinutes)) {
this.showSessionWarning();
}
}, 60000); // Vérifier toutes les minutes
}
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({
title: isAnglophone ? 'Session Warning' : 'Avertissement de session',
text: message,
icon: 'warning',
showCancelButton: true,
confirmButtonText: isAnglophone ? 'Extend' : 'Prolonger',
cancelButtonText: isAnglophone ? 'Logout' : 'Déconnexion'
}).then((result) => {
if (result.isConfirmed) {
sessionStorage.setItem('derniere_action', Date.now());
} else {
window.location.href = window.appConfig.racineWeb + 'Connexion/deconnecter';
}
});
}
initServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
console.log('[UX Manager] Service Worker prêt:', registration.scope);
});
}
}
}
// Navigation Manager - Gestion des menus (VERSION CORRIGÉE)
class NavigationManager {
constructor() {
this.currentOpenMenu = null;
this.activeMenuId = null; // Menu actif basé sur la page courante
}
init() {
this.setupActiveMenu();
this.setupMenuBehavior();
this.setupKeyboardNavigation();
}
setupActiveMenu() {
// Déterminer le menu actif basé sur la page courante
const activeParentId = window.appConfig?.activeParentId;
const activeLink = window.appConfig?.activeLink;
console.log('[Navigation] Page active:', activeLink, 'Parent ID:', activeParentId);
if (activeParentId !== null && activeParentId !== '') {
this.activeMenuId = `submenu${activeParentId}`;
console.log('[Navigation] Menu actif détecté:', this.activeMenuId);
// Ouvrir seulement le menu actif
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();
}
}
setupMenuBehavior() {
// Pour chaque lien de menu avec sous-menu
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);
// Si on clique sur le menu déjà ouvert, on le ferme
if (this.currentOpenMenu === targetId) {
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 ailleurs
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 du menu actif
document.querySelectorAll('.nav-submenu').forEach(menu => {
menu.addEventListener('click', (e) => {
e.stopPropagation();
});
});
}
openMenu(menuId) {
const menu = document.getElementById(menuId);
const link = document.querySelector(`[href="#${menuId}"]`);
if (menu && link) {
// Retirer 'show' de 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)';
}
}
});
// Ouvrir le menu sélectionné
menu.classList.add('show');
link.setAttribute('aria-expanded', 'true');
link.classList.add('active');
const arrow = link.querySelector('.nav-arrow');
if (arrow) arrow.style.transform = 'rotate(90deg)';
console.log('[Navigation] Menu ouvert:', menuId);
}
}
closeMenu(menuId) {
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);
return;
}
menu.classList.remove('show');
link.setAttribute('aria-expanded', 'false');
link.classList.remove('active');
const arrow = link.querySelector('.nav-arrow');
if (arrow) arrow.style.transform = 'rotate(0deg)';
console.log('[Navigation] Menu fermé:', menuId);
}
}
closeAllMenus() {
// Ne fermer que les menus qui ne sont pas actifs
document.querySelectorAll('.nav-submenu.show').forEach(menu => {
if (menu.id !== this.activeMenuId) {
this.closeMenu(menu.id);
}
});
// Si aucun menu actif, réinitialiser
if (!this.activeMenuId) {
this.currentOpenMenu = null;
}
}
setupKeyboardNavigation() {
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.closeAllMenus();
}
// Navigation par flèches
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
const focused = document.activeElement;
if (focused.classList.contains('nav-link')) {
e.preventDefault();
this.navigateMenu(focused, e.key);
}
}
});
}
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();
}
}
// Méthode publique pour forcer l'ouverture d'un menu
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
class ContextPanelManager {
constructor() {
this.panel = document.getElementById('contextPanel');
this.toggleButton = document.querySelector('.context-toggle');
this.proximityArea = document.querySelector('.proximity-hover-area');
this.isOpen = false;
}
init() {
this.setupProximityDetection();
this.setupKeyboardControls();
}
setupProximityDetection() {
if (!this.proximityArea) return;
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)';
}
});
// Initial opacity
this.toggleButton.style.opacity = '0.2';
}
setupKeyboardControls() {
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'C') {
e.preventDefault();
this.toggle();
}
if (e.key === 'Escape' && this.isOpen) {
this.close();
}
});
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
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
setTimeout(() => {
this.panel.querySelector('.context-close').focus();
}, 300);
}
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();
}
}
// Notification Manager
class NotificationManager {
constructor() {
this.countElement = document.getElementById('notificationCount');
this.unreadCount = 0;
}
init() {
this.loadNotifications();
this.setupPolling();
}
loadNotifications() {
// Simuler le chargement des notifications
this.updateCount(3); // Exemple: 3 notifications non lues
}
updateCount(count) {
this.unreadCount = count;
if (this.countElement) {
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;
}
}
setupPolling() {
// Vérifier les nouvelles notifications toutes les 30 secondes
setInterval(() => {
// À implémenter: appel AJAX pour vérifier les nouvelles notifications
// this.checkNewNotifications();
}, 30000);
}
showMessagesModal() {
const modal = new bootstrap.Modal(document.getElementById('messagesModal'));
modal.show();
// Charger les messages
this.loadMessages();
}
loadMessages() {
const container = document.getElementById('div_messagerie');
if (!container) return;
// Simuler le chargement des messages
container.innerHTML = `
<div class="list-group">
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">Nouvelle prescription</h6>
<small>Il y a 5 minutes</small>
</div>
<p class="mb-1">Une nouvelle prescription a été créée pour le bénéficiaire.</p>
</div>
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">Dérogation approuvée</h6>
<small>Il y a 2 heures</small>
</div>
<p class="mb-1">Votre demande de dérogation a été approuvée.</p>
</div>
</div>
`;
}
}
// Accessibility Manager
class AccessibilityManager {
init() {
this.setupFocusManagement();
this.setupAriaLiveRegions();
this.detectReducedMotion();
}
setupFocusManagement() {
// Gérer le focus pour les modales
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();
});
}
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);
}
detectReducedMotion() {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
if (mediaQuery.matches) {
document.documentElement.classList.add('reduced-motion');
}
}
}
// Performance Manager
class PerformanceManager {
init() {
this.monitorPerformance();
this.setupLazyLoading();
this.setupIntersectionObserver();
}
monitorPerformance() {
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);
}
}
});
observer.observe({ entryTypes: ['largest-contentful-paint', 'first-input'] });
}
}
setupLazyLoading() {
// Lazy loading pour les images
if ('loading' in HTMLImageElement.prototype) {
document.querySelectorAll('img[loading="lazy"]').forEach(img => {
img.addEventListener('load', () => {
img.classList.add('loaded');
});
});
}
}
setupIntersectionObserver() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('in-view');
}
});
}, {
threshold: 0.1
});
document.querySelectorAll('.content-card').forEach(card => {
observer.observe(card);
});
}
}
// Language Manager
class LanguageManager {
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?';
Swal.fire({
title: window.appConfig.isAnglophone ? 'Change Language' : 'Changer de langue',
text: message,
icon: 'question',
showCancelButton: true,
confirmButtonText: window.appConfig.isAnglophone ? 'Switch' : 'Changer',
cancelButtonText: window.appConfig.isAnglophone ? 'Cancel' : 'Annuler'
}).then((result) => {
if (result.isConfirmed) {
// Rediriger vers la page de connexion avec le nouveau paramètre de langue
window.location.href = window.appConfig.racineWeb +
'Connexion/index?langue=' + newLang;
}
});
}
init() {
// Initialiser les textes selon la langue
this.updateLanguageTexts();
}
updateLanguageTexts() {
// À implémenter: mise à jour des textes dynamiques
}
}
// Initialisation de l'application
document.addEventListener('DOMContentLoaded', () => {
window.appUX = new UXManager();
console.log('[App] Portail RH Inter Santé initialisé');
// Déclencher les animations après le chargement
setTimeout(() => {
document.body.classList.add('loaded');
}, 100);
});
// Fonctions globales
function appNotifications() {
return window.appUX?.notifications;
}
function appNavigation() {
return window.appUX?.navigation;
}
function appLanguage() {
return window.appUX?.language;
}
// Export pour les scripts externes
window.uxManager = {
toggleContextPanel: () => window.appUX?.toggleContextPanel(),
toggleSidebar: () => window.appUX?.toggleSidebar(),
showNotifications: () => window.appUX?.notifications.showMessagesModal()
};