MIGRATION VERS GITEA

This commit is contained in:
mathis committed 2026-08-25 01:23:13 +02:00
commit 5f9d991c15
333 files changed
+108514

No files matched your search

+488
View File
@@ -0,0 +1,488 @@
import React, { useEffect, useState } from 'react';
import { getCurrentUser, hasAccessTo } from '../../utils/userUtils';
import { getThreadsForUser } from '../../api/messagerie';
import { getAllUtilisateurs } from '../../api/utilisateurs';
import './AppHeader.css';
const NOTIFICATIONS_READ_PREFIX = 'app_notifications_read_v1';
const SNCF_LOGO_DATA_URI = "data:image/svg+xml,%3c?xml%20version='1.0'%20encoding='UTF-8'%20standalone='no'?%3e%3csvg%20version='1.0'%20viewBox='0%200%2070%2036.799999'%20xml:space='preserve'%20id='svg17'%20width='70'%20height='36.799999'%20xmlns='http://www.w3.org/2000/svg'%20xmlns:svg='http://www.w3.org/2000/svg'%3e%3cdefs%20id='defs21'%20/%3e%3clinearGradient%20id='SVG_GRADIENT'%20gradientUnits='userSpaceOnUse'%20x1='0'%20y1='16'%20x2='31.959299'%20y2='16'%20gradientTransform='matrix(2.1849396,0,0,2.1904762,-3.0604997e-7,-16.64762)'%3e%3cstop%20offset='0'%20style='stop-color:%23832F70'%20id='stop2'%20/%3e%3cstop%20offset='0.2472'%20style='stop-color:%23AC287C'%20id='stop4'%20/%3e%3cstop%20offset='0.8146'%20style='stop-color:%23DF2530'%20id='stop6'%20/%3e%3cstop%20offset='1'%20style='stop-color:%23DF2530'%20id='stop8'%20/%3e%3c/linearGradient%3e%3cpath%20fill='url(%23SVG_GRADIENT)'%20d='M%201.3109633,0.21904772%20C%200.43698745,0.21904772%200,0.43809528%200,1.3142858%20V%2035.704762%20C%200,36.361904%200.43698745,36.8%201.0924696,36.8%20H%2062.270776%20c%200.655482,0%201.09247,0%201.310963,-0.876191%200,0%206.117832,-19.495238%206.336326,-19.714286%200.218494,-0.657143%200,-1.095237%20-0.655483,-1.533333%20C%2049.379634,1.9714287%2019.445962,0%206.1178299,0%204.1513848,0%202.403433,0%201.3109633,0.21904772%20Z'%20id='path11'%20style='fill:url(%23SVG_GRADIENT);stroke-width:2.18771'%20/%3e%3cpath%20fill='%23ffffff'%20d='m%2013.983612,22.12381%20c%20-2.184939,-1.095239%20-3.714396,-1.752382%20-3.495902,-2.84762%200.436987,-1.314285%202.840421,-1.752381%204.806867,-1.752381%201.092469,0%201.747951,0.219047%202.621926,0.438096%20l%200.655482,-2.409525%20c%20-1.09247,-0.219047%20-1.747951,-0.219047%20-3.058915,-0.219047%20-4.151384,0%20-7.8657819,1.095238%20-8.7397579,3.504761%20-1.0924696,2.847619%201.966446,4.380953%204.8068679,5.695239%202.184939,1.095238%203.93289,1.971428%203.277408,3.285715%20-0.436987,1.095237%20-1.966446,1.533332%20-4.369878,1.533332%20-1.9664456,0%20-4.3698801,-0.657142%20-6.3363252,-1.533332%20l%20-1.0924697,2.409523%20c%201.3109634,0.657143%204.3698793,1.533333%206.7733121,1.533333%204.5883738,0%207.6472878,-1.095237%208.9582518,-3.723809%201.529458,-2.847619%20-1.966445,-4.600001%20-4.806867,-5.914285%20z'%20id='path13'%20style='stroke-width:2.18771'%20/%3e%3cpath%20fill='%23ffffff'%20d='m%2020.319937,26.285714%20c%200,0.219047%200,0.219047%200,0%200.218494,0%200.218494,0%200.218494,0%200,0%202.184939,-7.009524%202.184939,-7.009524%202.403434,3.285714%204.588373,7.666667%205.899337,12.266667%20h%203.714398%20l%201.966445,-5.914286%20c%200,0%200,0%200.218494,0%200,0%200,0%200.218493,0%200.655483,3.723809%204.151386,6.133333%209.395241,6.133333%203.277409,0%206.336325,-0.87619%206.991806,-1.314286%20l%201.966446,-6.133333%20h%207.428794%20l%200.655483,-2.409523%20h%20-7.428796%20l%201.310964,-3.942857%20h%208.958253%20L%2064.892704,15.55238%20H%2052.438549%20l%20-4.151387,13.361905%20c%20-0.873975,0.219048%20-1.966445,0.438095%20-3.277409,0.438095%20-2.403432,0%20-4.588373,-0.657142%20-5.680843,-1.971428%20-0.873975,-1.095238%20-1.092469,-2.190476%20-0.873975,-3.72381%200.436988,-3.504761%203.932891,-5.695237%208.302771,-5.695237%200.873975,0%201.966445,0%202.840421,0.219047%20l%200.873976,-2.628572%20c%20-1.092469,-0.219047%20-2.403435,-0.219047%20-3.714397,-0.219047%20-3.932892,0%20-7.428796,1.314286%20-9.613735,3.723809%200,0%20-0.218493,0%20-0.218493,0%200,0%20-0.218494,0%200,0%20l%201.09247,-3.504762%20H%2034.740537%20L%2030.589152,26.72381%20C%2029.496683,23.438094%2027.09325,19.057142%2024.252828,15.55238%20h%20-3.495904%20l%20-2.621926,8.104762%20c%201.310964,0.876191%201.747952,1.752381%202.184939,2.628572%20z'%20id='path15'%20style='stroke-width:2.18771'%20/%3e%3c/svg%3e";
const MENU_GROUPS = [
{
label: 'Dashboard',
href: '/dashboard',
match: ['/dashboard', '/']
},
{
label: 'Perturbations',
items: [
{ label: 'Gestion des travaux', href: '/travaux', match: ['/travaux'] },
{ label: 'Gestion des perturbations', href: '/perturbations', match: ['/perturbations'] }
]
},
{
label: 'Exploitation',
items: [
{ label: 'Gestion des gares', href: '/gares', match: ['/gares'] },
{ label: 'Gestion des materiels roulants', href: '/materiel-roulant', match: ['/materiel-roulant'] },
{ label: 'Gestion des horaires', href: '/horaires', match: ['/horaires'] },
{ label: 'Gestion des quais', href: '/quais', match: ['/quais'] },
{ label: 'Attribution Materiel-Roulant', href: '/attribution-materiel-roulant', match: ['/attribution-materiel-roulant'] },
{ label: 'Gestion des tarifications', href: '/tarifs', match: ['/tarifs'] }
]
},
{
label: 'Configuration',
items: [
{ label: 'Gestion des parametres', href: '/parametres', match: ['/parametres'] },
{ label: 'Gestion des menus regionaux', href: '/menu', match: ['/menu'] }
]
},
{
label: 'Diffusion',
items: [
{ label: "Gestion d'informations", href: '/informations', match: ['/informations'] },
{ label: 'Annonces sonores', href: '/annonces-sonores', match: ['/annonces-sonores'] }
]
},
{
label: 'Messagerie',
href: '/messagerie',
match: ['/messagerie']
}
];
function isPathActive(currentPath, matchList) {
return matchList.some((path) => {
if (path === '/') return currentPath === '/' || currentPath.startsWith('/dashboard');
return currentPath.startsWith(path);
});
}
export function addSystemNotification(userId, notif) {
if (!userId) return null;
try {
const existing = JSON.parse(window.localStorage.getItem(`app_system_notifications_${userId}`)) || [];
const id = Date.now();
existing.push({ id, date: new Date().toISOString(), ...notif });
window.localStorage.setItem(`app_system_notifications_${userId}`, JSON.stringify(existing));
window.dispatchEvent(new Event('app:notificationsChanged'));
return id;
} catch(_) {
return null;
}
}
export function updateSystemNotification(userId, notifId, updatedFields) {
if (!userId || !notifId) return;
try {
const existing = JSON.parse(window.localStorage.getItem(`app_system_notifications_${userId}`)) || [];
const index = existing.findIndex(n => n.id === notifId);
if (index !== -1) {
existing[index] = { ...existing[index], ...updatedFields };
window.localStorage.setItem(`app_system_notifications_${userId}`, JSON.stringify(existing));
window.dispatchEvent(new Event('app:notificationsChanged'));
}
} catch(_) {}
}
function getAccountPageHref() {
return '/mon-compte';
}
function getReadNotificationsForUser(userId) {
if (!userId) return [];
try {
const raw = window.localStorage.getItem(`${NOTIFICATIONS_READ_PREFIX}_${userId}`);
const parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? parsed : [];
} catch (_) {
return [];
}
}
function saveReadNotificationsForUser(userId, notificationIds) {
if (!userId) return;
try {
window.localStorage.setItem(`${NOTIFICATIONS_READ_PREFIX}_${userId}`, JSON.stringify(notificationIds || []));
} catch (_) {
// ignore localStorage failures
}
}
export default function AppHeader({ isLoggedIn, userName }) {
const currentPath = window.location.pathname;
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [expandedGroup, setExpandedGroup] = useState(null);
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const [isNotificationsOpen, setIsNotificationsOpen] = useState(false);
const [notifications, setNotifications] = useState([]);
const [notificationCount, setNotificationCount] = useState(0);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth > 980) {
setIsMenuOpen(false);
setExpandedGroup(null);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
const handleDocumentClick = (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
if (!target.closest('.app-header-user-menu')) {
setIsUserMenuOpen(false);
}
if (!target.closest('.has-submenu')) {
setExpandedGroup(null);
}
};
if (isUserMenuOpen || expandedGroup) {
document.addEventListener('click', handleDocumentClick);
}
return () => document.removeEventListener('click', handleDocumentClick);
}, [isUserMenuOpen, expandedGroup]);
useEffect(() => {
if (!isLoggedIn) {
setNotifications([]);
setNotificationCount(0);
return;
}
let active = true;
const loadNotifications = async () => {
const currentUser = getCurrentUser();
if (!currentUser?.id) return;
const [{ data: threads }, { data: users }] = await Promise.all([
getThreadsForUser(currentUser.id),
getAllUtilisateurs()
]);
if (!active) return;
const usersById = {};
(users || []).forEach((u) => { usersById[u.id] = u; });
const readIds = new Set(getReadNotificationsForUser(currentUser.id));
const items = (threads || [])
.filter((thread) => thread?.last_message && String(thread.last_message.sender_id) !== String(currentUser.id))
.map((thread) => {
const sender = usersById[thread.last_message.sender_id] || {};
const senderName = `${sender.prenom || ''} ${sender.nom || ''}`.trim() || sender.email || 'Utilisateur';
return {
id: `msg-${thread.last_message.id}`,
type: 'messagerie',
title: 'Nouveau message',
description: `${senderName}: ${thread.last_message.contenu || ''}`,
date: thread.last_message.created_at,
threadId: thread.id,
read: readIds.has(`msg-${thread.last_message.id}`)
};
})
.sort((a, b) => String(b.date).localeCompare(String(a.date)));
let systemItems = [];
try {
const sysRaw = JSON.parse(window.localStorage.getItem(`app_system_notifications_${currentUser.id}`)) || [];
systemItems = sysRaw.map(n => ({
id: `sys-${n.id}`,
type: 'system',
title: n.title,
description: n.description,
date: n.date,
link: n.link,
progress: n.progress,
read: readIds.has(`sys-${n.id}`)
}));
} catch(_) {}
const allItems = [...items, ...systemItems].sort((a, b) => String(b.date).localeCompare(String(a.date)));
setNotifications(allItems);
setNotificationCount(allItems.filter((item) => !item.read).length);
};
loadNotifications();
const onNotificationsChanged = () => loadNotifications();
window.addEventListener('app:notificationsChanged', onNotificationsChanged);
window.addEventListener('app:authChanged', onNotificationsChanged);
return () => {
active = false;
window.removeEventListener('app:notificationsChanged', onNotificationsChanged);
window.removeEventListener('app:authChanged', onNotificationsChanged);
};
}, [isLoggedIn]);
const handleAuthClick = () => {
if (isLoggedIn) {
window.location.href = '/logout';
}
};
const handleMenuToggle = () => {
setIsMenuOpen((prev) => !prev);
if (isMenuOpen) setExpandedGroup(null);
};
const handleSubmenuToggle = (groupLabel) => {
setExpandedGroup((prev) => (prev === groupLabel ? null : groupLabel));
};
const closeMobileNavAfterClick = () => {
if (window.innerWidth <= 980) {
setIsMenuOpen(false);
setExpandedGroup(null);
}
};
const handleUserMenuToggle = (event) => {
event.stopPropagation();
setIsUserMenuOpen((prev) => !prev);
};
const handleAuthAndClose = () => {
setIsUserMenuOpen(false);
handleAuthClick();
};
const handleNotificationClick = (notification) => {
const currentUser = getCurrentUser();
const alreadyRead = !!notification?.read;
if (currentUser?.id && notification?.id && !alreadyRead) {
const existing = getReadNotificationsForUser(currentUser.id);
const next = [...new Set([...existing, notification.id])];
saveReadNotificationsForUser(currentUser.id, next);
const nextNotifications = notifications.map((n) => (n.id === notification.id ? { ...n, read: true } : n));
setNotifications(nextNotifications);
setNotificationCount(nextNotifications.filter((n) => !n.read).length);
}
if (notification?.threadId) {
try { window.localStorage.setItem('messagerie_active_thread', notification.threadId); } catch (_) {}
window.location.href = '/messagerie';
return;
}
if (notification?.link) {
window.location.href = notification.link;
return;
}
setIsNotificationsOpen(false);
};
const accountPageHref = getAccountPageHref();
const user = getCurrentUser();
const filteredMenuGroups = MENU_GROUPS.map(group => {
// Cas où le groupe a ses propres attributs 'href' et 'match' (par exemple: Dashboard, Messagerie)
if (!group.items) {
const section = group.href.split('/')[1] || group.href.replace('/', '') || 'dashboard';
if (!hasAccessTo(user, section)) return null;
return group;
}
// Cas où le groupe contient des sous-éléments
const filteredItems = group.items.filter(item => {
if (item.href) {
const section = item.href.split('/')[1] || item.href.replace('/', '');
let permSection = section;
// Mappage des URLs vers les permissions exactes du système
if (section === 'materiel-roulant') permSection = 'materiel_roulant';
else if (section === 'attribution-materiel-roulant') permSection = 'horaires';
else if (section === 'tarifs') permSection = 'tarifs';
else if (section === 'menu') permSection = 'menu';
else if (section === 'travaux') permSection = 'travaux';
else if (section === 'perturbations') permSection = 'perturbations';
else if (section === 'pages') permSection = 'pages';
else if (section === 'informations') permSection = 'informations';
else if (section === 'parametres') permSection = 'parametres';
else if (section === 'gares') permSection = 'gares';
else if (section === 'quais') permSection = 'quais';
else if (section === 'annonces-sonores') permSection = 'annonces_sonores';
return hasAccessTo(user, permSection);
}
return true;
});
if (filteredItems.length === 0) return null;
return { ...group, items: filteredItems };
}).filter(Boolean);
return (
<header className="sncf-groupe app-header-shell">
<div className="app-header-main">
<a href={isLoggedIn ? '/dashboard' : '/'} className="app-header-brand" aria-label="Accueil gestion des exploitations">
<img alt="SNCF" src={SNCF_LOGO_DATA_URI} />
<div className="app-header-title">
<h1>Gestion des exploitations</h1>
<span>Panel d'administration regional</span>
</div>
</a>
{isLoggedIn && (
<button
type="button"
className="app-header-menu-toggle"
aria-label={isMenuOpen ? 'Fermer le menu' : 'Ouvrir le menu'}
aria-expanded={isMenuOpen}
onClick={handleMenuToggle}
>
<wcs-mat-icon icon={isMenuOpen ? 'close' : 'menu'}></wcs-mat-icon>
</button>
)}
{isLoggedIn && (
<nav className={`app-top-menu ${isMenuOpen ? 'open' : ''}`} aria-label="Navigation principale">
<ul>
{filteredMenuGroups.map((group) => {
const subItems = group.items || [];
const groupActive = group.match
? isPathActive(currentPath, group.match)
: subItems.some((item) => isPathActive(currentPath, item.match || [item.href]));
if (!subItems.length) {
return (
<li key={group.label} className="app-menu-group">
<a
className={groupActive ? 'active' : ''}
href={group.href}
aria-current={groupActive ? 'page' : undefined}
onClick={closeMobileNavAfterClick}
>
{group.label}
</a>
</li>
);
}
return (
<li key={group.label} className={`app-menu-group has-submenu ${expandedGroup === group.label ? 'open' : ''}`}>
<button
type="button"
className={groupActive ? 'active' : ''}
aria-expanded={expandedGroup === group.label}
onClick={() => handleSubmenuToggle(group.label)}
>
<span>{group.label}</span>
<wcs-mat-icon icon="expand_more"></wcs-mat-icon>
</button>
<div className="app-submenu" role="menu" aria-label={group.label}>
{subItems.map((item) => {
const itemActive = isPathActive(currentPath, item.match || [item.href]);
return (
<a
key={item.href}
href={item.href}
role="menuitem"
className={itemActive ? 'active' : ''}
aria-current={itemActive ? 'page' : undefined}
onClick={closeMobileNavAfterClick}
>
{item.label}
</a>
);
})}
</div>
</li>
);
})}
</ul>
</nav>
)}
<div className="app-header-actions">
{isLoggedIn && (
<button
type="button"
className="app-header-notification-btn"
aria-label="Ouvrir le centre de notifications"
onClick={() => setIsNotificationsOpen(true)}
>
<wcs-mat-icon icon="notifications"></wcs-mat-icon>
{notificationCount > 0 && <span className="app-header-notification-badge">{notificationCount > 99 ? '99+' : notificationCount}</span>}
</button>
)}
{isLoggedIn && (
<div className={`app-header-user-menu ${isUserMenuOpen ? 'open' : ''}`}>
<button
type="button"
className="app-header-user-trigger"
aria-expanded={isUserMenuOpen}
aria-label="Ouvrir le menu utilisateur"
onClick={handleUserMenuToggle}
>
<div className="app-header-user-pill" title={userName || 'Utilisateur'}>
<wcs-mat-icon icon="account_circle"></wcs-mat-icon>
<span>{userName || 'Utilisateur connecte'}</span>
</div>
<wcs-mat-icon icon="expand_more"></wcs-mat-icon>
</button>
<div className="app-header-user-accordion" role="menu" aria-label="Menu utilisateur">
<a href={accountPageHref} role="menuitem" onClick={() => setIsUserMenuOpen(false)}>
<wcs-mat-icon icon="manage_accounts"></wcs-mat-icon>
<span>Gestion du compte</span>
</a>
<button type="button" role="menuitem" onClick={handleAuthAndClose}>
<wcs-mat-icon icon="logout"></wcs-mat-icon>
<span>Deconnexion</span>
</button>
</div>
</div>
)}
</div>
</div>
{isLoggedIn && isNotificationsOpen && (
<div className="app-notifications-overlay" onClick={() => setIsNotificationsOpen(false)}>
<aside className="app-notifications-drawer" onClick={(e) => e.stopPropagation()}>
<div className="app-notifications-head">
<h3>Notifications</h3>
<button type="button" onClick={() => setIsNotificationsOpen(false)} aria-label="Fermer le panneau notifications">
<wcs-mat-icon icon="close"></wcs-mat-icon>
</button>
</div>
{notifications.length === 0 ? (
<p className="app-notifications-empty">Aucune notification pour le moment.</p>
) : (
<div className="app-notifications-list">
{notifications.map((notification) => (
<button key={notification.id} type="button" className={`app-notification-item ${notification.read ? 'read' : 'unread'}`} onClick={() => handleNotificationClick(notification)}>
<span className="app-notification-type">{notification.type === 'system' ? 'Système' : 'Messagerie'}</span>
<strong>{notification.title}</strong>
<span>{notification.description}</span>
{notification.progress !== undefined && notification.progress !== null && (
<div className="app-notification-progress-container">
<div className="app-notification-progress-bar" style={{ width: `${notification.progress}%` }}></div>
</div>
)}
<small>{notification.read ? 'Lu' : 'Non lu'}</small>
<small>{new Date(notification.date).toLocaleString('fr-FR')}</small>
</button>
))}
</div>
)}
</aside>
</div>
)}
</header>
);
}