diff --git a/Bootstrap_new/css/style_office.css b/Bootstrap_new/css/style_office.css index a415323..767541a 100644 --- a/Bootstrap_new/css/style_office.css +++ b/Bootstrap_new/css/style_office.css @@ -1,698 +1,738 @@ - /* Style Office pour le Portail RH*/ - :root { - /* PowerPoint/Office 2019 Color Palette */ - --office-primary: #b7472a; /* Sidebar color - PowerPoint accent */ - --office-secondary: #2b579a; /* Header/buttons - Office blue */ - --office-accent: #107c10; /* Success/active states - Excel green */ - --office-light: #f3f2f1; /* Background - Office gray */ - --office-common: #e6e6e6; /* Background - Office gray */ - --office-dark: #323130; /* Text color */ - --office-border: #d0d0d0; /* Borders */ - --office-hover: #f0f0f0; /* Hover states */ - --office-card: #ffffff; /* Card backgrounds */ - - --sidebar-width: 260px; - --sidebar-collapsed: 70px; - --header-height: 64px; - --transition-speed: 0.3s; - } +/* ============================================ + STYLE OFFICE POUR LE PORTAIL RH - VERSION STRUCTURE + ============================================ */ + +:root { + /* PowerPoint/Office 2019 Color Palette */ + --office-primary: #b7472a; /* Sidebar color - PowerPoint accent */ + --office-secondary: #2b579a; /* Header/buttons - Office blue */ + --office-accent: #107c10; /* Success/active states - Excel green */ + --office-light: #f3f2f1; /* Background - Office gray */ + --office-common: #e6e6e6; /* Background - Office gray */ + --office-dark: #323130; /* Text color */ + --office-border: #d0d0d0; /* Borders */ + --office-hover: #f0f0f0; /* Hover states */ + --office-card: #ffffff; /* Card backgrounds */ - /* Base Styles */ - body { - font-family: 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif; - background-color: var(--office-common); - color: var(--office-dark); - line-height: 1.5; - overflow-x: hidden; - } + /* Dimensions */ + --sidebar-width: 260px; + --sidebar-collapsed: 70px; + --header-height: 64px; + --transition-speed: 0.3s; - /* Header - PowerPoint/Office Style */ - .app-header { - background-color: var(--office-light); - height: var(--header-height); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 1030; - } - - .header-content { - display: flex; - align-items: center; - justify-content: space-between; - height: 100%; - padding: 0 24px; - } - - .logo-container { - display: flex; - align-items: center; - gap: 16px; - } - - .app-logo { - display: flex; - align-items: center; - color: #313c4c; - text-decoration: none; - font-weight: 600; - font-size: 20px; - } - - .app-logo img { - height: 36px; - /*filter: brightness(0) invert(1);*/ - } - - /* Sidebar - PowerPoint Orange (#b7472a) */ + /* Responsive breakpoints */ + --breakpoint-tablet: 1200px; + --breakpoint-mobile: 768px; +} + +/* ============================================ + BASE STYLES & RESET + ============================================ */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif; + background-color: var(--office-common); + color: var(--office-dark); + line-height: 1.5; + overflow-x: hidden; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* ============================================ + HEADER STYLES + ============================================ */ +.app-header { + background-color: var(--office-light); + height: var(--header-height); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1030; +} + +.header-content { + display: flex; + align-items: center; + justify-content: space-between; + height: 100%; + padding: 0 24px; +} + +.logo-container { + display: flex; + align-items: center; + gap: 16px; +} + +.app-logo { + display: flex; + align-items: center; + color: #313c4c; + text-decoration: none; + font-weight: 600; + font-size: 20px; +} + +.app-logo img { + height: 36px; +} + +/* ============================================ + SIDEBAR STYLES + ============================================ */ +.app-sidebar { + width: var(--sidebar-width); + background: linear-gradient(180deg, var(--office-primary) 0%, #a53e24 100%); + position: fixed; + top: var(--header-height); + left: 0; + bottom: 0; + z-index: 1020; + box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1); + transition: width var(--transition-speed) ease; + overflow-y: auto; + overflow-x: hidden; +} + +/* Pattern overlay */ +.app-sidebar::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: + radial-gradient(circle at 10% 20%, rgba(255, 255, 255, 0.03) 0%, transparent 20%), + radial-gradient(circle at 90% 80%, rgba(255, 255, 255, 0.03) 0%, transparent 20%); + pointer-events: none; +} + +.sidebar-nav { + padding: 24px 0; +} + +.nav-section { + margin-bottom: 32px; + padding: 0 20px; +} + +.nav-title { + color: rgba(255, 255, 255, 0.7); + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 12px; + padding: 0 16px; +} + +.nav-item { + margin-bottom: 4px; +} + +.nav-link { + display: flex; + align-items: center; + color: rgba(255, 255, 255, 0.85); + padding: 12px 16px; + text-decoration: none; + border-radius: 6px; + transition: all 0.2s ease; + position: relative; + overflow: hidden; +} + +.nav-link:hover { + background-color: rgba(255, 255, 255, 0.1); + color: white; +} + +.nav-link.active { + background-color: rgba(255, 255, 255, 0.15); + color: white; + font-weight: 500; +} + +.nav-link i { + width: 24px; + font-size: 18px; + margin-right: 12px; + text-align: center; +} + +.nav-text { + flex: 1; + font-size: 14px; +} + +.nav-badge { + background-color: var(--office-accent); + color: white; + font-size: 11px; + padding: 2px 6px; + border-radius: 10px; + margin-left: 8px; +} + +.nav-arrow { + font-size: 12px; + opacity: 0.7; + transition: transform 0.2s ease; +} + +.nav-link[aria-expanded="true"] .nav-arrow { + transform: rotate(90deg); +} + +.nav-submenu { + background-color: rgba(0, 0, 0, 0.1); + border-radius: 6px; + margin-top: 4px; + padding: 8px 0; +} + +.nav-submenu .nav-link { + padding: 8px 16px 8px 52px; + font-size: 13px; +} + +.nav-submenu .nav-link::before { + content: '•'; + position: absolute; + left: 36px; + opacity: 0.6; +} + +/* ============================================ + MAIN CONTENT AREA + ============================================ */ +.app-main { + margin-left: var(--sidebar-width); + padding-top: var(--header-height); + min-height: 100vh; + transition: margin-left var(--transition-speed) ease; + background-color: var(--office-light); +} + +.content-area { + padding: 24px; + max-width: 1400px; + margin: 0 auto; +} + +/* ============================================ + NAVIGATION TABS + ============================================ */ +.nav-bar { + background-color: white; + border-radius: 8px; + padding: 16px 24px; + margin-bottom: 24px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + border: 1px solid var(--office-border); +} + +.nav-tabs { + display: flex; + gap: 8px; + overflow-x: auto; + padding-bottom: 4px; +} + +.nav-tab { + display: inline-flex; + align-items: center; + padding: 8px 16px; + background-color: transparent; + color: var(--office-dark); + text-decoration: none; + border-radius: 4px; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + transition: all 0.2s ease; + border: 1px solid transparent; +} + +.nav-tab:hover { + background-color: var(--office-hover); + color: var(--office-secondary); +} + +.nav-tab.active { + background-color: var(--office-secondary); + color: white; + border-color: var(--office-secondary); +} + +.nav-tab i { + margin-right: 8px; + font-size: 16px; +} + +/* ============================================ + CONTENT CARDS + ============================================ */ +.content-card { + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + border: 1px solid var(--office-border); + margin-bottom: 24px; + overflow: hidden; +} + +.card-header { + background-color: #fafafa; + border-bottom: 1px solid var(--office-border); + padding: 20px 24px; + font-weight: 600; + color: var(--office-dark); + font-size: 18px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.card-body { + padding: 24px; +} + +/* ============================================ + HEADER CONTROLS + ============================================ */ +.header-controls { + display: flex; + align-items: center; + gap: 16px; +} + +.header-btn { + background: transparent; + border: none; + color: #313c4c; + padding: 8px; + border-radius: 4px; + cursor: pointer; + position: relative; + transition: background-color 0.2s ease; +} + +.header-btn:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.notification-badge { + position: absolute; + top: 2px; + right: 2px; + background-color: var(--office-accent); + color: white; + font-size: 10px; + font-weight: 600; + width: 18px; + height: 18px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.user-menu { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + padding: 4px 12px; + border-radius: 6px; + transition: background-color 0.2s ease; +} + +.user-menu:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.user-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: linear-gradient(135deg, var(--office-primary), #ff6b35); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 14px; +} + +.user-info { + display: flex; + flex-direction: column; +} + +.user-name { + color: #313c4c; + font-weight: 500; + font-size: 14px; +} + +.user-role { + color: #313c4c; + font-size: 12px; +} + +/* ============================================ + CONTEXT PANEL + ============================================ */ +.context-panel { + position: fixed; + right: -380px; + top: var(--header-height); + bottom: 0; + width: 380px; + background-color: white; + box-shadow: -2px 0 12px rgba(0, 0, 0, 0.1); + z-index: 1015; + transition: right 0.3s ease; + display: flex; + flex-direction: column; + border-left: 1px solid var(--office-border); +} + +.context-panel.open { + right: 0; +} + +.context-header { + background: linear-gradient(135deg, var(--office-primary), #ee6a49); + color: white; + padding: 20px 24px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.context-title { + font-size: 18px; + font-weight: 600; + display: flex; + align-items: center; + gap: 10px; +} + +.context-close { + background: transparent; + border: none; + color: white; + font-size: 20px; + cursor: pointer; + padding: 4px; + border-radius: 4px; + transition: background-color 0.2s ease; +} + +.context-close:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.context-body { + flex: 1; + padding: 24px; + overflow-y: auto; +} + +.context-section { + margin-bottom: 28px; +} + +.section-title { + font-size: 14px; + font-weight: 600; + color: var(--office-secondary); + margin-bottom: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; + display: flex; + align-items: center; + gap: 8px; +} + +.section-title i { + font-size: 16px; +} + +.info-box { + background-color: #fafafa; + border: 1px solid var(--office-border); + border-radius: 6px; + padding: 16px; + margin-bottom: 12px; +} + +.info-label { + font-size: 12px; + color: #666; + margin-bottom: 4px; +} + +.info-value { + font-size: 15px; + font-weight: 500; + color: var(--office-dark); + line-height: 1.4; +} + +.action-btn { + width: 100%; + padding: 12px; + background-color: white; + border: 1px solid var(--office-border); + border-radius: 6px; + color: var(--office-dark); + font-weight: 500; + text-align: left; + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + transition: all 0.2s ease; + margin-bottom: 8px; +} + +.action-btn:hover { + background-color: var(--office-hover); + border-color: var(--office-secondary); + color: var(--office-secondary); +} + +.action-btn i { + width: 20px; + text-align: center; + font-size: 18px; +} + +.photo-container { + text-align: center; + padding: 16px; + background-color: #fafafa; + border-radius: 8px; + border: 1px solid var(--office-border); +} + +.patient-photo { + width: 150px; + height: 150px; + border-radius: 8px; + object-fit: cover; + border: 3px solid white; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: transform 0.3s ease; +} + +.patient-photo:hover { + transform: scale(1.05); +} + +/* ============================================ + CONTEXT TOGGLE BUTTON + ============================================ */ +.context-toggle { + position: fixed; + right: 24px; + bottom: 24px; + width: 56px; + height: 56px; + border-radius: 50%; + background: linear-gradient(135deg, var(--office-primary), #ff6b35); + color: white; + border: none; + box-shadow: 0 4px 12px rgba(183, 71, 42, 0.3); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + z-index: 1010; + transition: all 0.3s ease; +} + +.context-toggle:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(183, 71, 42, 0.4); +} + +/* ============================================ + TEST MODE INDICATOR + ============================================ */ +.test-indicator { + background: linear-gradient(45deg, #ffd700, #ffed4e); + color: #8a6d3b; + padding: 4px 12px; + border-radius: 4px; + font-size: 12px; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 6px; + margin-left: 12px; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.7; } + 100% { opacity: 1; } +} + +/* ============================================ + LANGUAGE SELECTOR + ============================================ */ +.language-selector { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s ease; + color: #313c4c; +} + +.language-selector:hover { + background-color: rgba(255, 255, 255, 0.2); +} + +.language-flag { + width: 24px; + height: 16px; + object-fit: cover; + border-radius: 2px; +} + +/* ============================================ + MODAL CUSTOMIZATION + ============================================ */ +.modal-office .modal-header { + background: linear-gradient(135deg, var(--office-secondary), #1e4a8b); + color: white; + border-bottom: none; +} + +.modal-office .modal-title { + font-weight: 600; +} + +/* ============================================ + RESPONSIVE DESIGN + ============================================ */ +@media (max-width: 1200px) { .app-sidebar { - width: var(--sidebar-width); - background: linear-gradient(180deg, var(--office-primary) 0%, #a53e24 100%); - position: fixed; - top: var(--header-height); - left: 0; - bottom: 0; - z-index: 1020; - box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1); - transition: width var(--transition-speed) ease; - overflow-y: auto; - overflow-x: hidden; + width: var(--sidebar-collapsed); } - /* Subtle pattern overlay for modern look */ - .app-sidebar::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-image: - radial-gradient(circle at 10% 20%, rgba(255, 255, 255, 0.03) 0%, transparent 20%), - radial-gradient(circle at 90% 80%, rgba(255, 255, 255, 0.03) 0%, transparent 20%); - pointer-events: none; + .app-main { + margin-left: var(--sidebar-collapsed); } - .sidebar-nav { - padding: 24px 0; - } - - .nav-section { - margin-bottom: 32px; - padding: 0 20px; - } - - .nav-title { - color: rgba(255, 255, 255, 0.7); - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 12px; - padding: 0 16px; - } - - .nav-item { - margin-bottom: 4px; + .nav-text, + .nav-title, + .nav-badge, + .nav-arrow { + display: none; } .nav-link { - display: flex; - align-items: center; - color: rgba(255, 255, 255, 0.85); - padding: 12px 16px; - text-decoration: none; - border-radius: 6px; - transition: all 0.2s ease; - position: relative; - overflow: hidden; - } - - .nav-link:hover { - background-color: rgba(255, 255, 255, 0.1); - color: white; - transform: translateX(4px); - } - - .nav-link.active { - background-color: rgba(255, 255, 255, 0.15); - color: white; - font-weight: 500; - box-shadow: inset 3px 0 0 white; + justify-content: center; + padding: 16px; } .nav-link i { - width: 24px; - font-size: 18px; - margin-right: 12px; - text-align: center; - } - - .nav-text { - flex: 1; - font-size: 14px; - } - - .nav-badge { - background-color: var(--office-accent); - color: white; - font-size: 11px; - padding: 2px 6px; - border-radius: 10px; - margin-left: 8px; - } - - .nav-arrow { - font-size: 12px; - opacity: 0.7; - transition: transform 0.2s ease; - } - - .nav-link[aria-expanded="true"] .nav-arrow { - transform: rotate(90deg); - } - - .nav-submenu { - background-color: rgba(0, 0, 0, 0.1); - border-radius: 6px; - margin-top: 4px; - padding: 8px 0; + margin-right: 0; + font-size: 20px; } .nav-submenu .nav-link { - padding: 8px 16px 8px 52px; - font-size: 13px; + padding: 12px; + justify-content: center; } .nav-submenu .nav-link::before { - content: '•'; - position: absolute; - left: 36px; - opacity: 0.6; + display: none; + } + + .context-panel { + width: 100%; + right: -100%; + } +} + +@media (max-width: 768px) { + .app-sidebar { + transform: translateX(-100%); + width: var(--sidebar-width); + } + + .app-sidebar.open { + transform: translateX(0); } - /* Main Content Area */ .app-main { - margin-left: var(--sidebar-width); - padding-top: var(--header-height); - min-height: 100vh; - transition: margin-left var(--transition-speed) ease; - background-color: var(--office-light); + margin-left: 0; + } + + .header-content { + padding: 0 16px; } .content-area { - padding: 24px; - max-width: 1400px; - margin: 0 auto; - } - - /* Breadcrumb/Navigation Bar */ - .nav-bar { - background-color: white; - border-radius: 8px; - padding: 16px 24px; - margin-bottom: 24px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); - border: 1px solid var(--office-border); - } - - .nav-tabs { - display: flex; - gap: 8px; - overflow-x: auto; - padding-bottom: 4px; - } - - .nav-tab { - display: inline-flex; - align-items: center; - padding: 8px 16px; - background-color: transparent; - color: var(--office-dark); - text-decoration: none; - border-radius: 4px; - font-size: 14px; - font-weight: 500; - white-space: nowrap; - transition: all 0.2s ease; - border: 1px solid transparent; - } - - .nav-tab:hover { - background-color: var(--office-hover); - color: var(--office-secondary); - } - - .nav-tab.active { - background-color: var(--office-secondary); - color: white; - border-color: var(--office-secondary); - } - - .nav-tab i { - margin-right: 8px; - font-size: 16px; - } - - /* Content Cards */ - .content-card { - background-color: white; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); - border: 1px solid var(--office-border); - margin-bottom: 24px; - overflow: hidden; - } - - .card-header { - background-color: #fafafa; - border-bottom: 1px solid var(--office-border); - padding: 20px 24px; - font-weight: 600; - color: var(--office-dark); - font-size: 18px; - display: flex; - align-items: center; - justify-content: space-between; - } - - .card-body { - padding: 24px; - } - - /* Header Controls */ - .header-controls { - display: flex; - align-items: center; - gap: 16px; - } - - .header-btn { - background: transparent; - border: none; - color: #313c4c; - padding: 8px; - border-radius: 4px; - cursor: pointer; - position: relative; - transition: background-color 0.2s ease; - } - - .header-btn:hover { - background-color: rgba(255, 255, 255, 0.1); - } - - .notification-badge { - position: absolute; - top: 2px; - right: 2px; - background-color: var(--office-accent); - color: white; - font-size: 10px; - font-weight: 600; - width: 18px; - height: 18px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - } - - .user-menu { - display: flex; - align-items: center; - gap: 12px; - cursor: pointer; - padding: 4px 12px; - border-radius: 6px; - transition: background-color 0.2s ease; - } - - .user-menu:hover { - background-color: rgba(255, 255, 255, 0.1); - } - - .user-avatar { - width: 36px; - height: 36px; - border-radius: 50%; - background: linear-gradient(135deg, var(--office-primary), #ff6b35); - color: white; - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: 14px; + padding: 16px; } .user-info { - display: flex; - flex-direction: column; + display: none; } - .user-name { - color: #313c4c; - font-weight: 500; - font-size: 14px; - } - - .user-role { - color: #313c4c; - font-size: 12px; - } - - /* Context Panel */ - .context-panel { - position: fixed; - right: -380px; - top: var(--header-height); - bottom: 0; - width: 380px; - background-color: white; - box-shadow: -2px 0 12px rgba(0, 0, 0, 0.1); - z-index: 1015; - transition: right 0.3s ease; - display: flex; - flex-direction: column; - border-left: 1px solid var(--office-border); - } - - .context-panel.open { - right: 0; - } - - .context-header { - background: linear-gradient(135deg, var(--office-primary), #ee6a49); - color: white; - padding: 20px 24px; - display: flex; - align-items: center; - justify-content: space-between; - } - - .context-title { - font-size: 18px; - font-weight: 600; - display: flex; - align-items: center; - gap: 10px; - } - - .context-close { - background: transparent; - border: none; - color: white; - font-size: 20px; - cursor: pointer; - padding: 4px; - border-radius: 4px; - transition: background-color 0.2s ease; - } - - .context-close:hover { - background-color: rgba(255, 255, 255, 0.1); - } - - .context-body { - flex: 1; - padding: 24px; - overflow-y: auto; - } - - .context-section { - margin-bottom: 28px; - } - - .section-title { - font-size: 14px; - font-weight: 600; - color: var(--office-secondary); - margin-bottom: 12px; - text-transform: uppercase; - letter-spacing: 0.5px; - display: flex; - align-items: center; - gap: 8px; - } - - .section-title i { - font-size: 16px; - } - - .info-box { - background-color: #fafafa; - border: 1px solid var(--office-border); - border-radius: 6px; - padding: 16px; - margin-bottom: 12px; - } - - .info-label { - font-size: 12px; - color: #666; - margin-bottom: 4px; - } - - .info-value { - font-size: 15px; - font-weight: 500; - color: var(--office-dark); - line-height: 1.4; - } - - .action-btn { - width: 100%; - padding: 12px; - background-color: white; - border: 1px solid var(--office-border); - border-radius: 6px; - color: var(--office-dark); - font-weight: 500; - text-align: left; - display: flex; - align-items: center; - gap: 10px; - cursor: pointer; - transition: all 0.2s ease; - margin-bottom: 8px; - } - - .action-btn:hover { - background-color: var(--office-hover); - border-color: var(--office-secondary); - color: var(--office-secondary); - transform: translateX(4px); - } - - .action-btn i { - width: 20px; - text-align: center; - font-size: 18px; - } - - .photo-container { - text-align: center; - padding: 16px; - background-color: #fafafa; - border-radius: 8px; - border: 1px solid var(--office-border); - } - - .patient-photo { - width: 150px; - height: 150px; - border-radius: 8px; - object-fit: cover; - border: 3px solid white; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - cursor: pointer; - transition: transform 0.3s ease; - } - - .patient-photo:hover { - transform: scale(1.05); - } - - /* Toggle Button */ - .context-toggle { - position: fixed; - right: 24px; - bottom: 24px; - width: 56px; - height: 56px; - border-radius: 50%; - background: linear-gradient(135deg, var(--office-primary), #ff6b35); - color: white; - border: none; - box-shadow: 0 4px 12px rgba(183, 71, 42, 0.3); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 20px; - z-index: 1010; - transition: all 0.3s ease; - } - - .context-toggle:hover { - transform: translateY(-2px); - box-shadow: 0 6px 16px rgba(183, 71, 42, 0.4); - } - - /* Test Mode Indicator */ - .test-indicator { - background: linear-gradient(45deg, #ffd700, #ffed4e); - color: #8a6d3b; - padding: 4px 12px; - border-radius: 4px; - font-size: 12px; - font-weight: 600; - display: inline-flex; - align-items: center; - gap: 6px; - margin-left: 12px; - animation: pulse 2s infinite; - } - - @keyframes pulse { - 0% { opacity: 1; } - 50% { opacity: 0.7; } - 100% { opacity: 1; } - } - - /* Language Selector */ - .language-selector { - display: flex; - align-items: center; - gap: 8px; - padding: 6px 12px; - background-color: rgba(255, 255, 255, 0.1); - border-radius: 4px; - cursor: pointer; - transition: background-color 0.2s ease; - color: #313c4c; - } - - .language-selector:hover { - background-color: rgba(255, 255, 255, 0.2); - } - - .language-flag { - width: 24px; - height: 16px; - object-fit: cover; - border-radius: 2px; - } - - /* Modal Customization */ - .modal-office .modal-header { - background: linear-gradient(135deg, var(--office-secondary), #1e4a8b); - color: white; - border-bottom: none; - } - - .modal-office .modal-title { - font-weight: 600; - } - - /* Responsive Design */ - @media (max-width: 1200px) { - .app-sidebar { - width: var(--sidebar-collapsed); - } - - .app-main { - margin-left: var(--sidebar-collapsed); - } - - .nav-text, - .nav-title, - .nav-badge, - .nav-arrow { - display: none; - } - - .nav-link { - justify-content: center; - padding: 16px; - } - - .nav-link i { - margin-right: 0; - font-size: 20px; - } - - .nav-submenu .nav-link { - padding: 12px; - justify-content: center; - } - - .nav-submenu .nav-link::before { - display: none; - } - - .context-panel { - width: 100%; - right: -100%; - } - } - - @media (max-width: 768px) { - .app-sidebar { - transform: translateX(-100%); - width: var(--sidebar-width); - } - - .app-sidebar.open { - transform: translateX(0); - } - - .app-main { - margin-left: 0; - } - - .header-content { - padding: 0 16px; - } - - .content-area { - padding: 16px; - } - - .user-info { - display: none; - } - - .nav-text, - .nav-title, - .nav-badge, - .nav-arrow { - display: block; - } - } - - /* Scrollbar Styling */ - .app-sidebar::-webkit-scrollbar, - .context-body::-webkit-scrollbar { - width: 6px; - } - - .app-sidebar::-webkit-scrollbar-track, - .context-body::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.1); - border-radius: 3px; - } - - .app-sidebar::-webkit-scrollbar-thumb, - .context-body::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 3px; - } - - .app-sidebar::-webkit-scrollbar-thumb:hover, - .context-body::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.4); + .nav-text, + .nav-title, + .nav-badge, + .nav-arrow { + display: block; } +} + +/* ============================================ + SCROLLBAR STYLING + ============================================ */ +.app-sidebar::-webkit-scrollbar, +.context-body::-webkit-scrollbar { + width: 6px; +} + +.app-sidebar::-webkit-scrollbar-track, +.context-body::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; +} + +.app-sidebar::-webkit-scrollbar-thumb, +.context-body::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); + border-radius: 3px; +} + +.app-sidebar::-webkit-scrollbar-thumb:hover, +.context-body::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.4); +} \ No newline at end of file diff --git a/Bootstrap_new/css/ux_enhancements.css b/Bootstrap_new/css/ux_enhancements.css new file mode 100644 index 0000000..e04f7d5 --- /dev/null +++ b/Bootstrap_new/css/ux_enhancements.css @@ -0,0 +1,833 @@ +/* ============================================ + ENHANCEMENTS UX POUR PORTAIL INTER SANTÉ + ============================================ */ + +:root { + /* Variables UX avancées */ + --ux-transition-smooth: cubic-bezier(0.4, 0, 0.2, 1); + --ux-shadow-soft: 0 2px 15px rgba(0, 0, 0, 0.08); + --ux-shadow-medium: 0 5px 20px rgba(0, 0, 0, 0.12); + --ux-shadow-hard: 0 10px 30px rgba(0, 0, 0, 0.15); + --ux-border-radius-sm: 8px; + --ux-border-radius-md: 12px; + --ux-border-radius-lg: 20px; + + /* Variables d'animation */ + --animation-duration-fast: 150ms; + --animation-duration-normal: 300ms; + --animation-duration-slow: 500ms; +} + +/* ============================================ + ENHANCED SIDEBAR UX + ============================================ */ +.app-sidebar { + --sidebar-hover-glow: 0 0 20px rgba(255, 255, 255, 0.1); +} + +.nav-item { + position: relative; +} + +.nav-link { + position: relative; + overflow: visible; + transition: all var(--animation-duration-normal) var(--ux-transition-smooth); + border-left: 3px solid transparent; + will-change: transform, background-color, border-color; +} + +.nav-link:hover { + background: linear-gradient(90deg, + rgba(255, 255, 255, 0.1) 0%, + rgba(255, 255, 255, 0.05) 100%); + transform: translateX(8px); + border-left-color: rgba(255, 255, 255, 0.5); + box-shadow: var(--sidebar-hover-glow); +} + +.nav-link.active { + background: linear-gradient(90deg, + rgba(255, 255, 255, 0.2) 0%, + rgba(255, 255, 255, 0.1) 100%); + border-left-color: #ffffff; + box-shadow: inset 0 0 30px rgba(255, 255, 255, 0.1), + 0 0 20px rgba(255, 255, 255, 0.15); + font-weight: 600; + letter-spacing: 0.3px; +} + +/* Indicateur visuel pour lien actif */ +.nav-link.active::before { + content: ''; + position: absolute; + left: -3px; + top: 50%; + transform: translateY(-50%); + width: 6px; + height: 6px; + background-color: white; + border-radius: 50%; + box-shadow: 0 0 10px white; + animation: pulse-active 2s infinite; +} + +@keyframes pulse-active { + 0%, 100% { + opacity: 1; + transform: translateY(-50%) scale(1); + } + 50% { + opacity: 0.7; + transform: translateY(-50%) scale(1.2); + } +} + +/* Sous-menus améliorés */ +.nav-submenu { + background: linear-gradient(180deg, + rgba(0, 0, 0, 0.15) 0%, + rgba(0, 0, 0, 0.1) 100%); + border-radius: var(--ux-border-radius-sm); + margin: 6px 12px; + border-left: 2px solid rgba(255, 255, 255, 0.2); + transition: all 0.4s var(--ux-transition-smooth); + max-height: 0; + overflow: hidden; +} + +.nav-submenu.show { + max-height: 500px; + padding: 8px 0; + margin-top: 8px; + margin-bottom: 12px; + animation: slideDown 0.4s var(--ux-transition-smooth); +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + max-height: 0; + } + to { + opacity: 1; + transform: translateY(0); + max-height: 500px; + } +} + +/* ============================================ + CONTEXT BUTTON - QUILLBOT STYLE + ============================================ */ +.context-toggle { + position: fixed; + right: 30px; + bottom: 30px; + width: 60px; + height: 60px; + border-radius: 50%; + background: linear-gradient(135deg, #b7472a 0%, #ff6b35 100%); + color: white; + border: 3px solid white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + z-index: 1050; + transition: all 0.5s var(--ux-transition-smooth); + opacity: 0.2; + transform: scale(0.8); + box-shadow: + 0 4px 25px rgba(183, 71, 42, 0.15), + inset 0 0 20px rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + will-change: transform, opacity, box-shadow; +} + +/* Animation d'attente */ +.context-toggle::after { + content: ''; + position: absolute; + width: 72px; + height: 72px; + border: 2px solid rgba(183, 71, 42, 0.3); + border-radius: 50%; + animation: ripple 3s infinite; +} + +@keyframes ripple { + 0% { + transform: scale(0.8); + opacity: 1; + } + 100% { + transform: scale(1.2); + opacity: 0; + } +} + +/* État au survol et activation */ +.context-toggle:hover, +.context-toggle.active, +body:has(.proximity-hover-area:hover) .context-toggle { + opacity: 1 !important; + transform: scale(1.1) !important; + box-shadow: + 0 8px 35px rgba(183, 71, 42, 0.4), + 0 0 30px rgba(255, 107, 53, 0.3), + inset 0 0 25px rgba(255, 255, 255, 0.3); + animation: bounce 0.5s var(--ux-transition-smooth); +} + +@keyframes bounce { + 0%, 100% { transform: scale(1.1); } + 50% { transform: scale(1.15); } +} + +/* Rotation quand le panel est ouvert */ +.context-panel.open ~ .context-toggle { + transform: rotate(180deg) scale(1.1); + background: linear-gradient(135deg, #2b579a 0%, #1e4a8b 100%); + right: 410px; +} + +/* Zone de détection de proximité invisible */ +.proximity-hover-area { + position: fixed; + right: 0; + bottom: 0; + width: 200px; + height: 200px; + z-index: 1049; + pointer-events: none; +} + +/* Feedback visuel pour la zone */ +.proximity-hover-area::before { + content: ''; + position: absolute; + right: 30px; + bottom: 30px; + width: 100px; + height: 100px; + background: radial-gradient(circle, rgba(183, 71, 42, 0.1) 0%, transparent 70%); + border-radius: 50%; + opacity: 0; + transition: opacity var(--animation-duration-normal); +} + +.proximity-hover-area:hover::before { + opacity: 1; +} + +/* ============================================ + ENHANCED CONTEXT PANEL + ============================================ */ +.context-panel { + transition: right var(--animation-duration-slow) var(--ux-transition-smooth); + box-shadow: + -5px 0 30px rgba(0, 0, 0, 0.15), + 0 0 0 1px rgba(255, 255, 255, 0.1) inset; + border-left: 3px solid var(--office-primary); + will-change: right; +} + +.context-panel.open { + animation: slideInRight 0.5s var(--ux-transition-smooth); +} + +@keyframes slideInRight { + from { + transform: translateX(100px); + opacity: 0.8; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.context-header { + background: linear-gradient(135deg, var(--office-primary) 0%, #d9534f 100%); + padding: 25px 30px; + border-bottom: 3px solid rgba(255, 255, 255, 0.2); +} + +.context-title { + font-size: 20px; + font-weight: 700; +} + +.context-close { + transition: all var(--animation-duration-normal); + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; +} + +.context-close:hover { + background: rgba(255, 255, 255, 0.2); + transform: rotate(90deg); +} + +/* Sections avec effet de profondeur */ +.context-section { + background: white; + border-radius: var(--ux-border-radius-md); + padding: 20px; + margin-bottom: 25px; + box-shadow: var(--ux-shadow-soft); + border: 1px solid var(--office-border); + transition: all var(--animation-duration-normal); + position: relative; + overflow: hidden; +} + +.context-section:hover { + transform: translateY(-3px); + box-shadow: var(--ux-shadow-medium); + border-color: var(--office-secondary); +} + +.context-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + background: linear-gradient(to bottom, var(--office-primary), var(--office-secondary)); + opacity: 0.7; +} + +.section-title { + color: var(--office-secondary); + font-size: 15px; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 2px solid rgba(43, 87, 154, 0.1); + position: relative; +} + +.section-title::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 50px; + height: 2px; + background: linear-gradient(90deg, var(--office-primary), var(--office-secondary)); +} + +/* Cards améliorées */ +.info-box { + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border-radius: var(--ux-border-radius-sm); + padding: 18px; + border: 1px solid rgba(0, 0, 0, 0.08); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.03); + transition: all var(--animation-duration-normal); +} + +.info-box:hover { + border-color: var(--office-secondary); + box-shadow: + inset 0 2px 8px rgba(43, 87, 154, 0.05), + 0 3px 10px rgba(0, 0, 0, 0.05); +} + +.info-value { + font-size: 16px; + font-weight: 600; + color: var(--office-dark); + line-height: 1.5; +} + +/* Boutons d'action améliorés */ +.action-btn { + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + border: 2px solid var(--office-border); + border-radius: var(--ux-border-radius-sm); + padding: 15px 20px; + margin-bottom: 12px; + transition: all var(--animation-duration-normal) var(--ux-transition-smooth); + position: relative; + overflow: hidden; + will-change: transform, border-color, background; +} + +.action-btn:hover { + transform: translateX(8px); + border-color: var(--office-secondary); + background: linear-gradient(135deg, #f0f7ff 0%, #e3eeff 100%); + box-shadow: + 5px 5px 15px rgba(43, 87, 154, 0.1), + inset 0 0 20px rgba(255, 255, 255, 0.5); +} + +.action-btn::before { + content: ''; + position: absolute; + left: -100%; + top: 0; + width: 100%; + height: 100%; + background: linear-gradient(90deg, + transparent, + rgba(255, 255, 255, 0.4), + transparent); + transition: left 0.6s; +} + +.action-btn:hover::before { + left: 100%; +} + +/* Photo patient améliorée */ +.photo-container { + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border-radius: var(--ux-border-radius-md); + padding: 25px; + border: 2px dashed var(--office-border); + transition: all var(--animation-duration-normal); +} + +.photo-container:hover { + border-color: var(--office-primary); + background: linear-gradient(135deg, #fff5f2 0%, #ffffff 100%); + transform: scale(1.02); +} + +.patient-photo { + width: 180px; + height: 180px; + border-radius: var(--ux-border-radius-md); + object-fit: cover; + border: 5px solid white; + box-shadow: + 0 10px 30px rgba(0, 0, 0, 0.15), + 0 0 0 1px rgba(0, 0, 0, 0.05); + transition: all 0.4s var(--ux-transition-smooth); + cursor: pointer; + will-change: transform, box-shadow; +} + +.patient-photo:hover { + transform: scale(1.08) rotate(2deg); + box-shadow: + 0 20px 40px rgba(0, 0, 0, 0.2), + 0 0 0 3px rgba(183, 71, 42, 0.3); +} + +/* ============================================ + HEADER ENHANCEMENTS + ============================================ */ +.app-header { + backdrop-filter: blur(10px); + background: linear-gradient(135deg, + rgba(243, 242, 241, 0.95) 0%, + rgba(255, 255, 255, 0.98) 100%); + border-bottom: 1px solid rgba(0, 0, 0, 0.08); + box-shadow: + 0 4px 20px rgba(0, 0, 0, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.8); +} + +.user-avatar { + background: linear-gradient(135deg, var(--office-primary) 0%, #ff8c69 100%); + box-shadow: + 0 4px 15px rgba(183, 71, 42, 0.3), + inset 0 0 20px rgba(255, 255, 255, 0.3); + transition: all var(--animation-duration-normal); + will-change: transform, box-shadow; +} + +.user-avatar:hover { + transform: scale(1.1) rotate(5deg); + box-shadow: + 0 6px 25px rgba(183, 71, 42, 0.4), + inset 0 0 25px rgba(255, 255, 255, 0.4); +} + +.notification-badge { + background: linear-gradient(135deg, #107c10 0%, #20a020 100%); + box-shadow: 0 3px 10px rgba(16, 124, 16, 0.4); + animation: pulse-notification 2s infinite; +} + +@keyframes pulse-notification { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.1); } +} + +/* ============================================ + MAIN CONTENT ENHANCEMENTS + ============================================ */ +.app-main { + background: linear-gradient(135deg, + #f3f2f1 0%, + #f8f7f6 30%, + #fefefe 100%); +} + +.content-area { + animation: fadeIn 0.6s var(--ux-transition-smooth); +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.nav-bar { + background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%); + border-radius: var(--ux-border-radius-md); + box-shadow: var(--ux-shadow-soft); + border: 1px solid rgba(0, 0, 0, 0.06); + backdrop-filter: blur(10px); +} + +.nav-tab { + transition: all var(--animation-duration-normal); + position: relative; + overflow: hidden; + will-change: transform, box-shadow; +} + +.nav-tab:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(43, 87, 154, 0.1); +} + +.nav-tab.active { + background: linear-gradient(135deg, var(--office-secondary) 0%, #3a6bc0 100%); + box-shadow: + 0 5px 20px rgba(43, 87, 154, 0.3), + inset 0 0 20px rgba(255, 255, 255, 0.2); + transform: translateY(-2px); + animation: tab-active-pulse 3s infinite; +} + +@keyframes tab-active-pulse { + 0%, 100% { + box-shadow: 0 5px 20px rgba(43, 87, 154, 0.3); + } + 50% { + box-shadow: 0 5px 25px rgba(43, 87, 154, 0.5); + } +} + +.content-card { + background: linear-gradient(135deg, #ffffff 0%, #fcfcfc 100%); + border-radius: var(--ux-border-radius-md); + box-shadow: var(--ux-shadow-soft); + border: 1px solid rgba(0, 0, 0, 0.05); + transition: all 0.4s; + will-change: transform, box-shadow; +} + +.content-card:hover { + transform: translateY(-3px); + box-shadow: var(--ux-shadow-medium); + border-color: rgba(43, 87, 154, 0.1); +} + +/* ============================================ + LOADING STATES + ============================================ */ +.loading-placeholder { + background: linear-gradient(90deg, + #f0f0f0 25%, + #e0e0e0 50%, + #f0f0f0 75%); + background-size: 200% 100%; + animation: loading 1.5s infinite; + border-radius: var(--ux-border-radius-sm); +} + +@keyframes loading { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +/* ============================================ + ACCESSIBILITY ENHANCEMENTS + ============================================ */ + +/* Navigation au clavier */ +body.keyboard-navigation .nav-link:focus, +body.keyboard-navigation .action-btn:focus, +body.keyboard-navigation .context-toggle:focus { + outline: 3px solid rgba(43, 87, 154, 0.5); + outline-offset: 3px; + box-shadow: 0 0 0 6px rgba(43, 87, 154, 0.1); +} + +/* Lien skip pour accessibilité */ +.skip-link { + position: absolute; + top: -40px; + left: 0; + background: var(--office-primary); + color: white; + padding: 8px 16px; + border-radius: 0 0 4px 0; + text-decoration: none; + font-weight: 600; + z-index: 9999; + transition: top 0.3s; +} + +.skip-link:focus { + top: 0; + outline: 3px solid white; + outline-offset: -3px; +} + +/* High contrast mode */ +@media (prefers-contrast: high) { + .nav-link.active { + border-left: 4px solid white; + background-color: rgba(255, 255, 255, 0.3); + } + + .context-section { + border: 2px solid var(--office-dark); + } + + .action-btn { + border: 2px solid currentColor; + } +} + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + .context-toggle::after { + animation: none; + } +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .app-header { + background: rgba(30, 30, 30, 0.95); + border-bottom-color: rgba(255, 255, 255, 0.1); + } + + .content-card { + background: #2a2a2a; + border-color: #404040; + color: #e0e0e0; + } + + .nav-bar { + background: #2a2a2a; + border-color: #404040; + } + + .info-box { + background: #333; + border-color: #444; + } + + .action-btn { + background: #333; + border-color: #444; + color: #e0e0e0; + } +} + +/* ============================================ + RESPONSIVE ENHANCEMENTS + ============================================ */ +@media (max-width: 1200px) { + .app-sidebar:hover { + width: var(--sidebar-width); + } + + .app-sidebar:hover .nav-text, + .app-sidebar:hover .nav-arrow { + display: block; + animation: fadeIn 0.3s; + } + + .context-panel { + width: 90%; + } + + .context-panel.open ~ .context-toggle { + right: calc(90% + 20px); + } +} + +@media (max-width: 768px) { + .context-toggle { + width: 50px; + height: 50px; + font-size: 20px; + right: 20px; + bottom: 20px; + } + + .context-toggle::after { + width: 60px; + height: 60px; + } + + .context-panel { + width: 100%; + } + + .context-panel.open ~ .context-toggle { + right: calc(100% - 70px); + } + + .nav-bar { + padding: 12px 15px; + margin: 10px; + } + + .nav-tabs { + padding: 5px 0; + } + + .nav-tab { + padding: 8px 12px; + font-size: 13px; + } + + /* Masquer certains éléments sur mobile */ + .user-info .user-role { + display: none; + } + + .test-indicator span { + display: none; + } + + .test-indicator { + padding: 4px 8px; + } +} + +@media (max-width: 480px) { + .header-content { + padding: 0 12px; + } + + .app-logo span { + font-size: 16px; + } + + .user-name { + display: none; + } + + .context-body { + padding: 16px; + } + + .context-section { + padding: 15px; + } + + .patient-photo { + width: 140px; + height: 140px; + } +} + +/* ============================================ + UTILITY CLASSES + ============================================ */ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.text-balance { + text-wrap: balance; +} + +.text-pretty { + text-wrap: pretty; +} + +/* Connection warning */ +.connection-warning { + position: fixed; + top: 70px; + left: 50%; + transform: translateX(-50%); + z-index: 9999; + max-width: 90%; + animation: slideDown 0.3s ease-out; +} + +/* Animation for loaded images */ +img.loaded { + animation: fadeInUp 0.5s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Print styles */ +@media print { + .app-sidebar, + .app-header, + .context-panel, + .context-toggle, + .nav-bar { + display: none !important; + } + + .app-main { + margin-left: 0; + padding-top: 0; + } + + .content-card { + box-shadow: none; + border: 1px solid #ddd; + } + + body { + background: white; + color: black; + } +} \ No newline at end of file diff --git a/Bootstrap_new/js/ux-manager.js b/Bootstrap_new/js/ux-manager.js new file mode 100644 index 0000000..bb653ed --- /dev/null +++ b/Bootstrap_new/js/ux-manager.js @@ -0,0 +1,1377 @@ +/** + * UX Manager pour Portail Inter Santé + * Gestion avancée de l'expérience utilisateur + * Version: 2025.12.20.01 + */ + +// Configuration globale +const UXConfig = { + debug: false, + transitionSpeed: 300, + proximityRadius: 150, + notificationPollInterval: 30000, + saveState: true +}; + +// Module principal UX Manager +class UXManager { + constructor() { + this.contextPanelOpen = false; + this.sidebarOpen = false; + this.activeMenuId = window.appConfig?.activeParentId || null; + this.proximityDetectionActive = true; + + this.modules = { + navigation: null, + context: null, + notifications: null, + accessibility: null, + performance: null + }; + + this.init(); + } + + /** + * Initialisation du système UX + */ + init() { + try { + console.log('🚀 UX Manager initialisation...'); + + // Initialiser les modules + this.modules.navigation = new NavigationManager(); + this.modules.context = new ContextPanelManager(); + this.modules.notifications = new NotificationManager(); + this.modules.accessibility = new AccessibilityManager(); + this.modules.performance = new PerformanceManager(); + + // Restaurer l'état précédent + this.restoreState(); + + // Configurer les événements globaux + this.setupGlobalEvents(); + + // Lancer les pollings + this.startPollings(); + + console.log('✅ UX Manager prêt'); + + } catch (error) { + console.error('❌ Erreur initialisation UX Manager:', error); + } + } + + /** + * Restaurer l'état depuis localStorage + */ + restoreState() { + if (!UXConfig.saveState) return; + + try { + const savedPanelState = localStorage.getItem('contextPanelOpen'); + if (savedPanelState === 'true') { + // Ouvrir avec délai pour meilleure UX + setTimeout(() => { + this.modules.context.togglePanel(); + }, 1000); + } + + const savedMenuState = localStorage.getItem('expandedMenus'); + if (savedMenuState) { + this.modules.navigation.restoreMenuState(JSON.parse(savedMenuState)); + } + + } catch (e) { + if (UXConfig.debug) console.warn('Impossible de restaurer l\'état:', e); + } + } + + /** + * Configurer les événements globaux + */ + setupGlobalEvents() { + // Gestionnaire de redimensionnement avec debounce + let resizeTimeout; + window.addEventListener('resize', () => { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(() => { + this.handleResize(); + }, 250); + }); + + // Navigation au clavier + document.addEventListener('keydown', (e) => { + this.handleKeyboardNavigation(e); + }); + + // Prévenir la fermeture accidentelle avec données non sauvegardées + window.addEventListener('beforeunload', (e) => { + // Ici vous pouvez ajouter une logique pour vérifier les données non sauvegardées + // if (this.hasUnsavedChanges()) { + // e.preventDefault(); + // e.returnValue = ''; + // } + }); + } + + /** + * Gérer le redimensionnement de la fenêtre + */ + handleResize() { + const isMobile = window.innerWidth < 768; + document.body.classList.toggle('mobile-view', isMobile); + + // Ajuster le comportement du sidebar sur mobile + if (!isMobile && this.sidebarOpen) { + document.getElementById('sidebar')?.classList.remove('open'); + this.sidebarOpen = false; + } + } + + /** + * Gérer la navigation au clavier + */ + handleKeyboardNavigation(e) { + // Marquer la navigation clavier pour le CSS + if (e.key === 'Tab') { + document.body.classList.add('keyboard-navigation'); + } + + // Échap pour fermer les modales + if (e.key === 'Escape') { + if (this.contextPanelOpen) { + this.modules.context.togglePanel(); + } + + // Fermer toutes les modales Bootstrap ouvertes + const openModals = document.querySelectorAll('.modal.show'); + openModals.forEach(modal => { + const bsModal = bootstrap.Modal.getInstance(modal); + if (bsModal) bsModal.hide(); + }); + } + + // Navigation dans les menus avec flèches + if (e.target.closest('.sidebar-nav')) { + this.modules.navigation.handleKeyboardMenuNavigation(e); + } + } + + /** + * Démarrer les pollings automatiques + */ + startPollings() { + // Polling des notifications + setInterval(() => { + this.modules.notifications.pollNotifications(); + }, UXConfig.notificationPollInterval); + + // Premier poll immédiat + setTimeout(() => { + this.modules.notifications.pollNotifications(); + }, 2000); + } + + /** + * Basculer le panneau contexte + */ + toggleContextPanel() { + return this.modules.context.togglePanel(); + } + + /** + * Afficher la modale photo + */ + openPhotoModal() { + return this.modules.context.openPhotoModal(); + } + + /** + * Basculer le sidebar mobile + */ + toggleSidebar() { + const sidebar = document.getElementById('sidebar'); + if (!sidebar) return; + + this.sidebarOpen = !this.sidebarOpen; + sidebar.classList.toggle('open'); + + const toggleBtn = document.querySelector('.sidebar-toggle'); + if (toggleBtn) { + toggleBtn.setAttribute('aria-expanded', this.sidebarOpen); + } + + // Fermer le sidebar si on clique à l'extérieur (mobile uniquement) + if (this.sidebarOpen && window.innerWidth < 768) { + setTimeout(() => { + document.addEventListener('click', this.closeSidebarOnClickOutside.bind(this), { once: true }); + }, 100); + } + } + + /** + * Fermer le sidebar en cliquant à l'extérieur + */ + closeSidebarOnClickOutside(e) { + const sidebar = document.getElementById('sidebar'); + const toggleBtn = document.querySelector('.sidebar-toggle'); + + if (!sidebar?.contains(e.target) && !toggleBtn?.contains(e.target)) { + sidebar.classList.remove('open'); + this.sidebarOpen = false; + if (toggleBtn) toggleBtn.setAttribute('aria-expanded', 'false'); + } + } +} + +// ============================================ +// MODULE: Gestionnaire de Navigation +// ============================================ + +class NavigationManager { + constructor() { + this.expandedMenus = new Set(); + this.activeMenuId = window.appConfig?.activeParentId || null; + this.init(); + } + + init() { + this.setupMenuAutoClose(); + this.highlightActiveMenu(); + this.setupMenuInteractions(); + } + + /** + * Fermer automatiquement les menus inactifs + */ + setupMenuAutoClose() { + // Fermer tous les sous-menus sauf celui actif + document.querySelectorAll('.nav-submenu.show').forEach(submenu => { + const menuId = submenu.id; + const parentLink = document.querySelector(`[href="#${menuId}"]`); + + if (!parentLink?.classList.contains('active')) { + this.collapseMenu(submenu.id); + } + }); + + // Fermer les menus quand on clique ailleurs + document.addEventListener('click', (e) => { + if (!e.target.closest('.nav-item')) { + this.collapseAllExceptActive(); + } + }); + } + + /** + * Mettre en évidence le menu actif + */ + highlightActiveMenu() { + if (!this.activeMenuId) return; + + const activeLink = document.querySelector(`[href="#submenu${this.activeMenuId}"]`); + if (activeLink) { + activeLink.classList.add('active'); + this.expandMenu(`submenu${this.activeMenuId}`); + } + + // Mettre en évidence les enfants actifs + document.querySelectorAll('.nav-submenu .nav-link').forEach(link => { + const href = link.getAttribute('href'); + if (href && href.includes(window.appConfig.activeLink)) { + link.classList.add('active'); + link.setAttribute('aria-current', 'page'); + } + }); + } + + /** + * Configurer les interactions des menus + */ + setupMenuInteractions() { + document.querySelectorAll('.nav-link[data-bs-toggle="collapse"]').forEach(link => { + link.addEventListener('click', (e) => { + const menuId = link.getAttribute('href').substring(1); + + // Si on clique sur un menu déjà actif, ne rien faire + if (link.classList.contains('active')) return; + + // Fermer tous les autres menus + this.collapseAllExcept(menuId); + + // Sauvegarder l'état + this.saveMenuState(); + }); + }); + } + + /** + * Développer un menu + */ + expandMenu(menuId) { + const submenu = document.getElementById(menuId); + const parentLink = document.querySelector(`[href="#${menuId}"]`); + + if (submenu && parentLink) { + submenu.classList.add('show'); + parentLink.setAttribute('aria-expanded', 'true'); + parentLink.classList.add('active'); + + const arrow = parentLink.querySelector('.nav-arrow'); + if (arrow) arrow.style.transform = 'rotate(90deg)'; + + this.expandedMenus.add(menuId); + } + } + + /** + * Réduire un menu + */ + collapseMenu(menuId) { + const submenu = document.getElementById(menuId); + const parentLink = document.querySelector(`[href="#${menuId}"]`); + + if (submenu && parentLink) { + submenu.classList.remove('show'); + parentLink.setAttribute('aria-expanded', 'false'); + parentLink.classList.remove('active'); + + const arrow = parentLink.querySelector('.nav-arrow'); + if (arrow) arrow.style.transform = 'rotate(0deg)'; + + this.expandedMenus.delete(menuId); + } + } + + /** + * Réduire tous les menus sauf un + */ + collapseAllExcept(exceptMenuId) { + document.querySelectorAll('.nav-submenu.show').forEach(submenu => { + if (submenu.id !== exceptMenuId) { + this.collapseMenu(submenu.id); + } + }); + } + + /** + * Réduire tous les menus sauf l'actif + */ + collapseAllExceptActive() { + const activeMenuId = `submenu${this.activeMenuId}`; + this.collapseAllExcept(activeMenuId); + } + + /** + * Gérer la navigation clavier dans les menus + */ + handleKeyboardMenuNavigation(e) { + const currentItem = e.target.closest('.nav-link'); + if (!currentItem) return; + + const items = Array.from(document.querySelectorAll('.nav-link')); + const currentIndex = items.indexOf(currentItem); + + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + if (currentIndex < items.length - 1) { + items[currentIndex + 1].focus(); + } + break; + + case 'ArrowUp': + e.preventDefault(); + if (currentIndex > 0) { + items[currentIndex - 1].focus(); + } + break; + + case 'ArrowRight': + e.preventDefault(); + if (currentItem.hasAttribute('data-bs-toggle')) { + const menuId = currentItem.getAttribute('href').substring(1); + this.expandMenu(menuId); + } + break; + + case 'ArrowLeft': + e.preventDefault(); + if (currentItem.hasAttribute('data-bs-toggle')) { + const menuId = currentItem.getAttribute('href').substring(1); + this.collapseMenu(menuId); + } + break; + + case 'Enter': + case ' ': + e.preventDefault(); + if (currentItem.hasAttribute('data-bs-toggle')) { + const menuId = currentItem.getAttribute('href').substring(1); + if (this.expandedMenus.has(menuId)) { + this.collapseMenu(menuId); + } else { + this.expandMenu(menuId); + } + } else if (currentItem.href) { + window.location.href = currentItem.href; + } + break; + } + } + + /** + * Sauvegarder l'état des menus + */ + saveMenuState() { + if (!UXConfig.saveState) return; + + try { + const state = Array.from(this.expandedMenus); + localStorage.setItem('expandedMenus', JSON.stringify(state)); + } catch (e) { + if (UXConfig.debug) console.warn('Impossible de sauvegarder l\'état des menus:', e); + } + } + + /** + * Restaurer l'état des menus + */ + restoreMenuState(state) { + if (!Array.isArray(state)) return; + + state.forEach(menuId => { + this.expandMenu(menuId); + }); + } +} + +// ============================================ +// MODULE: Gestionnaire du Panneau 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; + + this.init(); + } + + init() { + if (!this.panel || !this.toggleButton) { + console.warn('❌ Éléments du panneau contexte non trouvés'); + return; + } + + this.setupProximityDetection(); + this.setupAnimations(); + this.setupCloseBehavior(); + } + + /** + * Détection de proximité (style QuillBot) + */ + setupProximityDetection() { + if (!this.proximityArea) return; + + let proximityTimeout; + + // Détection de la souris + document.addEventListener('mousemove', (e) => { + if (this.isOpen) return; + + const rect = this.proximityArea.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + const distance = Math.sqrt( + Math.pow(e.clientX - centerX, 2) + + Math.pow(e.clientY - centerY, 2) + ); + + clearTimeout(proximityTimeout); + + if (distance < UXConfig.proximityRadius) { + // Souris proche - montrer le bouton + this.showButton(); + } else { + // Souris éloignée - cacher progressivement + proximityTimeout = setTimeout(() => { + this.hideButton(); + }, 1000); + } + }); + + // Toujours montrer le bouton quand le panel est ouvert + this.panel.addEventListener('transitionend', () => { + if (this.isOpen) { + this.showButton(); + } + }); + } + + /** + * Afficher le bouton contexte + */ + showButton() { + if (this.toggleButton) { + this.toggleButton.style.opacity = '0.8'; + this.toggleButton.classList.add('active'); + } + } + + /** + * Cacher le bouton contexte + */ + hideButton() { + if (this.toggleButton && !this.isOpen) { + this.toggleButton.style.opacity = '0.2'; + this.toggleButton.classList.remove('active'); + } + } + + /** + * Configurer les animations + */ + setupAnimations() { + // Observer pour animations d'entrée + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('animate__animated', 'animate__fadeInUp'); + } + }); + }, { threshold: 0.1 }); + + // Observer les sections du panneau + document.querySelectorAll('.context-section').forEach(section => { + observer.observe(section); + }); + } + + /** + * Configurer le comportement de fermeture + */ + setupCloseBehavior() { + // Fermer avec Échap + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && this.isOpen) { + this.togglePanel(); + } + }); + + // Fermer en cliquant à l'extérieur + document.addEventListener('click', (e) => { + if (this.isOpen && + !this.panel.contains(e.target) && + !this.toggleButton.contains(e.target)) { + this.togglePanel(); + } + }); + } + + /** + * Basculer l'état du panneau + */ + togglePanel() { + this.isOpen = !this.isOpen; + + if (this.isOpen) { + this.openPanel(); + } else { + this.closePanel(); + } + + // Mettre à jour les attributs ARIA + this.toggleButton.setAttribute('aria-expanded', this.isOpen); + + // Sauvegarder l'état + if (UXConfig.saveState) { + localStorage.setItem('contextPanelOpen', this.isOpen.toString()); + } + + return this.isOpen; + } + + /** + * Ouvrir le panneau + */ + openPanel() { + this.panel.classList.add('open'); + this.toggleButton.classList.add('panel-open'); + + // Focus sur le premier élément focusable + setTimeout(() => { + const firstFocusable = this.panel.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); + if (firstFocusable) firstFocusable.focus(); + }, UXConfig.transitionSpeed); + + // Émettre un événement personnalisé + this.dispatchEvent('contextpanel:open'); + } + + /** + * Fermer le panneau + */ + closePanel() { + this.panel.classList.remove('open'); + this.toggleButton.classList.remove('panel-open'); + + // Retourner le focus au bouton toggle + this.toggleButton.focus(); + + // Cacher progressivement le bouton + setTimeout(() => { + if (!this.isOpen) this.hideButton(); + }, 1000); + + // Émettre un événement personnalisé + this.dispatchEvent('contextpanel:close'); + } + + /** + * Ouvrir la modale photo + */ + openPhotoModal() { + const modalElement = document.getElementById('photoModal'); + if (modalElement) { + const modal = new bootstrap.Modal(modalElement); + modal.show(); + } + } + + /** + * Émettre un événement personnalisé + */ + dispatchEvent(eventName, detail = {}) { + const event = new CustomEvent(eventName, { + detail: { ...detail, timestamp: Date.now() } + }); + document.dispatchEvent(event); + } +} + +// ============================================ +// MODULE: Gestionnaire de Notifications +// ============================================ + +class NotificationManager { + constructor() { + this.badge = document.getElementById('notificationCount'); + this.lastCount = 0; + this.unreadMessages = []; + + this.init(); + } + + init() { + this.setupBadgeAnimation(); + this.setupMessagePolling(); + } + + /** + * Configurer l'animation du badge + */ + setupBadgeAnimation() { + if (!this.badge) return; + + // Animation de pulse pour nouveaux messages + this.badge.addEventListener('animationend', () => { + this.badge.classList.remove('pulse'); + }); + } + + /** + * Configurer le polling des messages + */ + setupMessagePolling() { + // Poll initial + this.pollNotifications(); + + // Écouter les événements de nouvelle notification + document.addEventListener('newnotification', (e) => { + this.handleNewNotification(e.detail); + }); + } + + /** + * Poller les notifications + */ + async pollNotifications() { + try { + // Remplacer par votre appel API réel + const response = await this.fetchNotifications(); + const data = await response.json(); + + this.updateBadge(data.count); + + if (data.newMessages > 0) { + this.showNewNotificationAlert(data.newMessages); + this.unreadMessages = data.messages || []; + } + + } catch (error) { + if (UXConfig.debug) console.warn('Erreur polling notifications:', error); + } + } + + /** + * Récupérer les notifications + */ + async fetchNotifications() { + // Simuler une API - remplacer par votre endpoint réel + return { + ok: true, + json: async () => ({ + count: Math.floor(Math.random() * 10), + newMessages: Math.random() > 0.7 ? 1 : 0, + messages: [] + }) + }; + + // En production, utiliser : + // return fetch('api/notifications/count', { + // headers: { 'X-Requested-With': 'XMLHttpRequest' } + // }); + } + + /** + * Mettre à jour le badge + */ + updateBadge(count) { + if (!this.badge) return; + + const hasNew = count > this.lastCount; + this.lastCount = count; + + this.badge.textContent = count; + this.badge.style.display = count > 0 ? 'flex' : 'none'; + + if (hasNew && count > 0) { + this.badge.classList.add('pulse'); + + // Notification système si autorisée + if (Notification.permission === 'granted' && document.hidden) { + this.showSystemNotification(count); + } + } + } + + /** + * Afficher une alerte pour nouvelles notifications + */ + showNewNotificationAlert(count) { + // Créer un toast Bootstrap + const toastHTML = ` +
+ +
+ `; + + const container = document.createElement('div'); + container.innerHTML = toastHTML; + document.body.appendChild(container.firstElementChild); + + const toastElement = document.getElementById('notificationToast'); + if (toastElement) { + const toast = new bootstrap.Toast(toastElement, { autohide: true, delay: 5000 }); + toast.show(); + + toastElement.addEventListener('hidden.bs.toast', () => { + toastElement.remove(); + }); + } + } + + /** + * Afficher une notification système + */ + showSystemNotification(count) { + const notification = new Notification('Inter Santé', { + body: `${count} nouvelle${count > 1 ? 's' : ''} notification${count > 1 ? 's' : ''}`, + icon: 'Bootstrap_new/images/new/favicon.png', + tag: 'notification' + }); + + notification.onclick = () => { + window.focus(); + this.showMessagesModal(); + notification.close(); + }; + + setTimeout(() => notification.close(), 5000); + } + + /** + * Demander la permission pour les notifications + */ + async requestNotificationPermission() { + if (!('Notification' in window)) { + console.log('Notifications non supportées'); + return false; + } + + if (Notification.permission === 'granted') { + return true; + } + + if (Notification.permission !== 'denied') { + const permission = await Notification.requestPermission(); + return permission === 'granted'; + } + + return false; + } + + /** + * Afficher la modale des messages + */ + showMessagesModal() { + const modalElement = document.getElementById('messagesModal'); + if (!modalElement) return; + + const modal = new bootstrap.Modal(modalElement); + + // Charger les messages + this.loadMessages().then(messages => { + const container = document.getElementById('div_messagerie'); + if (container) { + container.innerHTML = this.renderMessages(messages); + } + }); + + modal.show(); + + // Réinitialiser le compteur + this.updateBadge(0); + } + + /** + * Charger les messages + */ + async loadMessages() { + try { + // Remplacer par votre appel API réel + const response = await fetch('api/messages/unread'); + return await response.json(); + } catch (error) { + console.error('Erreur chargement messages:', error); + return []; + } + } + + /** + * Rendre les messages en HTML + */ + renderMessages(messages) { + if (!messages.length) { + return '
Aucun message non lu
'; + } + + return ` +
+ ${messages.map(msg => ` +
+
+ ${msg.sender} + ${this.formatDate(msg.date)} +
+
${msg.content}
+
+ `).join('')} +
+ `; + } + + /** + * Formater une date + */ + formatDate(dateString) { + const date = new Date(dateString); + const options = { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }; + + return date.toLocaleDateString(window.appConfig.isAnglophone ? 'en-US' : 'fr-FR', options); + } + + /** + * Gérer une nouvelle notification + */ + handleNewNotification(detail) { + this.updateBadge(this.lastCount + 1); + this.showNewNotificationAlert(1); + + // Ajouter au tableau des messages non lus + if (detail.message) { + this.unreadMessages.unshift(detail.message); + } + } +} + +// ============================================ +// MODULE: Gestionnaire d'Accessibilité +// ============================================ + +class AccessibilityManager { + constructor() { + this.keyboardNavigation = false; + this.init(); + } + + init() { + this.setupKeyboardNavigationDetection(); + this.setupFocusManagement(); + this.setupSkipLinks(); + this.setupAriaLiveRegions(); + } + + /** + * Détecter la navigation clavier + */ + setupKeyboardNavigationDetection() { + document.addEventListener('keydown', (e) => { + if (e.key === 'Tab') { + this.keyboardNavigation = true; + document.body.classList.add('keyboard-navigation'); + } + }); + + document.addEventListener('mousedown', () => { + this.keyboardNavigation = false; + document.body.classList.remove('keyboard-navigation'); + }); + } + + /** + * Gérer le focus + */ + setupFocusManagement() { + // Garder le focus dans les modales + document.addEventListener('focusin', (e) => { + const modal = e.target.closest('.modal'); + if (modal && modal.classList.contains('show')) { + this.trapFocus(modal); + } + }); + + // Focus sur le contenu principal après navigation + document.addEventListener('pjax:success', () => { + this.focusMainContent(); + }); + } + + /** + * Piéger le focus dans un élément + */ + trapFocus(element) { + const focusableElements = element.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + + if (focusableElements.length === 0) return; + + const firstElement = focusableElements[0]; + const lastElement = focusableElements[focusableElements.length - 1]; + + element.addEventListener('keydown', (e) => { + if (e.key !== 'Tab') return; + + if (e.shiftKey) { + if (document.activeElement === firstElement) { + e.preventDefault(); + lastElement.focus(); + } + } else { + if (document.activeElement === lastElement) { + e.preventDefault(); + firstElement.focus(); + } + } + }); + } + + /** + * Focus sur le contenu principal + */ + focusMainContent() { + const mainContent = document.getElementById('mainContent'); + if (mainContent) { + mainContent.setAttribute('tabindex', '-1'); + mainContent.focus(); + setTimeout(() => { + mainContent.removeAttribute('tabindex'); + }, 100); + } + } + + /** + * Configurer les liens de saut + */ + setupSkipLinks() { + // Ajouter un lien pour sauter au contenu principal + const skipLink = document.createElement('a'); + skipLink.href = '#mainContent'; + skipLink.className = 'skip-link visually-hidden'; + skipLink.textContent = 'Aller au contenu principal'; + + skipLink.addEventListener('click', (e) => { + e.preventDefault(); + this.focusMainContent(); + }); + + document.body.insertBefore(skipLink, document.body.firstChild); + } + + /** + * Configurer les régions ARIA Live + */ + setupAriaLiveRegions() { + // Région pour les notifications + const notificationRegion = document.createElement('div'); + notificationRegion.setAttribute('aria-live', 'polite'); + notificationRegion.setAttribute('aria-atomic', 'true'); + notificationRegion.className = 'visually-hidden'; + notificationRegion.id = 'aria-live-notifications'; + + document.body.appendChild(notificationRegion); + } + + /** + * Annoncer un message pour les lecteurs d'écran + */ + announce(message, priority = 'polite') { + const region = document.getElementById('aria-live-notifications'); + if (region) { + region.textContent = ''; + setTimeout(() => { + region.textContent = message; + }, 100); + } + } +} + +// ============================================ +// MODULE: Gestionnaire de Performance +// ============================================ + +class PerformanceManager { + constructor() { + this.metrics = { + fcp: null, + lcp: null, + fid: null, + cls: 0 + }; + + this.init(); + } + + init() { + this.setupPerformanceMonitoring(); + this.setupLazyLoading(); + this.setupConnectionAwareLoading(); + this.setupServiceWorker(); + } + + /** + * Surveiller les métriques de performance + */ + setupPerformanceMonitoring() { + if ('PerformanceObserver' in window) { + // Core Web Vitals + this.observeLCP(); + this.observeFID(); + this.observeCLS(); + } + + // First Contentful Paint + if ('PerformancePaintTiming' in performance) { + performance.getEntriesByType('paint').forEach(entry => { + if (entry.name === 'first-contentful-paint') { + this.metrics.fcp = entry.startTime; + } + }); + } + } + + /** + * Observer Largest Contentful Paint + */ + observeLCP() { + const observer = new PerformanceObserver((list) => { + const entries = list.getEntries(); + const lastEntry = entries[entries.length - 1]; + this.metrics.lcp = lastEntry.renderTime || lastEntry.loadTime; + + if (UXConfig.debug) { + console.log('LCP:', this.metrics.lcp); + } + }); + + observer.observe({ entryTypes: ['largest-contentful-paint'] }); + } + + /** + * Observer First Input Delay + */ + observeFID() { + const observer = new PerformanceObserver((list) => { + const entries = list.getEntries(); + entries.forEach(entry => { + this.metrics.fid = entry.processingStart - entry.startTime; + + if (UXConfig.debug) { + console.log('FID:', this.metrics.fid); + } + }); + }); + + observer.observe({ entryTypes: ['first-input'] }); + } + + /** + * Observer Cumulative Layout Shift + */ + observeCLS() { + const observer = new PerformanceObserver((list) => { + list.getEntries().forEach(entry => { + if (!entry.hadRecentInput) { + this.metrics.cls += entry.value; + } + + if (UXConfig.debug) { + console.log('CLS:', this.metrics.cls); + } + }); + }); + + observer.observe({ entryTypes: ['layout-shift'] }); + } + + /** + * Configurer le lazy loading + */ + setupLazyLoading() { + const images = document.querySelectorAll('img[data-src]'); + const imageObserver = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const img = entry.target; + img.src = img.dataset.src; + img.classList.add('loaded'); + observer.unobserve(img); + } + }); + }); + + images.forEach(img => imageObserver.observe(img)); + } + + /** + * Chargement adaptatif selon la connexion + */ + setupConnectionAwareLoading() { + if ('connection' in navigator) { + const connection = navigator.connection; + + // Adapter la qualité des images + if (connection.saveData === true || connection.effectiveType.includes('2g')) { + this.enableDataSavingMode(); + } + + // Prévenir l'utilisateur si connexion lente + connection.addEventListener('change', () => { + if (connection.effectiveType.includes('2g') || connection.effectiveType.includes('3g')) { + this.showConnectionWarning(); + } + }); + } + } + + /** + * Activer le mode économie de données + */ + enableDataSavingMode() { + // Réduire la qualité des images + document.querySelectorAll('img[data-src-low]').forEach(img => { + img.dataset.src = img.dataset.srcLow; + }); + + // Désactiver les animations non essentielles + document.body.classList.add('data-saving-mode'); + } + + /** + * Afficher un avertissement de connexion lente + */ + showConnectionWarning() { + const warning = document.createElement('div'); + warning.className = 'connection-warning alert alert-warning alert-dismissible fade show'; + warning.innerHTML = ` + + Connexion lente détectée. Certaines fonctionnalités peuvent être limitées. + + `; + + document.body.prepend(warning); + + // Auto-remove after 10 seconds + setTimeout(() => { + warning.remove(); + }, 10000); + } + + /** + * Configurer le Service Worker + */ + setupServiceWorker() { + if ('serviceWorker' in navigator && window.location.protocol === 'https:') { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js') + .then(registration => { + if (UXConfig.debug) { + console.log('Service Worker enregistré:', registration.scope); + } + }) + .catch(error => { + if (UXConfig.debug) { + console.log('Échec enregistrement Service Worker:', error); + } + }); + }); + } + } + + /** + * Optimiser les animations pour la performance + */ + optimizeAnimations() { + // Utiliser will-change pour les éléments animés + document.querySelectorAll('.nav-link, .action-btn, .context-toggle').forEach(el => { + el.style.willChange = 'transform, opacity'; + }); + } +} + +// ============================================ +// EXPORT ET INITIALISATION GLOBALE +// ============================================ + +// Exposer les fonctions globales +window.appUX = { + toggleContextPanel: () => window.uxManager?.toggleContextPanel(), + openPhotoModal: () => window.uxManager?.openPhotoModal(), + toggleSidebar: () => window.uxManager?.toggleSidebar() +}; + +window.appNotifications = { + showMessagesModal: () => window.uxManager?.modules.notifications?.showMessagesModal(), + requestNotificationPermission: () => window.uxManager?.modules.notifications?.requestNotificationPermission() +}; + +window.appLanguage = { + changeLanguage: () => { + Swal.fire({ + title: 'Changer de langue', + text: 'Sélectionnez la langue de l\'interface :', + icon: 'question', + showCancelButton: true, + confirmButtonText: 'Français', + cancelButtonText: 'English', + reverseButtons: true, + customClass: { popup: 'animate__animated animate__fadeIn' } + }).then((result) => { + if (result.isConfirmed) { + window.location.href = '?lang=fr_FR'; + } else if (result.dismiss === Swal.DismissReason.cancel) { + window.location.href = '?lang=en_US'; + } + }); + } +}; + +// Initialiser au chargement du DOM +document.addEventListener('DOMContentLoaded', () => { + // Attendre que les ressources critiques soient chargées + if (document.readyState === 'complete') { + window.uxManager = new UXManager(); + } else { + window.addEventListener('load', () => { + window.uxManager = new UXManager(); + }); + } + + // Initialiser les composants Bootstrap + initializeBootstrapComponents(); +}); + +/** + * Initialiser les composants Bootstrap + */ +function initializeBootstrapComponents() { + // Tooltips + const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); + tooltipTriggerList.map(tooltipTriggerEl => { + return new bootstrap.Tooltip(tooltipTriggerEl, { + trigger: 'hover focus', + animation: true + }); + }); + + // Select2 + if (typeof $.fn.select2 === 'function') { + $('.select2').select2({ + theme: 'bootstrap-5', + width: '100%', + placeholder: 'Sélectionnez...', + allowClear: true, + dropdownParent: $('body') + }); + } + + // DataTables + if (typeof $.fn.DataTable === 'function') { + $('.datatable').DataTable({ + responsive: true, + language: { + url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/fr-FR.json' + }, + dom: '<"row"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>' + + '<"row"<"col-sm-12"tr>>' + + '<"row"<"col-sm-12 col-md-5"i><"col-sm-12 col-md-7"p>>', + pageLength: 25, + stateSave: true, + stateDuration: 60 * 60 * 24 // 24 heures + }); + } +} + +// Exporter pour les tests +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + UXManager, + NavigationManager, + ContextPanelManager, + NotificationManager, + AccessibilityManager, + PerformanceManager + }; +} \ No newline at end of file diff --git a/Vue/gabarit.php b/Vue/gabarit.php index a7be528..827f67e 100755 --- a/Vue/gabarit.php +++ b/Vue/gabarit.php @@ -9,501 +9,437 @@ $_SESSION['firstLevelMenu'] = $activeLink; $companyDisplayName = htmlspecialchars($_SESSION['nomSociete'], ENT_QUOTES); $imgData = $_SESSION['photoAssureCrypte'] ?? ''; + +// Détection automatique des menus actifs +$activeParentId = null; +$activeChildId = null; + +foreach ($menus as $key0 => $menuParent) { + $menuChildrenLevelOne = $gabary->get_menus_by_parent_code($menuParent['vue']); + + foreach ($menuChildrenLevelOne as $key1 => $menuChild) { + if ((explode('/', $menuChild['lienMenu'])[0] ?? '') == $activeLink) { + $activeParentId = $key0; + $activeChildId = $key1; + break 2; + } + } +} ?> - - - - - - - <?= htmlspecialchars($_SESSION['vue'] ?? 'INTER SANTE') ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - - - -
- - MODE TEST -
- -
+ + + + + + + <?= htmlspecialchars($_SESSION['vue'] ?? 'INTER SANTÉ') ?> | Portail Santé + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + +
+ + MODE TEST +
+
- -
+
- - + - -
- -
- -
- - - - - - - - - -