Validation

This commit is contained in:
KONE SOREL 2025-12-20 21:07:26 +00:00
parent 619efb6238
commit 2304800407
8 changed files with 939 additions and 21487 deletions

View File

@ -1,738 +1,163 @@
/* ============================================
STYLE OFFICE POUR LE PORTAIL RH - VERSION STRUCTURE
AJOUTS POUR MENU UNIQUE OUVERT
============================================ */
: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 */
/* Dimensions */
--sidebar-width: 260px;
--sidebar-collapsed: 70px;
--header-height: 64px;
--transition-speed: 0.3s;
/* Responsive breakpoints */
--breakpoint-tablet: 1200px;
--breakpoint-mobile: 768px;
/* Animation pour les sous-menus */
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
max-height: 0;
}
to {
opacity: 1;
transform: translateY(0);
max-height: 500px;
}
}
/* ============================================
BASE STYLES & RESET
============================================ */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
@keyframes slideUp {
from {
opacity: 1;
transform: translateY(0);
max-height: 500px;
}
to {
opacity: 0;
transform: translateY(-10px);
max-height: 0;
}
}
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) !important;
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;
/* Styles pour le menu unique ouvert */
.nav-submenu {
max-height: 0;
overflow: hidden;
animation: slideUp 0.3s ease forwards;
display: none;
}
.nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: white;
.nav-submenu.show {
display: block;
animation: slideDown 0.3s ease forwards;
}
.nav-link.active {
background-color: rgba(255, 255, 255, 0.15);
color: white;
font-weight: 500;
/* Désactiver la logique Bootstrap par défaut */
.nav-link[data-bs-toggle="collapse"] {
pointer-events: auto;
}
.nav-link i {
width: 24px;
font-size: 18px;
margin-right: 12px;
text-align: center;
.nav-link[data-bs-toggle="collapse"].collapsed .nav-arrow {
transform: rotate(0deg);
}
.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 {
.nav-link[data-bs-toggle="collapse"]:not(.collapsed) .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
AMÉLIORATIONS UX
============================================ */
.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 {
/* Zone de détection de proximité */
.proximity-hover-area {
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;
top: 50%;
width: 80px;
height: 200px;
transform: translateY(-50%);
z-index: 1005;
pointer-events: none;
}
.context-header {
background: linear-gradient(135deg, var(--office-primary), #ee6a49);
/* État hors ligne */
body.offline .app-header::after {
content: 'Hors ligne';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #ff6b35;
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 {
/* Spinner amélioré */
.spinner-responsive {
display: flex;
justify-content: center;
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;
min-height: 300px;
}
.language-selector:hover {
background-color: rgba(255, 255, 255, 0.2);
.spinner-responsive i {
font-size: 48px;
color: var(--office-primary);
animation: spin 1s linear infinite;
}
.language-flag {
width: 24px;
height: 16px;
object-fit: cover;
border-radius: 2px;
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* ============================================
MODAL CUSTOMIZATION
RESPONSIVE AMÉLIORÉ
============================================ */
.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);
transition: transform 0.3s ease;
}
.app-sidebar.open {
.app-sidebar.show {
transform: translateX(0);
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.15);
}
.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;
.context-toggle {
width: 48px;
height: 48px;
right: 16px;
bottom: 16px;
}
}
/* ============================================
SCROLLBAR STYLING
ACCESSIBILITÉ
============================================ */
.app-sidebar::-webkit-scrollbar,
.context-body::-webkit-scrollbar {
width: 6px;
.nav-link:focus {
outline: 2px solid var(--office-secondary);
outline-offset: -2px;
}
.app-sidebar::-webkit-scrollbar-track,
.context-body::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
.context-toggle:focus {
outline: 3px solid var(--office-secondary);
outline-offset: 2px;
}
.app-sidebar::-webkit-scrollbar-thumb,
.context-body::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
/* High contrast mode */
@media (prefers-contrast: high) {
:root {
--office-border: #000000;
--office-dark: #000000;
}
.nav-link.active {
outline: 3px solid #000;
}
}
.app-sidebar::-webkit-scrollbar-thumb:hover,
.context-body::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

View File

@ -1,558 +1,71 @@
/* ============================================
ENHANCEMENTS UX POUR PORTAIL INTER SANTÉ
UX ENHANCEMENTS - ANIMATIONS ET MICRO-INTERACTIONS
============================================ */
: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 {
/* Effet de vague au clic */
.ripple {
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 {
.ripple::after {
content: '';
position: absolute;
width: 72px;
height: 72px;
border: 2px solid rgba(183, 71, 42, 0.3);
border-radius: 50%;
animation: ripple 3s infinite;
top: 50%;
left: 50%;
width: 5px;
height: 5px;
background: rgba(255, 255, 255, 0.5);
opacity: 0;
border-radius: 100%;
transform: scale(1, 1) translate(-50%);
transform-origin: 50% 50%;
}
.ripple:focus:not(:active)::after {
animation: ripple 1s ease-out;
}
@keyframes ripple {
0% {
transform: scale(0.8);
opacity: 1;
0% {
transform: scale(0, 0);
opacity: 0.5;
}
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);
20% {
transform: scale(25, 25);
opacity: 0.3;
}
100% {
opacity: 0;
transform: scale(40, 40);
}
}
/* Effet de levitation pour les cartes */
.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;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.content-card:hover {
transform: translateY(-3px);
box-shadow: var(--ux-shadow-medium);
border-color: rgba(43, 87, 154, 0.1);
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
/* ============================================
LOADING STATES
============================================ */
.loading-placeholder {
background: linear-gradient(90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%);
/* Animation du bouton contexte */
.context-toggle {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.context-toggle:hover {
transform: scale(1.1) translateY(-2px);
box-shadow: 0 8px 20px rgba(183, 71, 42, 0.4);
}
/* Loading skeleton */
.skeleton {
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 {
@ -560,274 +73,66 @@ body:has(.proximity-hover-area:hover) .context-toggle {
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);
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* 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;
/* Focus styles améliorés */
:focus-visible {
outline: 3px solid var(--office-secondary);
outline-offset: 2px;
border-radius: 4px;
}
.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;
/* Animation d'entrée */
.fade-in-up {
animation: fadeInUp 0.6s ease forwards;
opacity: 0;
transform: translateY(20px);
}
@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;
}
/* Tooltip personnalisé */
[data-tooltip] {
position: relative;
}
[data-tooltip]:hover::before {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: var(--office-dark);
color: white;
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 1000;
margin-bottom: 8px;
animation: fadeIn 0.2s ease;
}
[data-tooltip]:hover::after {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: var(--office-dark);
margin-bottom: 2px;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

132
Js/sw-register.js Normal file
View File

@ -0,0 +1,132 @@
// Enregistrement du Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
// S'assurer que nous sommes sur HTTPS en production
if (location.protocol === 'https:' || location.hostname === 'localhost') {
navigator.serviceWorker.register('/service-worker.js')
.then(function(registration) {
console.log('[Service Worker] Enregistré avec succès:', registration.scope);
// Vérifier les mises à jour
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
console.log('[Service Worker] Mise à jour trouvée:', newWorker.state);
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// Nouvelle version disponible
console.log('[Service Worker] Nouvelle version disponible');
this.showUpdateNotification();
}
});
});
})
.catch(function(error) {
console.error('[Service Worker] Échec de l\'enregistrement:', error);
});
}
});
// Gestion hors ligne
window.addEventListener('online', () => {
document.documentElement.classList.remove('offline');
console.log('[App] Connexion rétablie');
this.showOnlineNotification();
});
window.addEventListener('offline', () => {
document.documentElement.classList.add('offline');
console.log('[App] Mode hors ligne');
this.showOfflineNotification();
});
}
// Notification de mise à jour
function showUpdateNotification() {
if (window.appConfig?.isAnglophone) {
if (confirm('A new version is available. Reload the page?')) {
window.location.reload();
}
} else {
if (confirm('Une nouvelle version est disponible. Recharger la page?')) {
window.location.reload();
}
}
}
// Notification en ligne
function showOnlineNotification() {
// Créer une notification toast
const toast = document.createElement('div');
toast.className = 'position-fixed top-0 end-0 p-3';
toast.style.zIndex = '9999';
toast.innerHTML = `
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header bg-success text-white">
<i class="bi bi-wifi me-2"></i>
<strong class="me-auto">${window.appConfig?.isAnglophone ? 'Online' : 'En ligne'}</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
${window.appConfig?.isAnglophone
? 'Connection restored. Synchronization in progress...'
: 'Connexion rétablie. Synchronisation en cours...'}
</div>
</div>
`;
document.body.appendChild(toast);
// Supprimer après 3 secondes
setTimeout(() => {
toast.remove();
}, 3000);
}
// Notification hors ligne
function showOfflineNotification() {
// Créer une notification toast
const toast = document.createElement('div');
toast.className = 'position-fixed top-0 end-0 p-3';
toast.style.zIndex = '9999';
toast.innerHTML = `
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header bg-warning text-dark">
<i class="bi bi-wifi-off me-2"></i>
<strong class="me-auto">${window.appConfig?.isAnglophone ? 'Offline' : 'Hors ligne'}</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
${window.appConfig?.isAnglophone
? 'No internet connection. Working in offline mode.'
: 'Pas de connexion Internet. Mode hors ligne actif.'}
</div>
</div>
`;
document.body.appendChild(toast);
// Supprimer après 5 secondes
setTimeout(() => {
toast.remove();
}, 5000);
}
// Fonction pour forcer la mise à jour du Service Worker
function updateServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.update().then(() => {
console.log('[Service Worker] Mise à jour forcée');
});
});
}
}
// Exporter pour une utilisation externe
window.serviceWorker = {
update: updateServiceWorker,
isSupported: 'serviceWorker' in navigator
};

View File

@ -34,61 +34,68 @@ foreach ($menus as $key0 => $menuParent) {
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<base href="<?= $racineWeb ?>">
<title><?= htmlspecialchars($_SESSION['vue'] ?? 'INTER SANTÉ') ?> | Portail Santé</title>
<!-- Meta pour UX améliorée -->
<meta name="description" content="Portail professionnel de gestion santé Inter Santé">
<meta name="theme-color" content="#b7472a">
<!-- Progressive Web App -->
<link rel="manifest" href="manifest.json">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<!-- Icône -->
<link rel="apple-touch-icon" href="Bootstrap_new/images/new/favicon.png">
<link rel="icon" href="Bootstrap_new/images/new/favicon.png" type="image/png">
<!-- Bootstrap 5 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Select2 -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@ttskch/select2-bootstrap5-theme@1.3.0/dist/select2-bootstrap5.min.css" rel="stylesheet">
<!-- DataTables -->
<link href="https://cdn.datatables.net/v/bs5/dt-1.13.6/datatables.min.css" rel="stylesheet">
<!-- SweetAlert2 -->
<link href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css" rel="stylesheet">
<!-- Animate.css pour animations douces -->
<!-- Animate.css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
<!-- Office/PowerPoint Inspired Styles -->
<link href="Bootstrap_new/css/style_office.css?ver=2025.12.20.00" rel="stylesheet">
<!-- UX Improvements CSS -->
<link href="Bootstrap_new/css/ux_enhancements.css?ver=2025.12.20.01" rel="stylesheet">
<!-- Progressive Web App -->
<link rel="manifest" href="/manifest.json">
<link rel="apple-touch-icon" href="Bootstrap_new/images/new/favicon.png">
<!-- jQuery UI (pour Datepicker) -->
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css">
<!-- Styles personnalisés -->
<link href="Bootstrap_new/css/style_office.css?ver=2025.12.21.01" rel="stylesheet">
<link href="Bootstrap_new/css/ux_enhancements.css?ver=2025.12.21.01" rel="stylesheet">
<script>
// Mode développeur
const modeDev = 1; //<?= $_SESSION['modeDev_C'] ?? 0 ?>;
if (modeDev != "1") {
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
});
const modeDev = <?= $_SESSION['modeDev_C'] ?? 0 ?>;
if (modeDev !== 1) {
document.addEventListener('contextmenu', e => e.preventDefault());
}
// Variables globales accessibles par tous les scripts
window.appConfig = {
activeParentId: '<?= $activeParentId ?>',
activeChildId: '<?= $activeChildId ?>',
activeLink: '<?= $activeLink ?>',
racineWeb: '<?= $racineWeb ?>',
isAnglophone: <?= est_anglophone() ? 'true' : 'false' ?>,
modeTest: <?= $_SESSION['bdTests_C'] == "1" ? 'true' : 'false' ?>
modeTest: <?= $_SESSION['bdTests_C'] == "1" ? 'true' : 'false' ?>,
userInitials: '<?= $_SESSION['userInitials_C'] ?? 'U' ?>'
};
</script>
</head>
@ -98,11 +105,11 @@ foreach ($menus as $key0 => $menuParent) {
<header class="app-header">
<div class="header-content">
<div class="logo-container">
<button class="header-btn sidebar-toggle d-lg-none" aria-label="Menu navigation">
<button class="header-btn sidebar-toggle d-lg-none" aria-label="Menu navigation" onclick="appUX.toggleSidebar()">
<i class="bi bi-list"></i>
</button>
<a href="#" class="app-logo">
<a href="Accueil" class="app-logo">
<img src="Bootstrap_new/images/new/favicon.png" alt="INTER-SANTÉ" width="36" height="36">
<span class="ms-2">INTER-SANTÉ</span>
</a>
@ -166,7 +173,7 @@ foreach ($menus as $key0 => $menuParent) {
<i class="fas fa-envelope me-2"></i> <?= _('Messagerie') ?>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger" href="#">
<a class="dropdown-item text-danger" href="Connexion/deconnecter">
<i class="fas fa-sign-out-alt me-2"></i> <?= _('Déconnexion') ?>
</a>
</div>
@ -190,20 +197,24 @@ foreach ($menus as $key0 => $menuParent) {
break;
}
}
// Déterminer si le menu doit être ouvert
$shouldBeOpen = $isParentActive || $hasActiveChild;
?>
<div class="nav-item">
<?php if (sizeof($menuChildrenLevelOne) > 0): ?>
<a href="#submenu<?= $key0 ?>"
class="nav-link <?= $isParentActive || $hasActiveChild ? 'active' : '' ?>"
class="nav-link <?= $shouldBeOpen ? 'active' : '' ?>"
data-bs-toggle="collapse"
aria-expanded="<?= $isParentActive || $hasActiveChild ? 'true' : 'false' ?>"
onclick="appNavigation.toggleMenu('submenu<?= $key0 ?>', event)"
aria-expanded="<?= $shouldBeOpen ? 'true' : 'false' ?>"
aria-controls="submenu<?= $key0 ?>">
<i class="<?= $menuParent['icone'] ?>"></i>
<span class="nav-text"><?= $menuParent['libeleMenu'] ?></span>
<i class="nav-arrow bi bi-chevron-right"></i>
</a>
<div class="nav-submenu collapse <?= $isParentActive || $hasActiveChild ? 'show' : '' ?>"
<div class="nav-submenu collapse <?= $shouldBeOpen ? 'show' : '' ?>"
id="submenu<?= $key0 ?>">
<?php foreach ($menuChildrenLevelOne as $key1 => $menuChild):
$childActive = (explode('/', $menuChild['lienMenu'])[0] ?? '') == $activeLink;
@ -258,6 +269,8 @@ foreach ($menus as $key0 => $menuParent) {
<input type="hidden" id="dureeSession" value="<?= $_SESSION['dureeSession'] ?>">
<input type="hidden" id="nomSociete" value="<?= $_SESSION['nomSociete'] ?>">
<input type="hidden" id="nomClient" value="<?= htmlspecialchars($_SESSION['nomClient_C']) ?>">
<input type="hidden" id="idBeneficiaire_C" value="<?= $_SESSION['idBeneficiaire_C'] ?>">
<input type="hidden" id="okId" value="<?= $_SESSION['okId'] ?>">
<!-- Content Container -->
<div class="content-card">
@ -421,25 +434,27 @@ foreach ($menus as $key0 => $menuParent) {
</div>
<!-- JavaScript Libraries -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.datatables.net/v/bs5/dt-1.13.6/datatables.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Application Scripts -->
<script src="Js/fonctions.js?ver=2025.12.20.00"></script>
<script src="Js/fonctions.js?ver=2025.12.21.01"></script>
<?php if (est_anglophone()): ?>
<script src="Js/datepicker-eng.js"></script>
<?php else: ?>
<script src="Js/datepicker-fr.js"></script>
<?php endif; ?>
<!-- UX Enhancement Script -->
<script src="Bootstrap_new/js/ux-manager.js?ver=2025.12.20.01"></script>
<script src="Bootstrap_new/js/ux-manager.js?ver=2025.12.21.01"></script>
<!-- AJAX Content Area -->
<div id="div_ajaxgabarit"></div>
<!-- Service Worker Registration -->
<script src="Js/sw-register.js?ver=2025.12.21.01"></script>
</body>
</html>

56
manifest.json Normal file
View File

@ -0,0 +1,56 @@
{
"name": "Portail RH Inter Santé",
"short_name": "InterSanté RH",
"description": "Portail RH professionnel avec design Office/PowerPoint",
"start_url": "/",
"display": "standalone",
"background_color": "#f3f2f1",
"theme_color": "#b7472a",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"categories": ["business", "productivity", "utilities"],
"dir": "ltr",
"lang": "fr-FR",
"prefer_related_applications": false
}

124
service-worker.js Normal file
View File

@ -0,0 +1,124 @@
// Service Worker pour Portail RH Inter Santé
const CACHE_NAME = 'inter-sante-portal-v1.0';
const OFFLINE_URL = '/offline.html';
// Ressources à mettre en cache immédiatement
const PRECACHE_URLS = [
'/',
'/style_office.css',
'/ux_enhancements.css',
'/ux-manager.js',
'/manifest.json',
'/icons/icon-192x192.png',
'/icons/icon-512x512.png'
];
// Installation - Pré-cache des ressources essentielles
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('[Service Worker] Pré-cache des ressources');
return cache.addAll(PRECACHE_URLS);
})
.then(() => {
console.log('[Service Worker] Installation terminée');
return self.skipWaiting();
})
);
});
// Activation - Nettoyage des anciens caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
console.log('[Service Worker] Suppression ancien cache:', cacheName);
return caches.delete(cacheName);
}
})
);
}).then(() => {
console.log('[Service Worker] Activation terminée');
return self.clients.claim();
})
);
});
// Stratégie de cache: Network First, fallback Cache
self.addEventListener('fetch', event => {
// Ignorer les requêtes non-GET et les requêtes cross-origin
if (event.request.method !== 'GET' ||
!event.request.url.startsWith(self.location.origin)) {
return;
}
// Pour les pages HTML: Network First
if (event.request.headers.get('accept').includes('text/html')) {
event.respondWith(
fetch(event.request)
.then(response => {
// Mettre à jour le cache avec la nouvelle version
const responseClone = response.clone();
caches.open(CACHE_NAME)
.then(cache => cache.put(event.request, responseClone));
return response;
})
.catch(() => {
// Fallback au cache ou page offline
return caches.match(event.request)
.then(cachedResponse => {
return cachedResponse || caches.match(OFFLINE_URL);
});
})
);
return;
}
// Pour les autres ressources: Cache First
event.respondWith(
caches.match(event.request)
.then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request)
.then(response => {
// Ne pas mettre en cache les erreurs
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Mettre en cache pour la prochaine fois
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => cache.put(event.request, responseToCache));
return response;
})
.catch(() => {
// Pour les CSS/JS, on peut retourner une réponse vide
if (event.request.url.includes('.css')) {
return new Response('/* Ressource non disponible hors ligne */', {
headers: { 'Content-Type': 'text/css' }
});
}
if (event.request.url.includes('.js')) {
return new Response('// Ressource non disponible hors ligne', {
headers: { 'Content-Type': 'application/javascript' }
});
}
});
})
);
});
// Gestion des messages
self.addEventListener('message', event => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});