RELEASE V.1.2.1 - Voici les fonctionnalités :

->Refonte visuelle + Prise en charge du mode sombre par l'ensemble des composants.
This commit is contained in:
mathis committed 2026-09-22 22:14:21 +02:00
1 parent d61422ddbf
commit 37ab831aac
44 files changed
+16238 -6613

No files matched your search

@@ -191,3 +191,85 @@
transform: none !important; transform: none !important;
cursor: default; cursor: default;
} }
/* Dark Mode */
body.dark-mode .cal-templates,
body.dark-mode .cal-scroll-area,
body.dark-mode .cal-legend,
[data-theme="dark"] .cal-templates,
[data-theme="dark"] .cal-scroll-area,
[data-theme="dark"] .cal-legend {
background: #151f30;
border-color: #243247;
color: #f1f5f9;
}
body.dark-mode .cal-templates-label,
[data-theme="dark"] .cal-templates-label {
color: #e2e8f0;
}
body.dark-mode .cal-tpl-btn,
[data-theme="dark"] .cal-tpl-btn {
background: #0b111e;
border-color: #243247;
color: #cbd5e1;
}
body.dark-mode .cal-tpl-btn:hover,
[data-theme="dark"] .cal-tpl-btn:hover {
background: #1e293b;
border-color: #3b82f6;
color: #f8fafc;
}
body.dark-mode .cal-month-title,
[data-theme="dark"] .cal-month-title {
color: #f8fafc;
border-bottom-color: #243247;
}
body.dark-mode .cal-header,
[data-theme="dark"] .cal-header {
color: #94a3b8;
}
body.dark-mode .cal-day,
[data-theme="dark"] .cal-day {
background: #0b111e;
border-color: #243247;
color: #e2e8f0;
}
body.dark-mode .cal-day:not(.disabled):not(.empty):hover,
[data-theme="dark"] .cal-day:not(.disabled):not(.empty):hover {
border-color: #38bdf8;
box-shadow: 0 0 0 1px #38bdf8;
}
body.dark-mode .cal-day.disabled,
[data-theme="dark"] .cal-day.disabled {
background: #111827;
color: #4b5563;
border-color: #1f2937;
}
body.dark-mode .cal-day.weekend,
[data-theme="dark"] .cal-day.weekend {
background: #1f1b13;
color: #fcd34d;
border-color: #3d2f14;
}
body.dark-mode .cal-day.ferie,
[data-theme="dark"] .cal-day.ferie {
background: #270d13;
color: #fca5a5;
border-color: #4c1d24;
}
body.dark-mode .cal-legend-item,
[data-theme="dark"] .cal-legend-item {
color: #94a3b8;
}
+161
View File
@@ -526,3 +526,164 @@
} }
} }
/* ==========================================================================
DARK MODE
========================================================================== */
body.dark-mode .app-header-shell,
[data-theme="dark"] .app-header-shell {
background: #151f30;
border-bottom-color: #243247;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
}
body.dark-mode .app-header-title h1,
[data-theme="dark"] .app-header-title h1 {
color: #f8fafc;
}
body.dark-mode .app-header-title span,
[data-theme="dark"] .app-header-title span {
color: #94a3b8;
}
body.dark-mode .app-menu-group > a,
body.dark-mode .app-menu-group > button,
[data-theme="dark"] .app-menu-group > a,
[data-theme="dark"] .app-menu-group > button {
color: #cbd5e1;
}
body.dark-mode .app-menu-group > a:hover,
body.dark-mode .app-menu-group > button:hover,
body.dark-mode .app-menu-group > a.active,
body.dark-mode .app-menu-group > button.active,
[data-theme="dark"] .app-menu-group > a:hover,
[data-theme="dark"] .app-menu-group > button:hover,
[data-theme="dark"] .app-menu-group > a.active,
[data-theme="dark"] .app-menu-group > button.active {
background: #1a2a42;
color: #38bdf8;
}
body.dark-mode .app-submenu,
[data-theme="dark"] .app-submenu {
background: #151f30;
border-color: #243247;
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.4);
}
body.dark-mode .app-submenu a,
[data-theme="dark"] .app-submenu a {
color: #cbd5e1;
}
body.dark-mode .app-submenu a:hover,
body.dark-mode .app-submenu a.active,
[data-theme="dark"] .app-submenu a:hover,
[data-theme="dark"] .app-submenu a.active {
background: #1a2a42;
color: #38bdf8;
}
body.dark-mode .app-header-notification-btn,
[data-theme="dark"] .app-header-notification-btn {
background: #0e1726;
border-color: #243247;
color: #38bdf8;
}
body.dark-mode .app-notifications-drawer,
[data-theme="dark"] .app-notifications-drawer {
background: #151f30;
border-left-color: #243247;
color: #f8fafc;
}
body.dark-mode .app-notifications-head,
[data-theme="dark"] .app-notifications-head {
border-bottom-color: #243247;
}
body.dark-mode .app-notification-item,
[data-theme="dark"] .app-notification-item {
background: #0e1726;
border-color: #243247;
color: #f8fafc;
}
body.dark-mode .app-notification-item:hover,
[data-theme="dark"] .app-notification-item:hover {
background: #1a2a42;
}
body.dark-mode .app-notification-item strong,
[data-theme="dark"] .app-notification-item strong {
color: #f8fafc;
}
body.dark-mode .app-notification-item span,
body.dark-mode .app-notification-item small,
[data-theme="dark"] .app-notification-item span,
[data-theme="dark"] .app-notification-item small {
color: #94a3b8;
}
body.dark-mode .app-header-user-pill,
[data-theme="dark"] .app-header-user-pill {
background: #0e1726;
border-color: #243247;
}
body.dark-mode .app-header-user-pill span,
[data-theme="dark"] .app-header-user-pill span {
color: #f8fafc;
}
body.dark-mode .app-header-user-accordion,
[data-theme="dark"] .app-header-user-accordion {
background: #151f30;
border-color: #243247;
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.4);
}
body.dark-mode .app-header-user-accordion a,
body.dark-mode .app-header-user-accordion button,
[data-theme="dark"] .app-header-user-accordion a,
[data-theme="dark"] .app-header-user-accordion button {
color: #cbd5e1;
}
body.dark-mode .app-header-user-accordion a:hover,
body.dark-mode .app-header-user-accordion button:hover,
[data-theme="dark"] .app-header-user-accordion a:hover,
[data-theme="dark"] .app-header-user-accordion button:hover {
background: #1a2a42;
color: #38bdf8;
}
body.dark-mode .app-header-menu-toggle,
[data-theme="dark"] .app-header-menu-toggle {
background: #0e1726;
border-color: #243247;
color: #38bdf8;
}
@media (max-width: 980px) {
body.dark-mode .app-top-menu,
[data-theme="dark"] .app-top-menu {
background: #151f30;
border-color: #243247;
}
body.dark-mode .app-submenu,
[data-theme="dark"] .app-submenu {
background: #0e1726;
}
body.dark-mode .app-header-user-accordion,
[data-theme="dark"] .app-header-user-accordion {
background: #0e1726;
}
}
+44 -51
View File
@@ -208,61 +208,48 @@ body.dark-mode .service-badge {
} }
/* Dashboard Overrides */ /* Dashboard Overrides */
body.dark-mode .dashboard-page { body.dark-mode .dashboard-page-root,
background-color: #121212 !important; body.dark-mode .dashboard-page,
[data-theme="dark"] .dashboard-page-root,
[data-theme="dark"] .dashboard-page {
background-color: #0b111e !important;
} }
body.dark-mode .dashboard-hero { body.dark-mode .dashboard-hero-card,
background: linear-gradient(140deg, #1e1e1e 0%, #2a2a2a 70%, #333 100%) !important; [data-theme="dark"] .dashboard-hero-card {
color: #e0e0e0 !important; background: linear-gradient(135deg, #051829 0%, #0d2847 60%, #15416b 100%) !important;
box-shadow: 0 14px 35px rgba(0,0,0,0.5) !important; border: 1px solid #243247 !important;
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.45) !important;
} }
body.dark-mode .dashboard-hero h1, body.dark-mode .dashboard-kpi-card,
body.dark-mode .dashboard-hero p, body.dark-mode .dashboard-widget-card,
body.dark-mode .dashboard-hero strong, body.dark-mode .dashboard-status-bar,
body.dark-mode .dashboard-subtitle, [data-theme="dark"] .dashboard-kpi-card,
body.dark-mode .dashboard-kicker { [data-theme="dark"] .dashboard-widget-card,
color: #e0e0e0 !important; [data-theme="dark"] .dashboard-status-bar {
background-color: #151f30 !important;
border-color: #243247 !important;
color: #f8fafc !important;
} }
body.dark-mode .stat-card, body.dark-mode .dashboard-incident-item,
body.dark-mode .dashboard-widget, body.dark-mode .dashboard-schedule-item,
body.dark-mode .widget-header-panel, body.dark-mode .dashboard-action-tile,
body.dark-mode .widget-content-panel { [data-theme="dark"] .dashboard-incident-item,
background-color: #1e1e1e !important; [data-theme="dark"] .dashboard-schedule-item,
border-color: #333 !important; [data-theme="dark"] .dashboard-action-tile {
color: #e0e0e0 !important; background-color: #0e1726 !important;
border-color: #243247 !important;
color: #f8fafc !important;
} }
body.dark-mode .stat-card-title, body.dark-mode .dashboard-incident-item:hover,
body.dark-mode .stat-card-value, body.dark-mode .dashboard-schedule-item:hover,
body.dark-mode .stat-card-subtitle, body.dark-mode .dashboard-action-tile:hover,
body.dark-mode .widget-item-text strong, [data-theme="dark"] .dashboard-incident-item:hover,
body.dark-mode .perturbation-item-route, [data-theme="dark"] .dashboard-schedule-item:hover,
body.dark-mode .horaire-item-route, [data-theme="dark"] .dashboard-action-tile:hover {
body.dark-mode .perturbation-item-details, background-color: #152238 !important;
body.dark-mode .quick-action-row-left { border-color: #38bdf8 !important;
color: #e0e0e0 !important;
}
body.dark-mode .stat-card-icon-wrap {
background-color: #333 !important;
color: #e0e0e0 !important;
}
body.dark-mode .quick-action-row {
background-color: #2a2a2a !important;
border-color: #333 !important;
color: #e0e0e0 !important;
}
body.dark-mode .quick-action-row:hover {
background-color: #333 !important;
color: #fff !important;
}
body.dark-mode .quick-action-icon {
background-color: #444 !important;
color: #e0e0e0 !important;
}
body.dark-mode .perturbation-item:hover,
body.dark-mode .horaire-item:hover {
background-color: #2a2a2a !important;
border-color: #444 !important;
} }
body.dark-mode .widget-header { body.dark-mode .widget-header {
background-color: #2a2a2a !important; background-color: #2a2a2a !important;
border-color: #333 !important; border-color: #333 !important;
@@ -565,6 +552,11 @@ body.dark-mode .materiel-roulant-card
body.dark-mode .dashboard-page, body.dark-mode .dashboard-page,
body.dark-mode .travaux-page, body.dark-mode .travaux-page,
body.dark-mode .travaux-page-root,
body.dark-mode .perturbations-page-root,
body.dark-mode .gares-page-root,
body.dark-mode .materiels-page-root,
body.dark-mode .horaires-page-root,
body.dark-mode .parametres-page, body.dark-mode .parametres-page,
body.dark-mode .tarifs-page, body.dark-mode .tarifs-page,
body.dark-mode .materiel-roulant-page, body.dark-mode .materiel-roulant-page,
@@ -577,7 +569,7 @@ body.dark-mode .systeme-maj-page,
body.dark-mode .activer-compte-page, body.dark-mode .activer-compte-page,
body.dark-mode .acces-non-autorise-page, body.dark-mode .acces-non-autorise-page,
body.dark-mode .menu-page { body.dark-mode .menu-page {
background-color: #121212 !important; background-color: #0b111e !important;
} }
body.dark-mode .premium-checkbox-label.selected { body.dark-mode .premium-checkbox-label.selected {
@@ -586,3 +578,4 @@ body.dark-mode .premium-checkbox-label.selected {
border-color: #0d47a1 !important; border-color: #0d47a1 !important;
} }
@@ -31,3 +31,25 @@
margin: 0 0 18px; margin: 0 0 18px;
} }
/* ==========================================================================
DARK MODE
========================================================================== */
body.dark-mode .acces-non-autorise-card,
[data-theme="dark"] .acces-non-autorise-card {
background: #151f30;
border-color: #243247;
color: #f8fafc;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
body.dark-mode .acces-non-autorise-card h2,
[data-theme="dark"] .acces-non-autorise-card h2 {
color: #f8fafc;
}
body.dark-mode .acces-non-autorise-card p,
[data-theme="dark"] .acces-non-autorise-card p {
color: #94a3b8;
}
@@ -50,3 +50,28 @@
} }
} }
/* ==========================================================================
DARK MODE
========================================================================== */
body.dark-mode .activation-account-card,
[data-theme="dark"] .activation-account-card {
background: #0e1726;
border-color: #243247;
}
body.dark-mode .activation-account-card h3,
[data-theme="dark"] .activation-account-card h3 {
color: #f8fafc;
}
body.dark-mode .activation-account-card p,
[data-theme="dark"] .activation-account-card p {
color: #cbd5e1;
}
body.dark-mode .activation-link,
[data-theme="dark"] .activation-link {
color: #38bdf8;
}
@@ -2058,3 +2058,59 @@
background-color: var(--as-gray-100); background-color: var(--as-gray-100);
} }
/* Dark Mode */
body.dark-mode .annonces-page,
[data-theme="dark"] .annonces-page {
background: #0b111e;
color: #f1f5f9;
}
body.dark-mode .annonces-page-header,
body.dark-mode .as-panel,
body.dark-mode .as-card,
body.dark-mode .as-modal-content,
[data-theme="dark"] .annonces-page-header,
[data-theme="dark"] .as-panel,
[data-theme="dark"] .as-card,
[data-theme="dark"] .as-modal-content {
background: #151f30;
border-color: #243247;
color: #f1f5f9;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
}
body.dark-mode .annonces-page-header h2,
[data-theme="dark"] .annonces-page-header h2 {
color: #f8fafc;
}
body.dark-mode .as-input-select,
body.dark-mode .as-input-text,
body.dark-mode .as-input-textarea,
[data-theme="dark"] .as-input-select,
[data-theme="dark"] .as-input-text,
[data-theme="dark"] .as-input-textarea {
background-color: #0b111e;
border-color: #243247;
color: #f8fafc;
}
body.dark-mode .as-table th,
[data-theme="dark"] .as-table th {
background-color: #0e1726;
border-bottom-color: #243247;
color: #94a3b8;
}
body.dark-mode .as-table td,
[data-theme="dark"] .as-table td {
border-bottom-color: #1e293b;
color: #e2e8f0;
}
body.dark-mode .as-table tbody tr:hover,
[data-theme="dark"] .as-table tbody tr:hover {
background-color: #1a273c;
}
@@ -373,4 +373,81 @@
} }
} }
/* Dark Mode */
body.dark-mode .attribution-mr-page,
[data-theme="dark"] .attribution-mr-page {
background-color: #0b111e;
color: #f1f5f9;
}
body.dark-mode .attribution-mr-header h2,
[data-theme="dark"] .attribution-mr-header h2 {
color: #f8fafc;
}
body.dark-mode .attribution-mr-description,
[data-theme="dark"] .attribution-mr-description {
color: #94a3b8;
}
body.dark-mode .attribution-mr-toolbar,
body.dark-mode .attribution-mr-timeline-wrap,
[data-theme="dark"] .attribution-mr-toolbar,
[data-theme="dark"] .attribution-mr-timeline-wrap {
background: #151f30;
border-color: #243247;
color: #f1f5f9;
}
body.dark-mode .attribution-mr-direction-btn,
[data-theme="dark"] .attribution-mr-direction-btn {
background: #0b111e;
border-color: #243247;
color: #cbd5e1;
}
body.dark-mode .attribution-mr-direction-btn.is-active,
[data-theme="dark"] .attribution-mr-direction-btn.is-active {
background: #0284c7;
border-color: #0284c7;
color: #ffffff;
}
body.dark-mode .attribution-mr-gantt-header,
[data-theme="dark"] .attribution-mr-gantt-header {
background: #0e1726;
border-color: #243247;
}
body.dark-mode .attribution-mr-gantt-row,
[data-theme="dark"] .attribution-mr-gantt-row {
border-bottom-color: #1e293b;
}
body.dark-mode .attribution-mr-gantt-label,
[data-theme="dark"] .attribution-mr-gantt-label {
background: #111a28;
border-right-color: #243247;
color: #f8fafc;
}
body.dark-mode .attribution-mr-materiels-group,
[data-theme="dark"] .attribution-mr-materiels-group {
background: #111a28;
border-color: #243247;
}
body.dark-mode .attribution-mr-materiels-group-title,
[data-theme="dark"] .attribution-mr-materiels-group-title {
color: #f8fafc;
}
body.dark-mode .attribution-mr-materiel-card,
[data-theme="dark"] .attribution-mr-materiel-card {
background: #151f30;
border-color: #243247;
color: #f1f5f9;
}
@@ -197,3 +197,54 @@
.prochains-departs li:last-child { .prochains-departs li:last-child {
border-bottom: none; border-bottom: none;
} }
/* Dark Mode */
body.dark-mode .vue-roulement-container,
[data-theme="dark"] .vue-roulement-container {
background-color: #0b111e;
color: #f1f5f9;
}
body.dark-mode .materiel-card,
[data-theme="dark"] .materiel-card {
background-color: #151f30;
border-color: #243247;
color: #f1f5f9;
}
body.dark-mode .materiel-header,
[data-theme="dark"] .materiel-header {
border-bottom-color: #243247;
}
body.dark-mode .materiel-header h3,
[data-theme="dark"] .materiel-header h3 {
color: #f8fafc;
}
body.dark-mode .materiel-header p,
[data-theme="dark"] .materiel-header p {
color: #94a3b8;
}
body.dark-mode .ligne-section h4,
[data-theme="dark"] .ligne-section h4 {
color: #e2e8f0;
}
body.dark-mode .timeline-body,
[data-theme="dark"] .timeline-body {
background-image: repeating-linear-gradient(to right, transparent 0, transparent calc(100% / 12 - 1px), #243247 calc(100% / 12 - 1px), #243247 calc(100% / 12));
}
body.dark-mode .prochains-departs ul,
[data-theme="dark"] .prochains-departs ul {
border-color: #243247;
background: #111a28;
}
body.dark-mode .prochains-departs li,
[data-theme="dark"] .prochains-departs li {
border-bottom-color: #1e293b;
color: #e2e8f0;
}
File diff suppressed because it is too large. Load diff
+462 -210
View File
@@ -16,6 +16,7 @@ export default function Dashboard() {
// État du chargement // État du chargement
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [lastRefreshed, setLastRefreshed] = useState(new Date());
// Perturbations récentes // Perturbations récentes
const [recentPerturbations, setRecentPerturbations] = useState([]); const [recentPerturbations, setRecentPerturbations] = useState([]);
@@ -23,12 +24,14 @@ export default function Dashboard() {
// Horaires récents // Horaires récents
const [recentHoraires, setRecentHoraires] = useState([]); const [recentHoraires, setRecentHoraires] = useState([]);
const [scopedRegionId, setScopedRegionId] = useState(null); const [scopedRegionId, setScopedRegionId] = useState(null);
const [currentUser, setCurrentUser] = useState(null);
// Fonction de chargement des données // Fonction de chargement des données
const loadDashboardData = useCallback(async () => { const loadDashboardData = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const user = getCurrentUser(); const user = getCurrentUser();
setCurrentUser(user);
const regionId = getScopedRegionId(user); const regionId = getScopedRegionId(user);
setScopedRegionId(regionId || null); setScopedRegionId(regionId || null);
@@ -88,10 +91,11 @@ export default function Dashboard() {
}); });
// Perturbations récentes (5 dernières) // Perturbations récentes (5 dernières)
setRecentPerturbations(perturbations.slice(0, 5)); setRecentPerturbations(perturbations.slice(0, 6));
// Horaires récents (5 derniers) // Horaires récents (6 derniers)
setRecentHoraires(horaires.slice(0, 5)); setRecentHoraires(horaires.slice(0, 6));
setLastRefreshed(new Date());
} catch (error) { } catch (error) {
console.error('Erreur lors du chargement du dashboard:', error); console.error('Erreur lors du chargement du dashboard:', error);
@@ -115,225 +119,473 @@ export default function Dashboard() {
window.location.href = path; window.location.href = path;
}; };
// Rendu des cartes de statistiques // Ponctualité théorique estimée
const renderStatCard = (title, value, icon, color = 'primary', subtitle = null) => ( const calculatePunctuality = () => {
<div className={`stat-card stat-card-${color}`}> if (!stats.horaires.total) return 100;
<div className="stat-card-header"> const impacted = stats.perturbations.actives;
<div className="stat-card-icon-wrap"> const rate = Math.max(0, Math.min(100, Math.round(((stats.horaires.total - impacted) / stats.horaires.total) * 100)));
<wcs-mat-icon icon={icon}></wcs-mat-icon> return rate;
</div> };
<div className="stat-card-content">
<span className="stat-card-title">{title}</span>
<span className="stat-card-value">{loading ? '...' : value}</span>
{subtitle && <span className="stat-card-subtitle">{subtitle}</span>}
</div>
</div>
</div>
);
// Rendu de la liste des perturbations récentes
const renderRecentPerturbations = () => (
<div className="dashboard-widget widget-panel">
<div className="widget-header widget-header-panel">
<h3>
<span className="widget-title-icon"><wcs-mat-icon icon="warning"></wcs-mat-icon></span>
Perturbations récentes
</h3>
<wcs-button
className="wcs-secondary wcs-small widget-link-btn"
onClick={() => navigate('/perturbations')}
>
Voir tout
</wcs-button>
</div>
<div className="widget-content widget-content-panel">
{loading ? (
<div className="dashboard-state widget-state-panel">
<wcs-spinner mode="border" />
<p>Chargement...</p>
</div>
) : recentPerturbations.length === 0 ? (
<div className="dashboard-state dashboard-state-empty widget-state-panel">
<wcs-mat-icon className="dashboard-state-icon" icon="check_circle"></wcs-mat-icon>
<p>Aucune perturbation</p>
</div>
) : (
<div className="perturbations-list widget-list-panel">
{recentPerturbations.map(p => (
<div key={p.id} className="perturbation-item widget-list-item">
<div className="perturbation-item-header widget-item-main">
<div className="widget-item-text">
<strong>{p.horaire?.numero_train || 'N/A'}</strong>
<span className="perturbation-item-route">
{p.horaire?.gare_depart_nom} → {p.horaire?.gare_arrivee_nom}
</span>
</div>
<wcs-badge className={
p.type_perturbation === 'retard' ? 'wcs-warning' :
p.type_perturbation === 'suppression' ? 'wcs-danger' :
'wcs-info'
}>
{p.type_perturbation === 'retard' ? `Retard +${p.duree_retard_minutes || 0} min` :
p.type_perturbation === 'suppression' ? 'Supprime' :
'Modifié'}
</wcs-badge>
</div>
<div className="perturbation-item-details">
<span>
<wcs-mat-icon icon="info"></wcs-mat-icon>
{p.cause || 'Non spécifiée'}
</span>
<span className={p.actif ? 'status-active' : 'status-inactive'}>
{p.actif ? '● Active' : '○ Inactive'}
</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
// Rendu de la liste des horaires récents
const renderRecentHoraires = () => (
<div className="dashboard-widget widget-panel">
<div className="widget-header widget-header-panel">
<h3>
<span className="widget-title-icon"><wcs-mat-icon icon="schedule"></wcs-mat-icon></span>
Horaires récents
</h3>
<wcs-button
className="wcs-secondary wcs-small widget-link-btn"
onClick={() => navigate('/horaires')}
>
Voir tout
</wcs-button>
</div>
<div className="widget-content widget-content-panel">
{loading ? (
<div className="dashboard-state widget-state-panel">
<wcs-spinner mode="border" />
<p>Chargement...</p>
</div>
) : recentHoraires.length === 0 ? (
<div className="dashboard-state dashboard-state-empty widget-state-panel">
<wcs-mat-icon className="dashboard-state-icon" icon="train"></wcs-mat-icon>
<p>Aucun horaire</p>
</div>
) : (
<div className="horaires-list widget-list-panel">
{recentHoraires.map(h => (
<div key={h.id} className="horaire-item widget-list-item">
<div className="horaire-item-header widget-item-main">
<div className="widget-item-text">
<strong>{h.numero_train}</strong>
{h.type_train && (
<wcs-badge className="wcs-info horaire-type-badge">
{h.type_train}
</wcs-badge>
)}
</div>
{h.ligne_nom && (
<wcs-badge className="wcs-secondary">{h.ligne_nom}</wcs-badge>
)}
</div>
<div className="horaire-item-route">
<span>
{h.gare_depart_nom} ({formatHeure(h.heure_depart)})
</span>
<wcs-mat-icon icon="arrow_forward"></wcs-mat-icon>
<span>
{h.gare_arrivee_nom} ({formatHeure(h.heure_arrivee)})
</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
// Rendu des actions rapides
const renderQuickActions = () => (
<div className="dashboard-widget widget-panel">
<div className="widget-header widget-header-panel">
<h3>
<span className="widget-title-icon"><wcs-mat-icon icon="bolt"></wcs-mat-icon></span>
Actions rapides
</h3>
</div>
<div className="widget-content widget-content-panel">
<div className="quick-actions">
<a className="quick-action-row" href="/perturbations/creer">
<span className="quick-action-row-left">
<span className="quick-action-icon"><wcs-mat-icon icon="add"></wcs-mat-icon></span>
<span>Créer une perturbation</span>
</span>
<wcs-mat-icon icon="arrow_forward"></wcs-mat-icon>
</a>
<a className="quick-action-row" href="/horaires">
<span className="quick-action-row-left">
<span className="quick-action-icon"><wcs-mat-icon icon="schedule"></wcs-mat-icon></span>
<span>Gérer les horaires</span>
</span>
<wcs-mat-icon icon="arrow_forward"></wcs-mat-icon>
</a>
<a className="quick-action-row" href="/gares">
<span className="quick-action-row-left">
<span className="quick-action-icon"><wcs-mat-icon icon="location_on"></wcs-mat-icon></span>
<span>Gérer les gares</span>
</span>
<wcs-mat-icon icon="arrow_forward"></wcs-mat-icon>
</a>
<button className="quick-action-row quick-action-row-refresh" type="button" onClick={loadDashboardData}>
<span className="quick-action-row-left">
<span className="quick-action-icon"><wcs-mat-icon icon="refresh"></wcs-mat-icon></span>
<span>Actualiser les données</span>
</span>
<wcs-mat-icon icon="refresh"></wcs-mat-icon>
</button>
</div>
</div>
</div>
);
return ( return (
<div className="sncf-groupe dashboard-page panel-layout"> <div className="dashboard-page-root">
<div className="panel-content"> <div className="dashboard-container">
<header className="dashboard-hero">
<div> {/* ================================================================
<p className="dashboard-kicker">Panel de gestion</p> HERO BANNER
<h1>Tableau de bord</h1> ================================================================ */}
<p className="dashboard-subtitle">Suivez l'activite des horaires, perturbations et donnees d'exploitation en temps reel.</p> <section className="dashboard-hero-card">
<div className="dashboard-hero-content">
<div className="dashboard-hero-kicker-row">
<span className="dashboard-hero-kicker">
<wcs-mat-icon icon="speed"></wcs-mat-icon>
Centre de supervision ferroviaire
</span>
{scopedRegionId && (
<span className="dashboard-region-badge">
<wcs-mat-icon icon="location_on"></wcs-mat-icon>
Région : {scopedRegionId}
</span>
)}
</div> </div>
<div className="dashboard-hero-side">
<wcs-button className="wcs-primary" onClick={loadDashboardData}> <h1 className="dashboard-hero-title">
Tableau de bord
{currentUser?.nom ? <span className="dashboard-user-greeting">, {currentUser.prenom || currentUser.nom}</span> : null}
</h1>
<p className="dashboard-hero-desc">
Pilotage centralisé des circulations, gestion du plan de transport, détection des aléas et état des infrastructures en temps réel.
</p>
</div>
<div className="dashboard-hero-actions">
<div className="dashboard-sync-status">
<span className="dashboard-pulse-dot"></span>
<span>Dernière synchro : {lastRefreshed.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}</span>
</div>
<div className="dashboard-btn-group">
<wcs-button
className="wcs-primary"
onClick={loadDashboardData}
disabled={loading}
>
<wcs-mat-icon icon="refresh"></wcs-mat-icon> <wcs-mat-icon icon="refresh"></wcs-mat-icon>
Actualiser Actualiser
</wcs-button> </wcs-button>
{scopedRegionId && <p>Region courante : <strong>{scopedRegionId}</strong></p>}
</div>
</header>
<div className="stats-grid"> <wcs-button
{renderStatCard('Horaires', stats.horaires.total, 'schedule', 'primary', `${stats.horaires.actifs} actifs`)} className="wcs-secondary dashboard-quick-create-btn"
{renderStatCard('Perturbations', stats.perturbations.total, 'warning', 'warning', `${stats.perturbations.actives} actives`)} onClick={() => navigate('/perturbations/creer')}
{renderStatCard('Gares', stats.gares.total, 'location_on', 'info', 'Desservies')} >
{renderStatCard('Lignes', stats.lignes.total, 'route', 'secondary', 'Actives')} <wcs-mat-icon icon="warning"></wcs-mat-icon>
{renderStatCard('Types de trains', stats.typesTrains.total, 'train', 'success', 'Disponibles')} Signaler un incident
{renderStatCard('Retards', stats.perturbations.retards, 'schedule', 'warning', 'En cours')} </wcs-button>
</div>
</div>
</section>
{/* ================================================================
SUPERVISION STATUS BAR
================================================================ */}
<div className="dashboard-status-bar">
<div className="dashboard-status-item">
<div className={`dashboard-status-indicator ${stats.perturbations.actives > 0 ? 'is-warning' : 'is-success'}`}>
<wcs-mat-icon icon={stats.perturbations.actives > 0 ? 'warning' : 'check_circle'}></wcs-mat-icon>
</div>
<div className="dashboard-status-text">
<span className="dashboard-status-label">État du Réseau</span>
<strong>{stats.perturbations.actives > 0 ? `${stats.perturbations.actives} incident(s) en cours` : 'Circulation normale et fluide'}</strong>
</div>
</div> </div>
<div className="widgets-grid"> <div className="dashboard-status-divider"></div>
<div className="widgets-column">
{renderRecentPerturbations()} <div className="dashboard-status-item">
{renderQuickActions()} <div className="dashboard-status-indicator is-info">
<wcs-mat-icon icon="analytics"></wcs-mat-icon>
</div> </div>
<div className="widgets-column">{renderRecentHoraires()}</div> <div className="dashboard-status-text">
<span className="dashboard-status-label">Taux de régularité théorique</span>
<strong>{calculatePunctuality()}% à l'heure</strong>
</div> </div>
</div> </div>
<div className="dashboard-status-divider"></div>
<div className="dashboard-status-item">
<div className="dashboard-status-indicator is-secondary">
<wcs-mat-icon icon="train"></wcs-mat-icon>
</div>
<div className="dashboard-status-text">
<span className="dashboard-status-label">Sillons actifs</span>
<strong>{stats.horaires.total} circulations programmées</strong>
</div>
</div>
</div>
{/* ================================================================
KPI CARDS
================================================================ */}
<div className="dashboard-kpis-grid">
{/* Sillons / Horaires */}
<div className="dashboard-kpi-card card-horaires" onClick={() => navigate('/horaires')}>
<div className="dashboard-kpi-top">
<div className="dashboard-kpi-icon-wrapper">
<wcs-mat-icon icon="schedule"></wcs-mat-icon>
</div>
<span className="dashboard-kpi-badge">{stats.horaires.actifs} actifs</span>
</div>
<div className="dashboard-kpi-number">{loading ? '...' : stats.horaires.total}</div>
<div className="dashboard-kpi-title">Horaires & Sillons</div>
<div className="dashboard-kpi-footer">
<span>Gérer les plans de transport</span>
<wcs-mat-icon icon="arrow_forward"></wcs-mat-icon>
</div>
</div>
{/* Perturbations */}
<div className="dashboard-kpi-card card-perturbations" onClick={() => navigate('/perturbations')}>
<div className="dashboard-kpi-top">
<div className="dashboard-kpi-icon-wrapper">
<wcs-mat-icon icon="warning"></wcs-mat-icon>
</div>
<span className={`dashboard-kpi-badge ${stats.perturbations.actives > 0 ? 'badge-danger' : 'badge-neutral'}`}>
{stats.perturbations.actives} actives
</span>
</div>
<div className="dashboard-kpi-number">{loading ? '...' : stats.perturbations.total}</div>
<div className="dashboard-kpi-title">Perturbations & Aléas</div>
<div className="dashboard-kpi-footer">
<span>{stats.perturbations.retards} retards · {stats.perturbations.suppressions} suppressions</span>
<wcs-mat-icon icon="arrow_forward"></wcs-mat-icon>
</div>
</div>
{/* Gares */}
<div className="dashboard-kpi-card card-gares" onClick={() => navigate('/gares')}>
<div className="dashboard-kpi-top">
<div className="dashboard-kpi-icon-wrapper">
<wcs-mat-icon icon="location_on"></wcs-mat-icon>
</div>
<span className="dashboard-kpi-badge">Réseau</span>
</div>
<div className="dashboard-kpi-number">{loading ? '...' : stats.gares.total}</div>
<div className="dashboard-kpi-title">Gares & Points d'arrêt</div>
<div className="dashboard-kpi-footer">
<span>Configuration des quais & services</span>
<wcs-mat-icon icon="arrow_forward"></wcs-mat-icon>
</div>
</div>
{/* Lignes */}
<div className="dashboard-kpi-card card-lignes" onClick={() => navigate('/horaires')}>
<div className="dashboard-kpi-top">
<div className="dashboard-kpi-icon-wrapper">
<wcs-mat-icon icon="route"></wcs-mat-icon>
</div>
<span className="dashboard-kpi-badge">Maillage</span>
</div>
<div className="dashboard-kpi-number">{loading ? '...' : stats.lignes.total}</div>
<div className="dashboard-kpi-title">Lignes Commerciales</div>
<div className="dashboard-kpi-footer">
<span>{stats.typesTrains.total} matériels types</span>
<wcs-mat-icon icon="arrow_forward"></wcs-mat-icon>
</div>
</div>
</div>
{/* ================================================================
MAIN PANELS GRID
================================================================ */}
<div className="dashboard-main-grid">
{/* Colonne gauche : Perturbations et Actions */}
<div className="dashboard-column">
{/* Widget Perturbations récentes */}
<div className="dashboard-widget-card">
<div className="dashboard-widget-header">
<div className="dashboard-widget-title-wrap">
<div className="dashboard-widget-icon icon-warning">
<wcs-mat-icon icon="warning_amber"></wcs-mat-icon>
</div>
<div>
<h3 className="dashboard-widget-title">Incidents & Perturbations</h3>
<span className="dashboard-widget-subtitle">Derniers aléas déclarés sur le réseau</span>
</div>
</div>
<wcs-button
className="wcs-secondary wcs-small"
onClick={() => navigate('/perturbations')}
>
Tout voir ({stats.perturbations.total})
</wcs-button>
</div>
<div className="dashboard-widget-body">
{loading ? (
<div className="dashboard-loading-box">
<wcs-spinner mode="border"></wcs-spinner>
<span>Chargement des incidents...</span>
</div>
) : recentPerturbations.length === 0 ? (
<div className="dashboard-empty-box">
<div className="dashboard-empty-icon is-success">
<wcs-mat-icon icon="check_circle_outline"></wcs-mat-icon>
</div>
<h4>Aucune perturbation active</h4>
<p>Toutes les circulations ferroviaires s'effectuent conformément au plan de transport.</p>
</div>
) : (
<div className="dashboard-incident-list">
{recentPerturbations.map(p => (
<div
key={p.id}
className={`dashboard-incident-item ${p.type_perturbation === 'suppression' ? 'item-suppression' : 'item-retard'}`}
onClick={() => navigate(`/perturbations/modifier/${p.id}`)}
>
<div className="dashboard-incident-header">
<div className="dashboard-incident-train-badge">
<wcs-mat-icon icon="train"></wcs-mat-icon>
<strong>{p.horaire?.numero_train || 'Train'}</strong>
</div>
<span className={`dashboard-badge-pill ${
p.type_perturbation === 'retard' ? 'pill-warning' :
p.type_perturbation === 'suppression' ? 'pill-danger' :
'pill-info'
}`}>
<wcs-mat-icon icon={p.type_perturbation === 'suppression' ? 'block' : 'schedule'}></wcs-mat-icon>
{p.type_perturbation === 'retard' ? `+${p.duree_retard_minutes || 0} min` :
p.type_perturbation === 'suppression' ? 'Supprimé' :
'Modifié'}
</span>
</div>
<div className="dashboard-incident-route">
<span className="station-name">{p.horaire?.gare_depart_nom || 'Départ'}</span>
<wcs-mat-icon icon="trending_flat"></wcs-mat-icon>
<span className="station-name">{p.horaire?.gare_arrivee_nom || 'Arrivée'}</span>
</div>
<div className="dashboard-incident-footer">
<span className="dashboard-incident-cause">
<wcs-mat-icon icon="info_outline"></wcs-mat-icon>
{p.cause || 'Motif d\'exploitation non spécifié'}
</span>
<span className={`dashboard-status-tag ${p.actif ? 'tag-active' : 'tag-inactive'}`}>
{p.actif ? 'En cours' : 'Résolu'}
</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Widget Actions Rapides */}
<div className="dashboard-widget-card">
<div className="dashboard-widget-header">
<div className="dashboard-widget-title-wrap">
<div className="dashboard-widget-icon icon-primary">
<wcs-mat-icon icon="bolt"></wcs-mat-icon>
</div>
<div>
<h3 className="dashboard-widget-title">Centre d'actions rapides</h3>
<span className="dashboard-widget-subtitle">Raccourcis de gestion et exploitation</span>
</div>
</div>
</div>
<div className="dashboard-widget-body">
<div className="dashboard-quick-grid">
<a className="dashboard-action-tile" href="/perturbations/creer">
<div className="dashboard-action-icon bg-amber">
<wcs-mat-icon icon="add_alert"></wcs-mat-icon>
</div>
<div className="dashboard-action-info">
<strong>Déclarer un retard / incident</strong>
<span>Ajuster les horaires et diffuser l'information</span>
</div>
<wcs-mat-icon className="action-chevron" icon="chevron_right"></wcs-mat-icon>
</a>
<a className="dashboard-action-tile" href="/travaux/creer">
<div className="dashboard-action-icon bg-orange">
<wcs-mat-icon icon="construction"></wcs-mat-icon>
</div>
<div className="dashboard-action-info">
<strong>Programmer des travaux</strong>
<span>Définir des chantiers et bus de substitution</span>
</div>
<wcs-mat-icon className="action-chevron" icon="chevron_right"></wcs-mat-icon>
</a>
<a className="dashboard-action-tile" href="/horaires/attribution-quais">
<div className="dashboard-action-icon bg-sky">
<wcs-mat-icon icon="alt_route"></wcs-mat-icon>
</div>
<div className="dashboard-action-info">
<strong>Attribution des voies & quais</strong>
<span>Régulation schématique des départs et arrivées</span>
</div>
<wcs-mat-icon className="action-chevron" icon="chevron_right"></wcs-mat-icon>
</a>
<a className="dashboard-action-tile" href="/attribution-materiel-roulant">
<div className="dashboard-action-icon bg-indigo">
<wcs-mat-icon icon="directions_railway"></wcs-mat-icon>
</div>
<div className="dashboard-action-info">
<strong>Roulement du matériel</strong>
<span>Affectation des rames et compositions</span>
</div>
<wcs-mat-icon className="action-chevron" icon="chevron_right"></wcs-mat-icon>
</a>
</div>
</div>
</div>
</div>
{/* Colonne droite : Horaires & Circulation */}
<div className="dashboard-column">
<div className="dashboard-widget-card">
<div className="dashboard-widget-header">
<div className="dashboard-widget-title-wrap">
<div className="dashboard-widget-icon icon-info">
<wcs-mat-icon icon="schedule"></wcs-mat-icon>
</div>
<div>
<h3 className="dashboard-widget-title">Plan de transport récent</h3>
<span className="dashboard-widget-subtitle">Circulations programmées au référentiel</span>
</div>
</div>
<wcs-button
className="wcs-secondary wcs-small"
onClick={() => navigate('/horaires')}
>
Tous les horaires ({stats.horaires.total})
</wcs-button>
</div>
<div className="dashboard-widget-body">
{loading ? (
<div className="dashboard-loading-box">
<wcs-spinner mode="border"></wcs-spinner>
<span>Chargement des horaires...</span>
</div>
) : recentHoraires.length === 0 ? (
<div className="dashboard-empty-box">
<div className="dashboard-empty-icon is-info">
<wcs-mat-icon icon="train"></wcs-mat-icon>
</div>
<h4>Aucun horaire enregistré</h4>
<p>Commencez par créer des circulations ou importez une fiche horaire.</p>
<wcs-button
className="wcs-primary wcs-small"
onClick={() => navigate('/horaires')}
>
Ajouter un horaire
</wcs-button>
</div>
) : (
<div className="dashboard-schedule-list">
{recentHoraires.map(h => (
<div
key={h.id}
className="dashboard-schedule-item"
onClick={() => navigate('/horaires')}
>
<div className="dashboard-schedule-header">
<div className="dashboard-schedule-badges">
<span className="train-id-badge">
<wcs-mat-icon icon="train"></wcs-mat-icon>
{h.numero_train}
</span>
{h.type_train && (
<span className="train-type-pill">{h.type_train}</span>
)}
</div>
{h.ligne_nom && (
<span className="line-pill">
<wcs-mat-icon icon="timeline"></wcs-mat-icon>
{h.ligne_nom}
</span>
)}
</div>
<div className="dashboard-schedule-timeline">
<div className="timeline-station departure">
<span className="station-time">{formatHeure(h.heure_depart)}</span>
<span className="station-dot"></span>
<span className="station-title">{h.gare_depart_nom || 'Départ'}</span>
</div>
<div className="timeline-connector">
<span className="connector-line"></span>
<wcs-mat-icon icon="chevron_right"></wcs-mat-icon>
</div>
<div className="timeline-station arrival">
<span className="station-time">{formatHeure(h.heure_arrivee)}</span>
<span className="station-dot"></span>
<span className="station-title">{h.gare_arrivee_nom || 'Arrivée'}</span>
</div>
</div>
{h.gares_desservies && h.gares_desservies.length > 0 && (
<div className="dashboard-schedule-stops">
<wcs-mat-icon icon="more_horiz"></wcs-mat-icon>
<span>{h.gares_desservies.length} arrêt(s) intermédiaire(s)</span>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
{/* Carte état du système / infos d'exploitation */}
<div className="dashboard-system-card">
<div className="dashboard-system-header">
<div className="dashboard-system-icon">
<wcs-mat-icon icon="storage"></wcs-mat-icon>
</div>
<div>
<h4>État des services d'exploitation</h4>
<p>Système opérationnel & données à jour</p>
</div>
</div>
<div className="dashboard-system-stats">
<div className="system-stat-col">
<span>Circulations</span>
<strong>{stats.horaires.total}</strong>
</div>
<div className="system-stat-col">
<span>Gares reliées</span>
<strong>{stats.gares.total}</strong>
</div>
<div className="system-stat-col">
<span>Incidents</span>
<strong className={stats.perturbations.actives > 0 ? 'text-warning' : 'text-success'}>
{stats.perturbations.actives}
</strong>
</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
); );
} }
+978 -330
View File
File diff suppressed because it is too large. Load diff
+594 -293
View File
File diff suppressed because it is too large. Load diff
+26 -6
View File
@@ -219,12 +219,32 @@ export default function ImportGaresGTFS() {
}; };
return ( return (
<div className="sncf-groupe gares-page-root">
<div className="panel-layout"> <div className="panel-layout">
<div className="panel-nav"><NavPanel /></div> <div className="panel-nav"><NavPanel /></div>
<div className="panel-content"> <div className="panel-content gares-modern-container">
<wcs-card> <section className="gares-hero-banner">
<div style={{ padding: 20 }}> <div className="gares-hero-content">
<h2>Import Gares depuis GTFS</h2> <div className="gares-hero-tags">
<span className="gares-kicker-pill">
<wcs-mat-icon icon="file_upload" size="s"></wcs-mat-icon>
GTFS Transits
</span>
</div>
<h1 className="gares-hero-title">Import de Gares GTFS</h1>
<p className="gares-hero-subtitle">
Analysez et importez en masse les données d&apos;arrêts et de correspondances depuis un flux GTFS.
</p>
</div>
<div className="gares-hero-actions">
<wcs-button className="wcs-secondary" onClick={() => { window.location.assign('/gares'); }}>
<wcs-mat-icon icon="arrow_back"></wcs-mat-icon>
Retour aux gares
</wcs-button>
</div>
</section>
<div className="gares-controls-card">
{step === 1 && ( {step === 1 && (
<div> <div>
@@ -292,7 +312,7 @@ export default function ImportGaresGTFS() {
<small>{groups[routeName].length} gare(s)</small> <small>{groups[routeName].length} gare(s)</small>
<wcs-button mode="clear" size="s" onClick={(e) => { e.stopPropagation(); selectAllInGroup(routeName, true); }}>Tout</wcs-button> <wcs-button mode="clear" size="s" onClick={(e) => { e.stopPropagation(); selectAllInGroup(routeName, true); }}>Tout</wcs-button>
<wcs-button mode="clear" size="s" onClick={(e) => { e.stopPropagation(); selectAllInGroup(routeName, false); }}>Aucun</wcs-button> <wcs-button mode="clear" size="s" onClick={(e) => { e.stopPropagation(); selectAllInGroup(routeName, false); }}>Aucun</wcs-button>
<span>{expandedRoutes[routeName] ? '▲' : '▼'}</span> <wcs-mat-icon icon={expandedRoutes[routeName] ? 'expand_less' : 'expand_more'} size="s"></wcs-mat-icon>
</div> </div>
</div> </div>
{expandedRoutes[routeName] && ( {expandedRoutes[routeName] && (
@@ -398,7 +418,7 @@ export default function ImportGaresGTFS() {
)} )}
</div> </div>
</wcs-card> </div>
</div> </div>
</div> </div>
); );
+89
View File
@@ -237,3 +237,92 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
/* Dark Mode */
body.dark-mode .info-gare-desc-preview,
[data-theme="dark"] .info-gare-desc-preview {
color: #94a3b8;
}
body.dark-mode .display-dates-info,
[data-theme="dark"] .display-dates-info {
color: #cbd5e1;
}
body.dark-mode .display-dates-info small,
[data-theme="dark"] .display-dates-info small {
color: #64748b;
}
body.dark-mode .status-active,
[data-theme="dark"] .status-active {
background: rgba(22, 163, 74, 0.2);
color: #4ade80;
border-color: rgba(22, 163, 74, 0.4);
}
body.dark-mode .status-scheduled,
[data-theme="dark"] .status-scheduled {
background: rgba(37, 99, 235, 0.2);
color: #60a5fa;
border-color: rgba(37, 99, 235, 0.4);
}
body.dark-mode .status-expired,
[data-theme="dark"] .status-expired {
background: #1e293b;
color: #94a3b8;
border-color: #334155;
}
body.dark-mode .target-gare,
[data-theme="dark"] .target-gare {
background: #0d2744;
color: #38bdf8;
border-color: #0369a1;
}
body.dark-mode .target-ligne,
[data-theme="dark"] .target-ligne {
background: #2b1307;
color: #fb923c;
border-color: #7c2d12;
}
body.dark-mode .target-global,
[data-theme="dark"] .target-global {
background: #250e38;
color: #c084fc;
border-color: #6b21a8;
}
body.dark-mode .gare-search-suggestions,
[data-theme="dark"] .gare-search-suggestions {
background: #151f30;
border-color: #243247;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.45);
}
body.dark-mode .gare-search-suggestions li,
[data-theme="dark"] .gare-search-suggestions li {
color: #e2e8f0;
}
body.dark-mode .gare-search-suggestions li:hover,
[data-theme="dark"] .gare-search-suggestions li:hover {
background: #1e293b;
color: #38bdf8;
}
body.dark-mode .selected-gare-badge,
[data-theme="dark"] .selected-gare-badge {
background: #0d2744;
border-color: #0369a1;
color: #38bdf8;
}
body.dark-mode .info-gare-modal .modal-footer,
[data-theme="dark"] .info-gare-modal .modal-footer {
border-top-color: #243247;
background: #0e1726;
}
+3 -3
View File
@@ -285,17 +285,17 @@ const InfoGaresTab = forwardRef((props, ref) => {
<div className="target-info-list"> <div className="target-info-list">
{targetGare && ( {targetGare && (
<span className="target-badge-item target-gare"> <span className="target-badge-item target-gare">
🚆 {targetGare} <wcs-mat-icon icon="train" size="s"></wcs-mat-icon> {targetGare}
</span> </span>
)} )}
{targetLigne && ( {targetLigne && (
<span className="target-badge-item target-ligne"> <span className="target-badge-item target-ligne">
🎫 Ligne {targetLigne} <wcs-mat-icon icon="confirmation_number" size="s"></wcs-mat-icon> Ligne {targetLigne}
</span> </span>
)} )}
{!targetGare && !targetLigne && ( {!targetGare && !targetLigne && (
<span className="target-badge-item target-global"> <span className="target-badge-item target-global">
🌐 Toutes gares / lignes <wcs-mat-icon icon="public" size="s"></wcs-mat-icon> Toutes gares / lignes
</span> </span>
)} )}
</div> </div>
+46
View File
@@ -100,3 +100,49 @@
pointer-events: none; pointer-events: none;
display: block; display: block;
} }
/* Dark Mode */
body.dark-mode .rich-text-editor,
[data-theme="dark"] .rich-text-editor {
background: #0b111e;
border-color: #243247;
}
body.dark-mode .rich-text-editor:focus-within,
[data-theme="dark"] .rich-text-editor:focus-within {
border-color: #0088ce;
box-shadow: 0 0 0 3px rgba(0, 136, 206, 0.25);
}
body.dark-mode .rte-toolbar,
[data-theme="dark"] .rte-toolbar {
background: #111a28;
border-bottom-color: #243247;
}
body.dark-mode .rte-btn,
[data-theme="dark"] .rte-btn {
color: #94a3b8;
}
body.dark-mode .rte-btn:hover,
[data-theme="dark"] .rte-btn:hover {
background: #1e293b;
color: #f8fafc;
border-color: #334155;
}
body.dark-mode .rte-separator,
[data-theme="dark"] .rte-separator {
background: #243247;
}
body.dark-mode .rte-content,
[data-theme="dark"] .rte-content {
color: #f8fafc;
}
body.dark-mode .rte-content:empty::before,
[data-theme="dark"] .rte-content:empty::before {
color: #64748b;
}
@@ -87,3 +87,47 @@
border-color: #0047ab; border-color: #0047ab;
box-shadow: 0 0 0 2px rgba(0, 71, 171, 0.2); box-shadow: 0 0 0 2px rgba(0, 71, 171, 0.2);
} }
/* Dark Mode */
body.dark-mode .attribution-depart-arrivee,
[data-theme="dark"] .attribution-depart-arrivee {
background-color: #151f30;
color: #f1f5f9;
border-color: #243247;
}
body.dark-mode .attribution-depart-arrivee-header h2,
[data-theme="dark"] .attribution-depart-arrivee-header h2 {
color: #38bdf8;
}
body.dark-mode .attribution-depart-arrivee-table th,
[data-theme="dark"] .attribution-depart-arrivee-table th {
background-color: #0e1726;
color: #94a3b8;
border-bottom-color: #243247;
}
body.dark-mode .attribution-depart-arrivee-table td,
[data-theme="dark"] .attribution-depart-arrivee-table td {
border-bottom-color: #1e293b;
color: #e2e8f0;
}
body.dark-mode .attribution-depart-arrivee-table tr.row-depart,
[data-theme="dark"] .attribution-depart-arrivee-table tr.row-depart {
background-color: #0d2744;
}
body.dark-mode .attribution-depart-arrivee-table tr.row-arrivee,
[data-theme="dark"] .attribution-depart-arrivee-table tr.row-arrivee {
background-color: #0e2e21;
}
body.dark-mode .quai-select,
[data-theme="dark"] .quai-select {
background-color: #0b111e;
border-color: #243247;
color: #f8fafc;
}
@@ -362,3 +362,76 @@
background-color: #fafafa; background-color: #fafafa;
border-radius: 0 0 8px 8px; border-radius: 0 0 8px 8px;
} }
/* Dark Mode */
body.dark-mode .timeline-container,
[data-theme="dark"] .timeline-container {
background-color: #151f30;
border-color: #243247;
}
body.dark-mode .timeline-header,
body.dark-mode .quai-label-header,
[data-theme="dark"] .timeline-header,
[data-theme="dark"] .quai-label-header {
background-color: #0e1726;
border-color: #243247;
color: #f8fafc;
}
body.dark-mode .quai-label,
[data-theme="dark"] .quai-label {
background-color: #111a28;
border-color: #243247;
color: #f8fafc;
}
body.dark-mode .timeline-track-row,
[data-theme="dark"] .timeline-track-row {
border-color: #243247;
}
body.dark-mode .hour-mark,
[data-theme="dark"] .hour-mark {
border-left-color: #243247;
color: #94a3b8;
}
body.dark-mode .train-modal-content,
[data-theme="dark"] .train-modal-content {
background-color: #151f30;
color: #f8fafc;
border: 1px solid #243247;
}
body.dark-mode .train-modal-header,
body.dark-mode .train-modal-footer,
[data-theme="dark"] .train-modal-header,
[data-theme="dark"] .train-modal-footer {
background-color: #0e1726;
border-color: #243247;
}
body.dark-mode .train-modal-header h3,
[data-theme="dark"] .train-modal-header h3 {
color: #38bdf8;
}
body.dark-mode .train-modal-tabs,
[data-theme="dark"] .train-modal-tabs {
background-color: #0e1726;
border-color: #243247;
}
body.dark-mode .tab-btn,
[data-theme="dark"] .tab-btn {
color: #94a3b8;
}
body.dark-mode .tab-btn.active,
[data-theme="dark"] .tab-btn.active {
background-color: #1e293b;
color: #38bdf8;
border-bottom-color: #38bdf8;
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+100
View File
@@ -337,3 +337,103 @@
box-shadow: 0 6px 15px rgba(37, 99, 235, 0.4) !important; box-shadow: 0 6px 15px rgba(37, 99, 235, 0.4) !important;
transform: translateY(-1px); transform: translateY(-1px);
} }
/* Dark Mode */
body.dark-mode .imports-page-container,
[data-theme="dark"] .imports-page-container {
color: #f1f5f9;
}
body.dark-mode .imports-header p,
[data-theme="dark"] .imports-header p {
color: #94a3b8;
}
body.dark-mode .stepper-line,
[data-theme="dark"] .stepper-line {
background-color: #243247;
}
body.dark-mode .step-circle,
[data-theme="dark"] .step-circle {
background-color: #151f30;
border-color: #243247;
color: #94a3b8;
}
body.dark-mode .step-item.active .step-circle,
[data-theme="dark"] .step-item.active .step-circle {
background-color: #0d2744;
border-color: #38bdf8;
color: #38bdf8;
}
body.dark-mode .step-label,
[data-theme="dark"] .step-label {
color: #94a3b8;
}
body.dark-mode .step-item.active .step-label,
[data-theme="dark"] .step-item.active .step-label {
color: #f8fafc;
}
body.dark-mode .step-content-card,
[data-theme="dark"] .step-content-card {
background: #151f30;
border-color: #243247;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
}
body.dark-mode .upload-dropzone,
[data-theme="dark"] .upload-dropzone {
background: #0b111e;
border-color: #243247;
}
body.dark-mode .import-table-wrapper,
[data-theme="dark"] .import-table-wrapper {
background: #151f30;
border-color: #243247;
}
body.dark-mode .import-table th,
[data-theme="dark"] .import-table th {
background: #0e1726;
color: #94a3b8;
border-bottom-color: #243247;
}
body.dark-mode .import-table td,
[data-theme="dark"] .import-table td {
border-bottom-color: #1e293b;
color: #e2e8f0;
}
body.dark-mode .import-table tbody tr:hover,
[data-theme="dark"] .import-table tbody tr:hover {
background-color: #1a273c;
}
body.dark-mode .step-actions,
[data-theme="dark"] .step-actions {
background: #0e1726;
border-color: #243247;
}
body.dark-mode .info-box,
[data-theme="dark"] .info-box {
background: #0d2744;
border-color: #0284c7;
}
body.dark-mode .info-box-title,
[data-theme="dark"] .info-box-title {
color: #f8fafc;
}
body.dark-mode .info-box-text,
[data-theme="dark"] .info-box-text {
color: #cbd5e1;
}
+35
View File
@@ -764,3 +764,38 @@ wcs-tabs[headers-only] {
border-color: #0b4a7a; border-color: #0b4a7a;
box-shadow: 0 0 0 3px rgba(11, 74, 122, 0.1); box-shadow: 0 0 0 3px rgba(11, 74, 122, 0.1);
} }
/* Dark Mode */
body.dark-mode .informations-page,
[data-theme="dark"] .informations-page {
background-color: #0b111e;
color: #f1f5f9;
}
body.dark-mode .informations-page-header,
body.dark-mode .informations-tabs-shell,
[data-theme="dark"] .informations-page-header,
[data-theme="dark"] .informations-tabs-shell {
background: #151f30;
border-color: #243247;
color: #f1f5f9;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
}
body.dark-mode .informations-page-header-main h2,
[data-theme="dark"] .informations-page-header-main h2 {
color: #f8fafc;
}
body.dark-mode .informations-page-description,
[data-theme="dark"] .informations-page-description {
color: #94a3b8;
}
body.dark-mode .ig-select,
[data-theme="dark"] .ig-select {
background-color: #0b111e;
border-color: #243247;
color: #f8fafc;
}
+49
View File
@@ -230,3 +230,52 @@
justify-content: center; justify-content: center;
} }
} }
/* ==========================================================================
DARK MODE
========================================================================== */
body.dark-mode .admin-login-page,
[data-theme="dark"] .admin-login-page {
background:
radial-gradient(ellipse at 15% 20%, rgba(0, 136, 206, 0.15) 0%, transparent 50%),
radial-gradient(ellipse at 85% 80%, rgba(131, 47, 112, 0.12) 0%, transparent 50%),
#0b111e;
}
body.dark-mode .admin-login-shell,
[data-theme="dark"] .admin-login-shell {
background: #151f30;
border-color: #243247;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
}
body.dark-mode .admin-login-panel,
[data-theme="dark"] .admin-login-panel {
background: #151f30;
}
body.dark-mode .admin-login-panel-header h2,
[data-theme="dark"] .admin-login-panel-header h2 {
color: #f8fafc;
}
body.dark-mode .admin-login-panel-header p,
[data-theme="dark"] .admin-login-panel-header p {
color: #94a3b8;
}
body.dark-mode .admin-login-form label,
[data-theme="dark"] .admin-login-form label {
color: #94a3b8;
}
body.dark-mode .admin-login-link,
[data-theme="dark"] .admin-login-link {
color: #38bdf8;
}
body.dark-mode .admin-login-link:hover,
[data-theme="dark"] .admin-login-link:hover {
color: #7dd3fc;
}
+41 -49
View File
@@ -62,13 +62,9 @@ export default function MaterielCard({ materiel, onEdit, onDelete }) {
return ( return (
<div <div
className="card materiel-card" className="materiel-card-modern"
style={{ style={{
border: '1px solid #e0e0e0', padding: '16px',
borderRadius: 8,
padding: 12,
boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
background: '#fff',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
justifyContent: 'space-between', justifyContent: 'space-between',
@@ -77,7 +73,21 @@ export default function MaterielCard({ materiel, onEdit, onDelete }) {
> >
<div> <div>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<div ref={imgContainerRef} style={{ width: '100%', height: 140, borderRadius: 6, marginBottom: 12, display: 'flex', alignItems: 'center', justifyContent: 'flex-start', overflow: 'hidden', position: 'relative' }}> <div
ref={imgContainerRef}
style={{
width: '100%',
height: 140,
borderRadius: 10,
marginBottom: 12,
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
overflow: 'hidden',
position: 'relative',
background: '#f1f5f9'
}}
>
{current ? ( {current ? (
<img <img
ref={imgRef} ref={imgRef}
@@ -87,7 +97,10 @@ export default function MaterielCard({ materiel, onEdit, onDelete }) {
style={{ height: '100%', width: 'auto', objectFit: 'contain', objectPosition: 'left center', display: 'block', transform: `translateX(${offsetX}px)` }} style={{ height: '100%', width: 'auto', objectFit: 'contain', objectPosition: 'left center', display: 'block', transform: `translateX(${offsetX}px)` }}
/> />
) : ( ) : (
<div style={{ width: '100%', height: '100%', background: '#f5f5f5', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999', borderRadius: 6 }}>No image</div> <div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', gap: 4 }}>
<wcs-mat-icon icon="train" size="m"></wcs-mat-icon>
<span style={{ fontSize: 12 }}>Sans image</span>
</div>
)} )}
{(() => { {(() => {
@@ -100,22 +113,9 @@ export default function MaterielCard({ materiel, onEdit, onDelete }) {
<button <button
aria-label="Précédent" aria-label="Précédent"
onClick={prev} onClick={prev}
style={{ className="materiel-card-nav-btn prev"
position: 'absolute',
left: 6,
top: '50%',
transform: 'translateY(-50%)',
background: 'rgba(0,0,0,0.55)',
color: '#fff',
border: 'none',
borderRadius: 6,
padding: '8px 10px',
cursor: 'pointer',
zIndex: 3,
boxShadow: '0 2px 6px rgba(0,0,0,0.2)'
}}
> >
‹ <wcs-mat-icon icon="chevron_left" size="s"></wcs-mat-icon>
</button> </button>
)} )}
@@ -123,22 +123,9 @@ export default function MaterielCard({ materiel, onEdit, onDelete }) {
<button <button
aria-label="Suivant" aria-label="Suivant"
onClick={next} onClick={next}
style={{ className="materiel-card-nav-btn next"
position: 'absolute',
right: 6,
top: '50%',
transform: 'translateY(-50%)',
background: 'rgba(0,0,0,0.55)',
color: '#fff',
border: 'none',
borderRadius: 6,
padding: '8px 10px',
cursor: 'pointer',
zIndex: 3,
boxShadow: '0 2px 6px rgba(0,0,0,0.2)'
}}
> >
› <wcs-mat-icon icon="chevron_right" size="s"></wcs-mat-icon>
</button> </button>
)} )}
</> </>
@@ -146,24 +133,29 @@ export default function MaterielCard({ materiel, onEdit, onDelete }) {
})()} })()}
</div> </div>
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 10 }}>
<div style={{ fontWeight: 600, fontSize: 15 }}>{materiel?.nom ?? '-'}</div> <div style={{ fontWeight: 800, fontSize: 16, color: '#0f172a' }}>{materiel?.nom ?? '-'}</div>
<div style={{ fontSize: 12, color: '#666' }}>{materiel?.nom_technique ?? '-'}</div> <div style={{ fontSize: 12, color: '#64748b', fontWeight: 600 }}>{materiel?.nom_technique ?? '-'}</div>
</div> </div>
<div style={{ fontSize: 13, color: '#333', marginBottom: 8 }}> <div style={{ fontSize: 13, color: '#334155', display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 12 }}>
<div>Capacité: <strong>{materiel?.capacite ?? '-'}</strong></div> <div>Capacité : <strong>{materiel?.capacite ? `${materiel.capacite} places` : '-'}</strong></div>
<div>Type: <strong>{materiel?.type_train ?? '-'}</strong></div> <div>Type de train : <strong>{materiel?.type_train ?? '-'}</strong></div>
<div>Numéro série: <strong>{materiel?.numero_serie ?? '-'}</strong></div> <div>N° de série : <strong>{materiel?.numero_serie ?? '-'}</strong></div>
</div> </div>
</div> </div>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 8 }}> <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 12, borderTop: '1px solid #f1f5f9', paddingTop: 10 }}>
<wcs-button onClick={onEdit}>Modifier</wcs-button> <wcs-button className="wcs-secondary" onClick={onEdit}>
<wcs-button className="wcs-danger" onClick={onDelete}>Supprimer</wcs-button> <wcs-mat-icon icon="edit" size="s"></wcs-mat-icon>
Modifier
</wcs-button>
<wcs-button className="wcs-danger" onClick={onDelete}>
<wcs-mat-icon icon="delete" size="s"></wcs-mat-icon>
Supprimer
</wcs-button>
</div> </div>
</div> </div>
</div> </div>
); );
} }
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+172
View File
@@ -645,3 +645,175 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
/* ==========================================================================
DARK MODE
========================================================================== */
body.dark-mode .menu-regional-page .panel-content,
[data-theme="dark"] .menu-regional-page .panel-content {
background: #0b111e;
}
body.dark-mode .menu-page-header,
[data-theme="dark"] .menu-page-header {
background: #151f30;
border-color: #243247;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
}
body.dark-mode .menu-page-header-main h2,
[data-theme="dark"] .menu-page-header-main h2 {
color: #f8fafc;
}
body.dark-mode .menu-page-kicker,
body.dark-mode .menu-page-description,
[data-theme="dark"] .menu-page-kicker,
[data-theme="dark"] .menu-page-description {
color: #94a3b8;
}
body.dark-mode .menu-page-region,
[data-theme="dark"] .menu-page-region {
color: #cbd5e1;
}
body.dark-mode .menu-card,
[data-theme="dark"] .menu-card {
background: #151f30;
border-color: #243247;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
}
body.dark-mode .menu-card-title,
[data-theme="dark"] .menu-card-title {
color: #f8fafc;
}
body.dark-mode .menu-card-desc,
body.dark-mode .menu-card-meta,
[data-theme="dark"] .menu-card-desc,
[data-theme="dark"] .menu-card-meta {
color: #94a3b8;
}
body.dark-mode .structure-panel,
body.dark-mode .edit-panel,
body.dark-mode .menu-simple-card,
body.dark-mode .menu-editor,
body.dark-mode .menu-hier-editor,
[data-theme="dark"] .structure-panel,
[data-theme="dark"] .edit-panel,
[data-theme="dark"] .menu-simple-card,
[data-theme="dark"] .menu-editor,
[data-theme="dark"] .menu-hier-editor {
background: #151f30;
border-color: #243247;
color: #f8fafc;
}
body.dark-mode .structure-panel-header,
body.dark-mode .edit-panel-header,
[data-theme="dark"] .structure-panel-header,
[data-theme="dark"] .edit-panel-header {
background: #0e1726;
border-bottom-color: #243247;
color: #f8fafc;
}
body.dark-mode .menu-element,
body.dark-mode .sous-element-row,
body.dark-mode .brique-row,
[data-theme="dark"] .menu-element,
[data-theme="dark"] .sous-element-row,
[data-theme="dark"] .brique-row {
border-color: #243247;
}
body.dark-mode .menu-element-row:hover,
[data-theme="dark"] .menu-element-row:hover {
background: #1a2a42;
}
body.dark-mode .menu-element-row.selected,
[data-theme="dark"] .menu-element-row.selected {
background: #1e3a8a;
}
body.dark-mode .menu-element-label,
[data-theme="dark"] .menu-element-label {
color: #f8fafc;
}
body.dark-mode .sous-elements-list,
[data-theme="dark"] .sous-elements-list {
background: #0e1726;
}
body.dark-mode .sous-element-row:hover,
[data-theme="dark"] .sous-element-row:hover {
background: #1a2a42;
}
body.dark-mode .sous-element-row.selected,
[data-theme="dark"] .sous-element-row.selected {
background: #1e3a8a;
}
body.dark-mode .briques-list,
[data-theme="dark"] .briques-list {
background: #0b111e;
border-top-color: #243247;
}
body.dark-mode .brique-row:hover,
[data-theme="dark"] .brique-row:hover {
background: #1a2a42;
}
body.dark-mode .brique-row.selected,
[data-theme="dark"] .brique-row.selected {
background: #1e3a8a;
}
body.dark-mode .form-field label,
body.dark-mode .menu-field label,
[data-theme="dark"] .form-field label,
[data-theme="dark"] .menu-field label {
color: #94a3b8;
}
body.dark-mode .form-field input,
body.dark-mode .form-field select,
body.dark-mode .form-field textarea,
body.dark-mode .menu-field input,
body.dark-mode .menu-field select,
[data-theme="dark"] .form-field input,
[data-theme="dark"] .form-field select,
[data-theme="dark"] .form-field textarea,
[data-theme="dark"] .menu-field input,
[data-theme="dark"] .menu-field select {
background: #0b111e;
border-color: #243247;
color: #f8fafc;
}
body.dark-mode .menu-node,
body.dark-mode .menu-editor-item,
[data-theme="dark"] .menu-node,
[data-theme="dark"] .menu-editor-item {
background: #0e1726;
border-color: #243247;
}
body.dark-mode .menu-node.nested,
[data-theme="dark"] .menu-node.nested {
background: #0b111e;
border-color: #243247;
}
body.dark-mode .menu-toggle,
[data-theme="dark"] .menu-toggle {
color: #f8fafc;
}
+58
View File
@@ -177,3 +177,61 @@
} }
} }
/* Dark Mode */
body.dark-mode .messagerie-sidebar,
body.dark-mode .messagerie-main,
[data-theme="dark"] .messagerie-sidebar,
[data-theme="dark"] .messagerie-main {
background: #151f30;
border-color: #243247;
color: #f1f5f9;
}
body.dark-mode .messagerie-thread-item,
[data-theme="dark"] .messagerie-thread-item {
background: #111a28;
border-color: #243247;
color: #f1f5f9;
}
body.dark-mode .messagerie-thread-item.active,
[data-theme="dark"] .messagerie-thread-item.active {
background: #0d2744;
border-color: #0088ce;
}
body.dark-mode .messagerie-conversation,
[data-theme="dark"] .messagerie-conversation {
background: #0b111e;
}
body.dark-mode .messagerie-bubble,
[data-theme="dark"] .messagerie-bubble {
background: #151f30;
border-color: #243247;
color: #f1f5f9;
}
body.dark-mode .messagerie-bubble.mine,
[data-theme="dark"] .messagerie-bubble.mine {
background: #0d2744;
border-color: #0369a1;
}
body.dark-mode .messagerie-compose-head,
body.dark-mode .messagerie-compose-box,
[data-theme="dark"] .messagerie-compose-head,
[data-theme="dark"] .messagerie-compose-box {
border-color: #243247;
}
body.dark-mode .messagerie-compose-head select,
body.dark-mode .messagerie-compose-box textarea,
[data-theme="dark"] .messagerie-compose-head select,
[data-theme="dark"] .messagerie-compose-box textarea {
background: #0b111e;
border-color: #243247;
color: #f8fafc;
}
+66
View File
@@ -122,3 +122,69 @@
} }
} }
@media (max-width: 600px) {
.mon-compte-page-header {
padding: 16px;
}
.mon-compte-card {
padding: 14px;
}
.mon-compte-actions wcs-button {
width: 100%;
}
}
/* ==========================================================================
DARK MODE
========================================================================== */
body.dark-mode .mon-compte-page,
[data-theme="dark"] .mon-compte-page {
background: #0b111e;
}
body.dark-mode .mon-compte-page-header,
[data-theme="dark"] .mon-compte-page-header {
background: linear-gradient(140deg, #151f30 0%, #1a2a42 60%, #203554 100%);
border: 1px solid #243247;
box-shadow: 0 14px 35px rgba(0, 0, 0, 0.4);
}
body.dark-mode .mon-compte-card,
[data-theme="dark"] .mon-compte-card {
background: #151f30;
border-color: #243247;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
color: #f8fafc;
}
body.dark-mode .mon-compte-card .form-group label,
[data-theme="dark"] .mon-compte-card .form-group label {
color: #94a3b8;
}
body.dark-mode .mon-compte-card input,
[data-theme="dark"] .mon-compte-card input {
background: #0b111e;
border-color: #243247;
color: #f8fafc;
}
body.dark-mode .mon-compte-card input:focus,
[data-theme="dark"] .mon-compte-card input:focus {
border-color: #38bdf8;
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2);
}
body.dark-mode .mon-compte-meta,
[data-theme="dark"] .mon-compte-meta {
color: #94a3b8;
}
body.dark-mode .mon-compte-loading,
[data-theme="dark"] .mon-compte-loading {
color: #94a3b8;
}
+171
View File
@@ -438,3 +438,174 @@ h1.preview-titre {
font-size: 12px; font-size: 12px;
margin: 0; margin: 0;
} }
@media (max-width: 900px) {
.editeur-layout,
.cms-page-editor {
grid-template-columns: 1fr;
}
.editeur-header {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.preview-toolbar {
padding: 10px 16px;
}
.preview-content {
padding: 30px 16px;
}
}
/* ==========================================================================
DARK MODE
========================================================================== */
body.dark-mode .editeur-page-container,
[data-theme="dark"] .editeur-page-container {
background-color: #0b111e;
color: #f8fafc;
}
body.dark-mode .editeur-header,
[data-theme="dark"] .editeur-header {
background: #151f30;
border: 1px solid #243247;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
body.dark-mode .page-builder,
[data-theme="dark"] .page-builder {
background: #151f30;
border-color: #243247;
}
body.dark-mode .builder-item,
[data-theme="dark"] .builder-item {
border-color: #243247;
background: #0e1726;
}
body.dark-mode .item-controls,
[data-theme="dark"] .item-controls {
background: #151f30;
border-bottom-color: #243247;
}
body.dark-mode .comp-name,
[data-theme="dark"] .comp-name {
color: #94a3b8;
}
body.dark-mode .comp-edit label,
[data-theme="dark"] .comp-edit label {
color: #94a3b8;
}
body.dark-mode .comp-edit input,
body.dark-mode .comp-edit textarea,
body.dark-mode .comp-edit select,
[data-theme="dark"] .comp-edit input,
[data-theme="dark"] .comp-edit textarea,
[data-theme="dark"] .comp-edit select {
background: #0b111e;
border-color: #243247;
color: #f8fafc;
}
body.dark-mode .sidebar-section,
[data-theme="dark"] .sidebar-section {
background: #151f30;
border: 1px solid #243247;
color: #f8fafc;
}
body.dark-mode .sidebar-section h3,
[data-theme="dark"] .sidebar-section h3 {
border-bottom-color: #243247;
color: #f8fafc;
}
body.dark-mode .comp-tool,
[data-theme="dark"] .comp-tool {
border-color: #243247;
background: #0e1726;
color: #f8fafc;
}
body.dark-mode .comp-tool:hover,
[data-theme="dark"] .comp-tool:hover {
background: #1a2a42;
border-color: #38bdf8;
}
body.dark-mode .cms-builder,
[data-theme="dark"] .cms-builder {
background: #151f30;
border-color: #243247;
}
body.dark-mode .cms-block,
[data-theme="dark"] .cms-block {
border-color: #243247;
}
body.dark-mode .cms-block-header,
[data-theme="dark"] .cms-block-header {
background: #0e1726;
border-bottom-color: #243247;
}
body.dark-mode .cms-block-title,
[data-theme="dark"] .cms-block-title {
color: #f8fafc;
}
body.dark-mode .cms-sidebar,
[data-theme="dark"] .cms-sidebar {
background: #151f30;
border-color: #243247;
}
body.dark-mode .cms-block-add,
[data-theme="dark"] .cms-block-add {
border-color: #243247;
background: #0e1726;
color: #38bdf8;
}
body.dark-mode .cms-block-add:hover,
[data-theme="dark"] .cms-block-add:hover {
background: #1a2a42;
}
body.dark-mode .cms-label,
[data-theme="dark"] .cms-label {
color: #94a3b8;
}
body.dark-mode .cms-input,
body.dark-mode .cms-select,
body.dark-mode .cms-textarea,
[data-theme="dark"] .cms-input,
[data-theme="dark"] .cms-select,
[data-theme="dark"] .cms-textarea {
background: #0b111e;
border-color: #243247;
color: #f8fafc;
}
body.dark-mode .cms-checkbox,
[data-theme="dark"] .cms-checkbox {
color: #f8fafc;
}
body.dark-mode .titre-input,
[data-theme="dark"] .titre-input {
background: #151f30;
border-color: #243247;
color: #f8fafc;
}
+53
View File
@@ -52,3 +52,56 @@
display: flex; display: flex;
gap: 10px; gap: 10px;
} }
@media (max-width: 768px) {
.pages-gestion-container {
padding: 12px;
}
.header-actions {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
}
/* ==========================================================================
DARK MODE
========================================================================== */
body.dark-mode .pages-gestion-container,
[data-theme="dark"] .pages-gestion-container {
background: #0b111e;
color: #f8fafc;
}
body.dark-mode .wcs-table,
[data-theme="dark"] .wcs-table {
background: #151f30;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
body.dark-mode .wcs-table th,
[data-theme="dark"] .wcs-table th {
background-color: #0e1726;
color: #94a3b8;
border-bottom-color: #243247;
}
body.dark-mode .wcs-table td,
[data-theme="dark"] .wcs-table td {
border-bottom-color: #243247;
color: #f8fafc;
}
body.dark-mode .badge-statut.publie,
[data-theme="dark"] .badge-statut.publie {
background-color: rgba(34, 197, 94, 0.15);
color: #4ade80;
}
body.dark-mode .badge-statut.brouillon,
[data-theme="dark"] .badge-statut.brouillon {
background-color: rgba(234, 179, 8, 0.15);
color: #fde047;
}
+139
View File
@@ -208,3 +208,142 @@
} }
} }
@media (max-width: 600px) {
.param-logo-bg-controls {
flex-direction: column;
}
}
/* ==========================================================================
DARK MODE
========================================================================== */
body.dark-mode .parametres-page,
[data-theme="dark"] .parametres-page {
background: #0b111e;
}
body.dark-mode .parametres-page-header,
[data-theme="dark"] .parametres-page-header {
background: linear-gradient(140deg, #151f30 0%, #1a2a42 60%, #203554 100%);
border: 1px solid #243247;
box-shadow: 0 14px 35px rgba(0, 0, 0, 0.4);
}
body.dark-mode .parametres-main-card,
[data-theme="dark"] .parametres-main-card {
background: #151f30;
border-color: #243247;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
body.dark-mode .parametres-tabs,
[data-theme="dark"] .parametres-tabs {
background: #0e1726;
border-color: #243247;
}
body.dark-mode .parametres-tab-btn,
[data-theme="dark"] .parametres-tab-btn {
color: #94a3b8;
}
body.dark-mode .parametres-tab-btn:hover,
[data-theme="dark"] .parametres-tab-btn:hover {
color: #f8fafc;
background: rgba(255, 255, 255, 0.05);
}
body.dark-mode .parametres-tab-btn.active,
[data-theme="dark"] .parametres-tab-btn.active {
background: #0088ce;
color: #ffffff;
box-shadow: 0 4px 12px rgba(0, 136, 206, 0.4);
}
body.dark-mode .parametres-page .table-container,
[data-theme="dark"] .parametres-page .table-container {
background: #151f30;
border-color: #243247;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
body.dark-mode .parametres-page .wcs-table th,
[data-theme="dark"] .parametres-page .wcs-table th {
background: #0e1726;
color: #94a3b8;
}
body.dark-mode .parametres-page .wcs-table td,
[data-theme="dark"] .parametres-page .wcs-table td {
border-bottom-color: #243247;
color: #f8fafc;
}
body.dark-mode .parametres-page .wcs-table tr:hover,
[data-theme="dark"] .parametres-page .wcs-table tr:hover {
background: #1a2a42;
}
body.dark-mode .parametres-page .form-group label,
body.dark-mode .parametres-page label,
[data-theme="dark"] .parametres-page .form-group label,
[data-theme="dark"] .parametres-page label {
color: #94a3b8;
}
body.dark-mode .parametres-page input,
body.dark-mode .parametres-page select,
body.dark-mode .parametres-page textarea,
[data-theme="dark"] .parametres-page input,
[data-theme="dark"] .parametres-page select,
[data-theme="dark"] .parametres-page textarea {
background: #0b111e;
border-color: #243247 !important;
color: #f8fafc;
}
body.dark-mode .parametres-page input:focus,
body.dark-mode .parametres-page select:focus,
body.dark-mode .parametres-page textarea:focus,
[data-theme="dark"] .parametres-page input:focus,
[data-theme="dark"] .parametres-page select:focus,
[data-theme="dark"] .parametres-page textarea:focus {
border-color: #38bdf8 !important;
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2);
}
body.dark-mode .parametres-page .modal-content,
[data-theme="dark"] .parametres-page .modal-content {
background: #151f30;
border-color: #243247;
box-shadow: 0 20px 42px rgba(0, 0, 0, 0.6);
color: #f8fafc;
}
body.dark-mode .parametres-page .modal-header,
[data-theme="dark"] .parametres-page .modal-header {
background: #0e1726;
border-bottom-color: #243247;
}
body.dark-mode .parametres-page .modal-footer,
[data-theme="dark"] .parametres-page .modal-footer {
background: #0e1726;
border-top-color: #243247;
}
body.dark-mode .param-logo-bg-btn,
[data-theme="dark"] .param-logo-bg-btn {
background: #0b111e;
border-color: #243247;
color: #94a3b8;
}
body.dark-mode .param-logo-bg-btn.active,
[data-theme="dark"] .param-logo-bg-btn.active {
background: #0088ce;
border-color: #0088ce;
color: #ffffff;
}
+113 -28
View File
@@ -146,7 +146,7 @@ export default function CreerPerturbation() {
const handleSelectSingleHoraire = useCallback((h) => { const handleSelectSingleHoraire = useCallback((h) => {
setSelectedHoraires([h]); setSelectedHoraires([h]);
setSearchQuery(`${h.numero_train} - ${h.gare_depart_nom} → ${h.gare_arrivee_nom}`); setSearchQuery(`${h.numero_train} - ${h.gare_depart_nom} vers ${h.gare_arrivee_nom}`);
setShowResults(false); setShowResults(false);
setPerturbationsArretsMap({ [h.id]: buildArretFromHoraire(h) }); setPerturbationsArretsMap({ [h.id]: buildArretFromHoraire(h) });
setOpenTrains({ [h.id]: true }); setOpenTrains({ [h.id]: true });
@@ -184,13 +184,33 @@ export default function CreerPerturbation() {
if (checked) { if (checked) {
if (!types.includes(typeValue)) types.push(typeValue); if (!types.includes(typeValue)) types.push(typeValue);
if (typeValue === 'retard') { a.manual_retard = true; a.retard_minutes = a.retard_minutes || 0; } if (typeValue === 'retard') {
if (typeValue === 'arret_prolonge') { a.manual_arret_prolonge = true; a.arret_prolonge_minutes = a.arret_prolonge_minutes || 0; } a.manual_retard = true;
a.retard_minutes = Number(a.retard_minutes) > 0 ? a.retard_minutes : 15;
}
if (typeValue === 'reduction_retard') {
a.manual_reduction = true;
a.reduction_retard_minutes = Number(a.reduction_retard_minutes) > 0 ? a.reduction_retard_minutes : 5;
}
if (typeValue === 'arret_prolonge') {
a.manual_arret_prolonge = true;
a.arret_prolonge_minutes = Number(a.arret_prolonge_minutes) > 0 ? a.arret_prolonge_minutes : 5;
}
if (typeValue === 'modification_parcours') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; } if (typeValue === 'modification_parcours') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; }
} else { } else {
types = types.filter(t => t !== typeValue); types = types.filter(t => t !== typeValue);
if (typeValue === 'retard') { a.manual_retard = false; a.retard_minutes = 0; } if (typeValue === 'retard') {
if (typeValue === 'arret_prolonge') { a.manual_arret_prolonge = false; a.arret_prolonge_minutes = 0; } a.manual_retard = false;
a.retard_minutes = 0;
}
if (typeValue === 'reduction_retard') {
a.manual_reduction = false;
a.reduction_retard_minutes = 0;
}
if (typeValue === 'arret_prolonge') {
a.manual_arret_prolonge = false;
a.arret_prolonge_minutes = 0;
}
if (typeValue === 'arret_supprime') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; } if (typeValue === 'arret_supprime') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; }
if (typeValue === 'modification_parcours') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; } if (typeValue === 'modification_parcours') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; }
} }
@@ -209,10 +229,24 @@ export default function CreerPerturbation() {
nextArr[index] = { ...nextArr[index], [field]: value }; nextArr[index] = { ...nextArr[index], [field]: value };
if (field === 'retard_minutes') { if (field === 'retard_minutes') {
nextArr[index].manual_retard = true; const mins = Number(value) || 0;
nextArr[index].retard_minutes = Number(value) || 0; nextArr[index].retard_minutes = mins;
nextArr[index].manual_retard = mins > 0;
const types = [...(nextArr[index].types_perturbation || [])]; const types = [...(nextArr[index].types_perturbation || [])];
if (!types.includes('retard')) types.push('retard'); if (mins > 0 && !types.includes('retard')) types.push('retard');
nextArr[index].types_perturbation = types;
nextArr[index].type_perturbation = getPrimaryType(types);
}
if (field === 'reduction_retard_minutes') {
const mins = Number(value) || 0;
nextArr[index].reduction_retard_minutes = mins;
nextArr[index].manual_reduction = mins > 0;
const types = [...(nextArr[index].types_perturbation || [])];
if (mins > 0 && !types.includes('reduction_retard')) types.push('reduction_retard');
else if (mins === 0) {
const idx = types.indexOf('reduction_retard');
if (idx !== -1) types.splice(idx, 1);
}
nextArr[index].types_perturbation = types; nextArr[index].types_perturbation = types;
nextArr[index].type_perturbation = getPrimaryType(types); nextArr[index].type_perturbation = getPrimaryType(types);
} }
@@ -538,8 +572,10 @@ export default function CreerPerturbation() {
</div> </div>
<div className="horaire-display-content" style={{ maxHeight: '200px', overflowY: 'auto' }}> <div className="horaire-display-content" style={{ maxHeight: '200px', overflowY: 'auto' }}>
{selectedHoraires.map(h => ( {selectedHoraires.map(h => (
<div key={h.id} style={{ padding: '8px 0', borderBottom: '1px solid #eee' }}> <div key={h.id} style={{ padding: '8px 0', borderBottom: '1px solid #eee', display: 'flex', alignItems: 'center', gap: '6px' }}>
<strong>Train {h.numero_train}</strong> ({h.ligne_nom || 'N/A'}) - {h.gare_depart_nom} → {h.gare_arrivee_nom} <strong>Train {h.numero_train}</strong> ({h.ligne_nom || 'N/A'}) - {h.gare_depart_nom}
<wcs-mat-icon icon="arrow_forward" style={{ fontSize: '13px', margin: '0 4px', color: '#94a3b8' }}></wcs-mat-icon>
{h.gare_arrivee_nom}
</div> </div>
))} ))}
</div> </div>
@@ -573,8 +609,8 @@ export default function CreerPerturbation() {
style={{ padding: '12px 16px', background: '#e9ecef', cursor: 'pointer', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }} style={{ padding: '12px 16px', background: '#e9ecef', cursor: 'pointer', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
onClick={() => toggleTrainAccordion(h.id)} onClick={() => toggleTrainAccordion(h.id)}
> >
<h4 style={{ margin: 0, fontSize: '1rem', color: '#333' }}> <h4 style={{ margin: 0, fontSize: '1rem', color: '#333', display: 'flex', alignItems: 'center', gap: '6px' }}>
Train {h.numero_train} ({h.gare_depart_nom} → {h.gare_arrivee_nom}) Train {h.numero_train} ({h.gare_depart_nom} <wcs-mat-icon icon="arrow_forward" style={{ fontSize: '13px' }}></wcs-mat-icon> {h.gare_arrivee_nom})
</h4> </h4>
<wcs-mat-icon icon={openTrains[h.id] ? 'expand_less' : 'expand_more'} style={{ color: '#555' }} /> <wcs-mat-icon icon={openTrains[h.id] ? 'expand_less' : 'expand_more'} style={{ color: '#555' }} />
</div> </div>
@@ -677,8 +713,10 @@ export default function CreerPerturbation() {
Départ : {formatHeure(h.heure_depart)} Départ : {formatHeure(h.heure_depart)}
</span> </span>
</div> </div>
<div> <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
{h.gare_depart_nom} → {h.gare_arrivee_nom} <span>{h.gare_depart_nom}</span>
<wcs-mat-icon icon="arrow_forward" style={{ fontSize: '13px', margin: '0 4px', color: '#94a3b8' }}></wcs-mat-icon>
<span>{h.gare_arrivee_nom}</span>
</div> </div>
<div style={{ fontSize: '0.9rem', color: '#4b5563' }}> <div style={{ fontSize: '0.9rem', color: '#4b5563' }}>
Matériel actuel : {h.composition_train?.map(c => `${c.materiel_nom}`).join(', ') || 'Aucun'} Matériel actuel : {h.composition_train?.map(c => `${c.materiel_nom}`).join(', ') || 'Aucun'}
@@ -717,7 +755,7 @@ export default function CreerPerturbation() {
<select id="sillon" value={sillonSubstitutionId} onChange={e => setSillonSubstitutionId(e.target.value)} className="form-select"> <select id="sillon" value={sillonSubstitutionId} onChange={e => setSillonSubstitutionId(e.target.value)} className="form-select">
<option value="">Aucun</option> <option value="">Aucun</option>
{horaires.filter(h => h.id !== selectedHoraires[0].id).slice(0, 50).map(h => ( {horaires.filter(h => h.id !== selectedHoraires[0].id).slice(0, 50).map(h => (
<option key={h.id} value={h.id}>{h.numero_train} - {h.gare_depart_nom} → {h.gare_arrivee_nom}</option> <option key={h.id} value={h.id}>{h.numero_train} - {h.gare_depart_nom} vers {h.gare_arrivee_nom}</option>
))} ))}
</select> </select>
</div> </div>
@@ -762,29 +800,76 @@ export default function CreerPerturbation() {
// ── Rendu principal ─────────────────────────────────────────────────────── // ── Rendu principal ───────────────────────────────────────────────────────
return ( return (
<div className="sncf-groupe"> <div className="sncf-groupe perturbations-page-root">
<div className="panel-layout"> <div className="panel-layout">
<div className="panel-nav"><NavPanel /></div> <div className="panel-nav"><NavPanel /></div>
<div className="panel-content"> <div className="panel-content perturbations-modern-container">
<header className="page-header"> {/* Header Banner */}
<h1>Créer une perturbation</h1> <header className="page-header-modern">
<p className="page-description"> <div className="header-info">
Étape {currentStep === 'horaire' ? '1' : '2'} / 2 —{' '} <div className="perturbations-hero-tags" style={{ marginBottom: '8px' }}>
{currentStep === 'horaire' ? 'Sélection des horaires' : 'Configuration'} <span className="perturbations-kicker-pill">
</p> <wcs-mat-icon icon="add_alert"></wcs-mat-icon>
Déclaration d'incident
</span>
{scopedRegionId && (
<span className="perturbations-region-badge">
<wcs-mat-icon icon="place"></wcs-mat-icon>
Région : {scopedRegionId}
</span>
)}
</div>
<h1>Déclarer une perturbation sur les circulations</h1>
<p>Signalez un retard, une suppression d'arrêt ou une modification de parcours en temps réel.</p>
</div>
<div className="header-actions">
<wcs-button className="wcs-secondary" mode="stroked" onClick={handleCancel}>
<wcs-mat-icon icon="close" style={{ marginRight: '6px' }}></wcs-mat-icon>
Annuler
</wcs-button>
</div>
</header> </header>
<div className="creation-container">
<div className="creation-main"> {/* Stepper */}
<div className="stepper-horizontal">
<div
className={`step-item ${currentStep === 'horaire' ? 'active' : 'completed'}`}
onClick={() => setCurrentStep('horaire')}
style={{ cursor: 'pointer' }}
>
<div className="step-icon">
{currentStep === 'perturbation' ? <wcs-mat-icon icon="check" style={{ fontSize: '14px' }}></wcs-mat-icon> : '1'}
</div>
<span>1. Sélection des trains</span>
</div>
<div className="step-divider"></div>
<div
className={`step-item ${currentStep === 'perturbation' ? 'active' : ''}`}
onClick={() => selectedHoraires.length > 0 && setCurrentStep('perturbation')}
style={{ cursor: selectedHoraires.length > 0 ? 'pointer' : 'not-allowed' }}
>
<div className="step-icon">2</div>
<span>2. Configuration de l'incident & Parcours</span>
</div>
</div>
<div className="creation-layout-modern">
<div className="creation-main-content">
{currentStep === 'horaire' && renderStepHoraire()} {currentStep === 'horaire' && renderStepHoraire()}
{currentStep === 'perturbation' && renderStepPerturbation()} {currentStep === 'perturbation' && renderStepPerturbation()}
</div> </div>
<div className="creation-preview">
<div className="sticky-sidebar">
{selectedHoraires.length === 1 && ( {selectedHoraires.length === 1 && (
<ParcoursPreview arrets={perturbationsArretsMap[selectedHoraires[0].id]} /> <ParcoursPreview arrets={perturbationsArretsMap[selectedHoraires[0].id]} />
)} )}
{selectedHoraires.length > 1 && ( {selectedHoraires.length > 1 && (
<div style={{ padding: '15px', color: '#666', fontStyle: 'italic', background: '#f9f9f9', borderRadius: '8px', border: '1px solid #ddd' }}> <div className="summary-card">
Aperçu du parcours non disponible lors de la sélection multiple. Veuillez vous référer aux accordéons détaillés. <h3>Sélection multiple ({selectedHoraires.length} trains)</h3>
<p style={{ fontSize: '13px', color: '#64748b', margin: 0 }}>
Configurez le motif commun et ajustez les arrêts de chaque train via les accordéons.
</p>
</div> </div>
)} )}
</div> </div>
+102 -26
View File
@@ -106,12 +106,16 @@ export default function ModifierPerturbation() {
// Arrêts // Arrêts
if (arretsData.length > 0) { if (arretsData.length > 0) {
setPerturbationsArrets(arretsData.map(a => ({ const mapped = arretsData.map(a => ({
...a, ...a,
manual_retard: a.manual_retard ?? (Number(a.retard_ajoute || a.retard_minutes) > 0),
manual_reduction: a.manual_reduction ?? (Number(a.reduction_retard_minutes) > 0),
reduction_retard_minutes: Number(a.reduction_retard_minutes) || 0,
types_perturbation: Array.isArray(a.types_perturbation) types_perturbation: Array.isArray(a.types_perturbation)
? a.types_perturbation ? a.types_perturbation
: (a.type_perturbation ? [a.type_perturbation] : []) : (a.type_perturbation ? [a.type_perturbation] : [])
}))); }));
setPerturbationsArrets(recomputePropagation(mapped));
} else { } else {
setPerturbationsArrets(buildArretFromHoraire(horaire)); setPerturbationsArrets(buildArretFromHoraire(horaire));
} }
@@ -148,12 +152,23 @@ export default function ModifierPerturbation() {
let types = [...(a.types_perturbation || [])]; let types = [...(a.types_perturbation || [])];
if (checked) { if (checked) {
if (!types.includes(typeValue)) types.push(typeValue); if (!types.includes(typeValue)) types.push(typeValue);
if (typeValue === 'retard') { a.manual_retard = true; a.retard_minutes = a.retard_minutes || 0; } if (typeValue === 'retard') {
if (typeValue === 'arret_prolonge') { a.manual_arret_prolonge = true; a.arret_prolonge_minutes = a.arret_prolonge_minutes || 0; } a.manual_retard = true;
a.retard_minutes = Number(a.retard_minutes) > 0 ? a.retard_minutes : 15;
}
if (typeValue === 'reduction_retard') {
a.manual_reduction = true;
a.reduction_retard_minutes = Number(a.reduction_retard_minutes) > 0 ? a.reduction_retard_minutes : 5;
}
if (typeValue === 'arret_prolonge') {
a.manual_arret_prolonge = true;
a.arret_prolonge_minutes = Number(a.arret_prolonge_minutes) > 0 ? a.arret_prolonge_minutes : 5;
}
if (typeValue === 'modification_parcours') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; } if (typeValue === 'modification_parcours') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; }
} else { } else {
types = types.filter(t => t !== typeValue); types = types.filter(t => t !== typeValue);
if (typeValue === 'retard') { a.manual_retard = false; a.retard_minutes = 0; } if (typeValue === 'retard') { a.manual_retard = false; a.retard_minutes = 0; }
if (typeValue === 'reduction_retard') { a.manual_reduction = false; a.reduction_retard_minutes = 0; }
if (typeValue === 'arret_prolonge') { a.manual_arret_prolonge = false; a.arret_prolonge_minutes = 0; } if (typeValue === 'arret_prolonge') { a.manual_arret_prolonge = false; a.arret_prolonge_minutes = 0; }
if (typeValue === 'arret_supprime') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; } if (typeValue === 'arret_supprime') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; }
if (typeValue === 'modification_parcours') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; } if (typeValue === 'modification_parcours') { a.is_nouveau_terminus = false; a.is_nouvelle_origine = false; }
@@ -170,10 +185,24 @@ export default function ModifierPerturbation() {
const next = [...prev]; const next = [...prev];
next[index] = { ...next[index], [field]: value }; next[index] = { ...next[index], [field]: value };
if (field === 'retard_minutes') { if (field === 'retard_minutes') {
next[index].manual_retard = true; const mins = Number(value) || 0;
next[index].retard_minutes = Number(value) || 0; next[index].retard_minutes = mins;
next[index].manual_retard = mins > 0;
const types = [...(next[index].types_perturbation || [])]; const types = [...(next[index].types_perturbation || [])];
if (!types.includes('retard')) types.push('retard'); if (mins > 0 && !types.includes('retard')) types.push('retard');
next[index].types_perturbation = types;
next[index].type_perturbation = getPrimaryType(types);
}
if (field === 'reduction_retard_minutes') {
const mins = Number(value) || 0;
next[index].reduction_retard_minutes = mins;
next[index].manual_reduction = mins > 0;
const types = [...(next[index].types_perturbation || [])];
if (mins > 0 && !types.includes('reduction_retard')) types.push('reduction_retard');
else if (mins === 0) {
const idx = types.indexOf('reduction_retard');
if (idx !== -1) types.splice(idx, 1);
}
next[index].types_perturbation = types; next[index].types_perturbation = types;
next[index].type_perturbation = getPrimaryType(types); next[index].type_perturbation = getPrimaryType(types);
} }
@@ -512,8 +541,10 @@ export default function ModifierPerturbation() {
Départ : {formatHeure(h.heure_depart)} Départ : {formatHeure(h.heure_depart)}
</span> </span>
</div> </div>
<div> <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
{h.gare_depart_nom} → {h.gare_arrivee_nom} <span>{h.gare_depart_nom}</span>
<wcs-mat-icon icon="arrow_forward" style={{ fontSize: '13px', margin: '0 4px', color: '#94a3b8' }}></wcs-mat-icon>
<span>{h.gare_arrivee_nom}</span>
</div> </div>
<div style={{ fontSize: '0.9rem', color: '#4b5563' }}> <div style={{ fontSize: '0.9rem', color: '#4b5563' }}>
Matériel actuel : {h.composition_train?.map(c => `${c.materiel_nom}`).join(', ') || 'Aucun'} Matériel actuel : {h.composition_train?.map(c => `${c.materiel_nom}`).join(', ') || 'Aucun'}
@@ -552,7 +583,7 @@ export default function ModifierPerturbation() {
<select id="sillon" value={sillonSubstitutionId} onChange={e => setSillonSubstitutionId(e.target.value)} className="form-select"> <select id="sillon" value={sillonSubstitutionId} onChange={e => setSillonSubstitutionId(e.target.value)} className="form-select">
<option value="">Aucun</option> <option value="">Aucun</option>
{horaires.filter(h => h.id !== selectedHoraire?.id).slice(0, 50).map(h => ( {horaires.filter(h => h.id !== selectedHoraire?.id).slice(0, 50).map(h => (
<option key={h.id} value={h.id}>{h.numero_train} - {h.gare_depart_nom} → {h.gare_arrivee_nom}</option> <option key={h.id} value={h.id}>{h.numero_train} - {h.gare_depart_nom} vers {h.gare_arrivee_nom}</option>
))} ))}
</select> </select>
</div> </div>
@@ -597,21 +628,22 @@ export default function ModifierPerturbation() {
// ── États de chargement / erreur ────────────────────────────────────────── // ── États de chargement / erreur ──────────────────────────────────────────
if (loading) return ( if (loading) return (
<div className="sncf-groupe"> <div className="sncf-groupe perturbations-page-root">
<div className="panel-layout"> <div className="panel-layout">
<div className="panel-nav"><NavPanel /></div> <div className="panel-nav"><NavPanel /></div>
<div className="panel-content"> <div className="panel-content perturbations-modern-container" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '60vh' }}>
<div className="loading-state"><wcs-spinner mode="border" /><p>Chargement de la perturbation...</p></div> <wcs-spinner mode="border" style={{ width: '48px', height: '48px', color: '#0088ce' }}></wcs-spinner>
<p style={{ marginTop: '16px', color: '#64748b', fontWeight: 600 }}>Chargement de la perturbation...</p>
</div> </div>
</div> </div>
</div> </div>
); );
if (error || !selectedHoraire) return ( if (error || !selectedHoraire) return (
<div className="sncf-groupe"> <div className="sncf-groupe perturbations-page-root">
<div className="panel-layout"> <div className="panel-layout">
<div className="panel-nav"><NavPanel /></div> <div className="panel-nav"><NavPanel /></div>
<div className="panel-content"> <div className="panel-content perturbations-modern-container">
<wcs-message mode="danger"><wcs-mat-icon icon="error" />{error || 'Perturbation introuvable'}</wcs-message> <wcs-message mode="danger"><wcs-mat-icon icon="error" />{error || 'Perturbation introuvable'}</wcs-message>
<wcs-button className="wcs-secondary" onClick={() => (window.location.href = '/perturbations')}>Retour à la liste</wcs-button> <wcs-button className="wcs-secondary" onClick={() => (window.location.href = '/perturbations')}>Retour à la liste</wcs-button>
</div> </div>
@@ -621,23 +653,67 @@ export default function ModifierPerturbation() {
// ── Rendu principal ─────────────────────────────────────────────────────── // ── Rendu principal ───────────────────────────────────────────────────────
return ( return (
<div className="sncf-groupe"> <div className="sncf-groupe perturbations-page-root">
<div className="panel-layout"> <div className="panel-layout">
<div className="panel-nav"><NavPanel /></div> <div className="panel-nav"><NavPanel /></div>
<div className="panel-content"> <div className="panel-content perturbations-modern-container">
<header className="page-header"> {/* Header Banner */}
<h1>Modifier une perturbation</h1> <header className="page-header-modern">
<p className="page-description"> <div className="header-info">
Étape {currentStep === 'horaire' ? '1' : '2'} / 2 —{' '} <div className="perturbations-hero-tags" style={{ marginBottom: '8px' }}>
{currentStep === 'horaire' ? 'Horaire perturbé' : 'Configuration'} <span className="perturbations-kicker-pill">
</p> <wcs-mat-icon icon="edit"></wcs-mat-icon>
Modification d'incident
</span>
{selectedHoraire?.numero_train && (
<span className="perturbations-region-badge">
<wcs-mat-icon icon="train"></wcs-mat-icon>
Train : <strong>{selectedHoraire.numero_train}</strong>
</span>
)}
</div>
<h1>Modifier la perturbation</h1>
<p>Mettez à jour les retards, motifs, arrêts impactés et composition.</p>
</div>
<div className="header-actions">
<wcs-button className="wcs-secondary" mode="stroked" onClick={handleCancel}>
<wcs-mat-icon icon="close" style={{ marginRight: '6px' }}></wcs-mat-icon>
Annuler
</wcs-button>
</div>
</header> </header>
<div className="creation-container">
<div className="creation-main"> {/* Stepper */}
<div className="stepper-horizontal">
<div
className={`step-item ${currentStep === 'horaire' ? 'active' : 'completed'}`}
onClick={() => setCurrentStep('horaire')}
style={{ cursor: 'pointer' }}
>
<div className="step-icon">
{currentStep === 'perturbation' ? <wcs-mat-icon icon="check" style={{ fontSize: '14px' }}></wcs-mat-icon> : '1'}
</div>
<span>1. Train concerné</span>
</div>
<div className="step-divider"></div>
<div
className={`step-item ${currentStep === 'perturbation' ? 'active' : ''}`}
onClick={() => setCurrentStep('perturbation')}
style={{ cursor: 'pointer' }}
>
<div className="step-icon">2</div>
<span>2. Configuration des arrêts & propagation</span>
</div>
</div>
<div className="creation-layout-modern">
<div className="creation-main-content">
{currentStep === 'horaire' && renderStepHoraire()} {currentStep === 'horaire' && renderStepHoraire()}
{currentStep === 'perturbation' && renderStepPerturbation()} {currentStep === 'perturbation' && renderStepPerturbation()}
</div> </div>
<div className="creation-preview">
<div className="sticky-sidebar">
<ParcoursPreview arrets={perturbationsArrets} /> <ParcoursPreview arrets={perturbationsArrets} />
</div> </div>
</div> </div>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+163 -39
View File
@@ -27,7 +27,8 @@ export function doesHoraireCirculateOnDate(horaire, dateString) {
export const TYPES_PERTURBATION_ARRET = [ export const TYPES_PERTURBATION_ARRET = [
{ value: 'retard', label: 'Retard', color: '#f39c12', bg: '#fff8e6', icon: 'schedule' }, { value: 'retard', label: 'Retard (+)', color: '#f39c12', bg: '#fff8e6', icon: 'schedule' },
{ value: 'reduction_retard', label: 'Réduction / Rattrapage (-)', color: '#059669', bg: '#ecfdf5', icon: 'fast_forward' },
{ value: 'arret_supprime', label: 'Arrêt Supprimé', color: '#e74c3c', bg: '#fef5f5', icon: 'cancel' }, { value: 'arret_supprime', label: 'Arrêt Supprimé', color: '#e74c3c', bg: '#fef5f5', icon: 'cancel' },
{ value: 'modification_parcours',label: 'Modification de parcours', color: '#9b59b6', bg: '#f5f0ff', icon: 'alt_route' }, { value: 'modification_parcours',label: 'Modification de parcours', color: '#9b59b6', bg: '#f5f0ff', icon: 'alt_route' },
{ value: 'arret_prolonge', label: 'Arrêt Prolongé', color: '#3498db', bg: '#e8f4fd', icon: 'timelapse' }, { value: 'arret_prolonge', label: 'Arrêt Prolongé', color: '#3498db', bg: '#e8f4fd', icon: 'timelapse' },
@@ -59,6 +60,7 @@ export function getPrimaryType(types) {
if (types.includes('modification_parcours')) return 'modification_parcours'; if (types.includes('modification_parcours')) return 'modification_parcours';
if (types.includes('arret_supprime')) return 'arret_supprime'; if (types.includes('arret_supprime')) return 'arret_supprime';
if (types.includes('arret_prolonge')) return 'arret_prolonge'; if (types.includes('arret_prolonge')) return 'arret_prolonge';
if (types.includes('reduction_retard')) return 'reduction_retard';
if (types.includes('retard')) return 'retard'; if (types.includes('retard')) return 'retard';
return types[0]; return types[0];
} }
@@ -71,12 +73,15 @@ export function buildArretFromHoraire(horaire) {
types_perturbation: [], types_perturbation: [],
type_perturbation: '', type_perturbation: '',
retard_minutes: 0, retard_minutes: 0,
reduction_retard_minutes: 0,
retard_cumule: 0, retard_cumule: 0,
retard_entrant: 0,
arret_prolonge_minutes: 0, arret_prolonge_minutes: 0,
cause_arret: '', cause_arret: '',
is_nouveau_terminus: false, is_nouveau_terminus: false,
is_nouvelle_origine: false, is_nouvelle_origine: false,
manual_retard: false, manual_retard: false,
manual_reduction: false,
manual_arret_prolonge: false, manual_arret_prolonge: false,
}; };
@@ -94,25 +99,72 @@ export function recomputePropagation(arrets) {
if (!arrets || arrets.length === 0) return arrets; if (!arrets || arrets.length === 0) return arrets;
let cumulativeDelay = 0; let cumulativeDelay = 0;
let hadDelayEarlier = false;
return arrets.map((arret) => { return arrets.map((arret, idx) => {
const newArret = { ...arret }; const newArret = { ...arret, index: idx };
const isSupprime = (newArret.types_perturbation || []).includes('arret_supprime');
const currentDelay = Number(newArret.retard_minutes) || 0; const manualRetard = Number(newArret.retard_minutes) || 0;
const reductionRetard = Number(newArret.reduction_retard_minutes) || 0;
const prolongedStopTime = Number(newArret.arret_prolonge_minutes) || 0; const prolongedStopTime = Number(newArret.arret_prolonge_minutes) || 0;
cumulativeDelay += currentDelay + prolongedStopTime; // Enregistrer le retard entrant à cet arrêt
newArret.retard_cumule = cumulativeDelay; const delayBeforeStop = cumulativeDelay;
if (delayBeforeStop > 0 || manualRetard > 0 || prolongedStopTime > 0) {
const types = Array.isArray(newArret.types_perturbation) ? [...newArret.types_perturbation] : []; hadDelayEarlier = true;
if (currentDelay > 0 && !types.includes('retard')) {
types.push('retard');
} }
if (prolongedStopTime > 0 && !types.includes('arret_prolonge')) { // Ajouter les retards déclarés spécifiquement à cet arrêt
if (newArret.manual_retard) {
cumulativeDelay += manualRetard;
}
if (newArret.manual_arret_prolonge) {
cumulativeDelay += prolongedStopTime;
}
// Appliquer la réduction / rattrapage de retard
if (reductionRetard > 0) {
cumulativeDelay = Math.max(0, cumulativeDelay - reductionRetard);
}
newArret.retard_cumule = cumulativeDelay;
newArret.retard_entrant = delayBeforeStop;
// Retard rattrapé : il y avait un retard et le train est maintenant remis à l'heure
newArret.retard_rattrape = hadDelayEarlier && cumulativeDelay === 0 && (delayBeforeStop > 0 || reductionRetard > 0);
let types = Array.isArray(newArret.types_perturbation)
? [...newArret.types_perturbation]
: (newArret.type_perturbation ? [newArret.type_perturbation] : []);
if (!isSupprime) {
if (cumulativeDelay > 0) {
if (!types.includes('retard')) {
types.push('retard');
}
newArret.is_propagated_delay = !newArret.manual_retard;
} else {
if (!newArret.manual_retard) {
types = types.filter(t => t !== 'retard');
newArret.is_propagated_delay = false;
}
}
if (reductionRetard > 0) {
if (!types.includes('reduction_retard')) {
types.push('reduction_retard');
}
} else if (!newArret.manual_reduction) {
types = types.filter(t => t !== 'reduction_retard');
}
if (prolongedStopTime > 0) {
if (!types.includes('arret_prolonge')) {
types.push('arret_prolonge'); types.push('arret_prolonge');
} }
} else if (!newArret.manual_arret_prolonge) {
types = types.filter(t => t !== 'arret_prolonge');
}
}
newArret.types_perturbation = types; newArret.types_perturbation = types;
newArret.type_perturbation = getPrimaryType(types); newArret.type_perturbation = getPrimaryType(types);
@@ -123,7 +175,7 @@ export function recomputePropagation(arrets) {
/** Construit le payload API et les modifications de parcours depuis l'état du formulaire */ /** Construit le payload API et les modifications de parcours depuis l'état du formulaire */
export function buildPayload({ selectedHoraire, perturbationsArrets, cause, causeDetails, sillonSubstitutionId, joursPerturbation, actif, notes }) { export function buildPayload({ selectedHoraire, perturbationsArrets, cause, causeDetails, sillonSubstitutionId, joursPerturbation, actif, notes }) {
const hasRetard = perturbationsArrets.some(a => (a.types_perturbation || []).some(t => ['retard', 'arret_prolonge'].includes(t))); const hasRetard = perturbationsArrets.some(a => (a.types_perturbation || []).some(t => ['retard', 'arret_prolonge', 'reduction_retard'].includes(t)) || (a.retard_cumule > 0));
const hasSuppression = perturbationsArrets.some(a => (a.types_perturbation || []).includes('arret_supprime')); const hasSuppression = perturbationsArrets.some(a => (a.types_perturbation || []).includes('arret_supprime'));
const hasModif = perturbationsArrets.some(a => (a.types_perturbation || []).includes('modification_parcours')); const hasModif = perturbationsArrets.some(a => (a.types_perturbation || []).includes('modification_parcours'));
@@ -131,24 +183,23 @@ export function buildPayload({ selectedHoraire, perturbationsArrets, cause, caus
if (hasModif) typePerturbation = 'modification_parcours'; if (hasModif) typePerturbation = 'modification_parcours';
else if (hasSuppression && !hasRetard) typePerturbation = 'suppression'; else if (hasSuppression && !hasRetard) typePerturbation = 'suppression';
const retardTotal = perturbationsArrets.reduce((acc, a) => { const maxDelay = Math.max(0, ...perturbationsArrets.map(a => Number(a.retard_cumule) || 0));
let add = 0;
if (a.manual_retard) add += Number(a.retard_minutes) || 0;
if (a.manual_arret_prolonge) add += Number(a.arret_prolonge_minutes) || 0;
return acc + add;
}, 0);
const modificationsParcours = perturbationsArrets const modificationsParcours = perturbationsArrets
.filter(a => (a.types_perturbation || []).length > 0) .filter(a => (a.types_perturbation || []).length > 0 || (a.retard_cumule > 0 && !((a.types_perturbation || []).includes('arret_supprime'))))
.map(a => ({ .map(a => ({
gare_nom: a.gare_nom, gare_nom: a.gare_nom,
type_modification: getPrimaryType(a.types_perturbation), type_modification: getPrimaryType(a.types_perturbation) || (a.retard_cumule > 0 ? 'retard' : ''),
types_perturbation: a.types_perturbation, types_perturbation: a.types_perturbation?.length > 0 ? a.types_perturbation : (a.retard_cumule > 0 ? ['retard'] : []),
retard_minutes: a.retard_minutes || null, retard_minutes: a.retard_cumule > 0 ? a.retard_cumule : (a.retard_minutes || null),
retard_ajoute: a.manual_retard ? (a.retard_minutes || 0) : 0,
reduction_retard_minutes: a.reduction_retard_minutes || null,
retard_cumule: a.retard_cumule || null,
arret_prolonge_minutes: a.arret_prolonge_minutes || null, arret_prolonge_minutes: a.arret_prolonge_minutes || null,
cause: a.cause_arret, cause: a.cause_arret || cause,
is_nouveau_terminus: a.is_nouveau_terminus, is_nouveau_terminus: a.is_nouveau_terminus,
is_nouvelle_origine: a.is_nouvelle_origine, is_nouvelle_origine: a.is_nouvelle_origine,
is_propagated_delay: !!a.is_propagated_delay,
})); }));
const allDates = (joursPerturbation || []).flatMap(j => const allDates = (joursPerturbation || []).flatMap(j =>
@@ -161,7 +212,7 @@ export function buildPayload({ selectedHoraire, perturbationsArrets, cause, caus
const payload = { const payload = {
...(selectedHoraire ? { horaire_id: selectedHoraire.id } : {}), ...(selectedHoraire ? { horaire_id: selectedHoraire.id } : {}),
type_perturbation: typePerturbation, type_perturbation: typePerturbation,
duree_retard_minutes: retardTotal > 0 ? retardTotal : null, duree_retard_minutes: maxDelay > 0 ? maxDelay : null,
cause, cause,
cause_details: causeDetails || null, cause_details: causeDetails || null,
sillon_substitution_id: sillonSubstitutionId || null, sillon_substitution_id: sillonSubstitutionId || null,
@@ -200,18 +251,19 @@ export function ArretCard({ arret, index, isOpen, onToggle, onToggleType, onArre
? arret.types_perturbation ? arret.types_perturbation
: (arret.type_perturbation ? [arret.type_perturbation] : []); : (arret.type_perturbation ? [arret.type_perturbation] : []);
const primaryType = getPrimaryType(types); const primaryType = getPrimaryType(types);
const hasPerturbation = types.length > 0; const hasPerturbation = types.length > 0 || (arret.retard_cumule > 0) || (arret.reduction_retard_minutes > 0) || arret.retard_rattrape;
const isSupp = types.includes('arret_supprime'); const isSupp = types.includes('arret_supprime');
const borderColor = primaryType === 'retard' ? '#f39c12' const borderColor = primaryType === 'retard' ? '#f39c12'
: primaryType === 'reduction_retard' ? '#059669'
: primaryType === 'arret_supprime' ? '#e74c3c' : primaryType === 'arret_supprime' ? '#e74c3c'
: primaryType === 'arret_prolonge' ? '#3498db' : primaryType === 'arret_prolonge' ? '#3498db'
: primaryType === 'modification_parcours'? '#9b59b6' : primaryType === 'modification_parcours'? '#9b59b6'
: '#ddd'; : (arret.retard_cumule > 0 ? '#f39c12' : '#ddd');
const inputStyle = (border) => ({ const inputStyle = (border) => ({
padding: '0.6rem 0.75rem', fontSize: '1rem', padding: '0.6rem 0.75rem', fontSize: '0.95rem',
border: `1px solid ${border}`, borderRadius: '4px', background: 'white', border: `1px solid ${border}`, borderRadius: '6px', background: 'white',
}); });
return ( return (
@@ -251,12 +303,27 @@ export function ArretCard({ arret, index, isOpen, onToggle, onToggleType, onArre
<div style={{ display: 'flex', gap: '0.75rem', fontSize: '0.85rem', color: '#666', marginTop: '0.2rem' }}> <div style={{ display: 'flex', gap: '0.75rem', fontSize: '0.85rem', color: '#666', marginTop: '0.2rem' }}>
{arret.heure_arrivee && <span>Arr: {formatHeure(arret.heure_arrivee)}</span>} {arret.heure_arrivee && <span>Arr: {formatHeure(arret.heure_arrivee)}</span>}
{arret.heure_depart && <span>Dép: {formatHeure(arret.heure_depart)}</span>} {arret.heure_depart && <span>Dép: {formatHeure(arret.heure_depart)}</span>}
{tempsArret !== null && <span style={{ color: '#0088ce' }}>⏱ {tempsArret} min</span>} {tempsArret !== null && <span style={{ color: '#0088ce' }}><wcs-mat-icon icon="schedule" style={{ fontSize: '13px', verticalAlign: 'middle', marginRight: '2px' }}></wcs-mat-icon>{tempsArret} min</span>}
</div> </div>
</div> </div>
{hasPerturbation && ( {hasPerturbation && (
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{types.includes('retard') && <wcs-badge className="wcs-warning">Retard +{arret.retard_minutes} min</wcs-badge>} {(types.includes('retard') || arret.retard_cumule > 0) && !isSupp && (
<wcs-badge className="wcs-warning">
{arret.manual_retard
? `Retard +${arret.retard_minutes} min${arret.retard_cumule && arret.retard_cumule !== arret.retard_minutes ? ` (cumul: +${arret.retard_cumule} min)` : ''}`
: `Retard +${arret.retard_cumule} min (propagé)`}
</wcs-badge>
)}
{arret.reduction_retard_minutes > 0 && !isSupp && (
<wcs-badge className="wcs-success">Rattrapage -{arret.reduction_retard_minutes} min</wcs-badge>
)}
{arret.retard_rattrape && !isSupp && (
<wcs-badge className="wcs-info" style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
<wcs-mat-icon icon="info" style={{ fontSize: '13px' }}></wcs-mat-icon>
Arrivée à l'heure
</wcs-badge>
)}
{types.includes('arret_supprime') && <wcs-badge className="wcs-danger">Supprimé</wcs-badge>} {types.includes('arret_supprime') && <wcs-badge className="wcs-danger">Supprimé</wcs-badge>}
{types.includes('arret_prolonge') && <wcs-badge className="wcs-info">Prolongé +{arret.arret_prolonge_minutes} min</wcs-badge>} {types.includes('arret_prolonge') && <wcs-badge className="wcs-info">Prolongé +{arret.arret_prolonge_minutes} min</wcs-badge>}
{types.includes('modification_parcours') && ( {types.includes('modification_parcours') && (
@@ -280,7 +347,7 @@ export function ArretCard({ arret, index, isOpen, onToggle, onToggleType, onArre
['Heure d\'arrivée', arret.heure_arrivee ? formatHeure(arret.heure_arrivee) : '—', 'heure_arrivee'], ['Heure d\'arrivée', arret.heure_arrivee ? formatHeure(arret.heure_arrivee) : '—', 'heure_arrivee'],
['Heure de départ', arret.heure_depart ? formatHeure(arret.heure_depart) : '—', 'heure_depart'], ['Heure de départ', arret.heure_depart ? formatHeure(arret.heure_depart) : '—', 'heure_depart'],
['Temps d\'arrêt', tempsArret !== null ? `${tempsArret} min` : '—', null], ['Temps d\'arrêt', tempsArret !== null ? `${tempsArret} min` : '—', null],
['Retard actuel', arret.retard_cumule > 0 ? `${arret.retard_cumule} min` : '—', null], ['Retard estimé à ce point', arret.retard_cumule > 0 ? `+${arret.retard_cumule} min` : (arret.retard_rattrape ? 'Arrivée à l\'heure (rattrapé)' : 'À l\'heure'), null],
].map(([lbl, val, field]) => ( ].map(([lbl, val, field]) => (
<div key={lbl}> <div key={lbl}>
<label style={{ display: 'block', fontSize: '0.72rem', color: '#888', marginBottom: '0.2rem' }}>{lbl}</label> <label style={{ display: 'block', fontSize: '0.72rem', color: '#888', marginBottom: '0.2rem' }}>{lbl}</label>
@@ -343,12 +410,18 @@ export function ArretCard({ arret, index, isOpen, onToggle, onToggleType, onArre
{/* Retard */} {/* Retard */}
{types.includes('retard') && ( {types.includes('retard') && (
<div style={{ marginTop: '1rem', padding: '1rem', background: '#fff8e6', borderRadius: '6px', border: '1px solid #f39c12' }}> <div style={{ marginTop: '1rem', padding: '1rem', background: '#fff8e6', borderRadius: '6px', border: '1px solid #f39c12' }}>
{arret.is_propagated_delay && (
<div style={{ marginBottom: '10px', padding: '6px 10px', background: '#fef3c7', borderRadius: '4px', fontSize: '0.85rem', color: '#92400e', display: 'flex', alignItems: 'center', gap: '6px' }}>
<wcs-mat-icon icon="info" style={{ fontSize: '15px' }}></wcs-mat-icon>
<span>Retard reporté depuis les arrêts précédents : <strong>+{arret.retard_cumule} min</strong>.</span>
</div>
)}
<label style={{ display: 'block', fontWeight: 500, marginBottom: '0.4rem', color: '#8a6d3b' }}> <label style={{ display: 'block', fontWeight: 500, marginBottom: '0.4rem', color: '#8a6d3b' }}>
Temps de retard (minutes) {arret.is_propagated_delay ? 'Retard supplémentaire à cet arrêt (minutes)' : 'Temps de retard (minutes)'}
</label> </label>
<input type="number" min="1" placeholder="Ex: 15" value={arret.retard_minutes || ''} <input type="number" min="0" placeholder={arret.is_propagated_delay ? "0 (retard propagé)" : "Ex: 15"} value={arret.retard_minutes || ''}
onChange={e => onArretChange(index, 'retard_minutes', parseInt(e.target.value, 10) || 0)} onChange={e => onArretChange(index, 'retard_minutes', parseInt(e.target.value, 10) || 0)}
style={{ ...inputStyle('#f39c12'), width: '150px' }} /> style={{ ...inputStyle('#f39c12'), width: '180px' }} />
<label style={{ display: 'block', fontWeight: 500, marginBottom: '0.4rem', color: '#8a6d3b', marginTop: '0.75rem' }}>Cause du retard</label> <label style={{ display: 'block', fontWeight: 500, marginBottom: '0.4rem', color: '#8a6d3b', marginTop: '0.75rem' }}>Cause du retard</label>
<input type="text" placeholder="Ex : Incident technique, travaux..." value={arret.cause_arret || ''} <input type="text" placeholder="Ex : Incident technique, travaux..." value={arret.cause_arret || ''}
onChange={e => onArretChange(index, 'cause_arret', e.target.value)} onChange={e => onArretChange(index, 'cause_arret', e.target.value)}
@@ -356,6 +429,41 @@ export function ArretCard({ arret, index, isOpen, onToggle, onToggleType, onArre
</div> </div>
)} )}
{/* Réduction / Rattrapage de retard */}
{types.includes('reduction_retard') && (
<div style={{ marginTop: '1rem', padding: '1rem', background: '#ecfdf5', borderRadius: '6px', border: '1px solid #10b981' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px', color: '#065f46', fontWeight: 600, fontSize: '0.92rem' }}>
<wcs-mat-icon icon="fast_forward" style={{ fontSize: '18px' }}></wcs-mat-icon>
<span>Rattrapage / Réduction du retard</span>
</div>
<p style={{ fontSize: '0.85rem', color: '#047857', margin: '0 0 10px' }}>
Indiquez le temps rattrapé sur ce tronçon ou arrêt pour diminuer le retard cumulé reporté.
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
<div>
<label style={{ display: 'block', fontWeight: 500, marginBottom: '0.4rem', color: '#065f46', fontSize: '0.85rem' }}>
Temps réduit / rattrapé (minutes)
</label>
<input
type="number"
min="1"
placeholder="Ex: 5"
value={arret.reduction_retard_minutes || ''}
onChange={e => onArretChange(index, 'reduction_retard_minutes', parseInt(e.target.value, 10) || 0)}
style={{ ...inputStyle('#10b981'), width: '150px' }}
/>
</div>
{arret.retard_entrant > 0 && (
<div style={{ padding: '8px 12px', background: '#ffffff', borderRadius: '6px', border: '1px solid #a7f3d0', fontSize: '0.85rem', color: '#065f46' }}>
Retard en entrée : <strong>+{arret.retard_entrant} min</strong>
<wcs-mat-icon icon="arrow_forward" style={{ fontSize: '12px', margin: '0 6px', verticalAlign: 'middle' }}></wcs-mat-icon>
Sortie : <strong>{arret.retard_cumule > 0 ? `+${arret.retard_cumule} min` : "À l'heure"}</strong>
</div>
)}
</div>
</div>
)}
{/* Arrêt prolongé */} {/* Arrêt prolongé */}
{types.includes('arret_prolonge') && ( {types.includes('arret_prolonge') && (
<div style={{ marginTop: '1rem', padding: '1rem', background: '#e8f4fd', borderRadius: '6px', border: '1px solid #3498db' }}> <div style={{ marginTop: '1rem', padding: '1rem', background: '#e8f4fd', borderRadius: '6px', border: '1px solid #3498db' }}>
@@ -401,8 +509,8 @@ export function ArretCard({ arret, index, isOpen, onToggle, onToggleType, onArre
)} )}
</div> </div>
{(arret.is_nouveau_terminus || arret.is_nouvelle_origine) && ( {(arret.is_nouveau_terminus || arret.is_nouvelle_origine) && (
<p style={{ fontSize: '0.83rem', color: '#27ae60', marginTop: '0.6rem', marginBottom: 0 }}> <p style={{ fontSize: '0.83rem', color: '#27ae60', marginTop: '0.6rem', marginBottom: 0, display: 'flex', alignItems: 'center', gap: '4px' }}>
✓ {arret.is_nouveau_terminus ? 'Terminus' : 'Origine'} désigné — les arrêts {arret.is_nouveau_terminus ? 'suivants' : 'précédents'} seront supprimés. <wcs-mat-icon icon="check" style={{ fontSize: '14px' }}></wcs-mat-icon> {arret.is_nouveau_terminus ? 'Terminus' : 'Origine'} désigné — les arrêts {arret.is_nouveau_terminus ? 'suivants' : 'précédents'} seront supprimés.
</p> </p>
)} )}
</div> </div>
@@ -505,8 +613,9 @@ export function ParcoursPreview({ arrets }) {
const types = Array.isArray(arret.types_perturbation) const types = Array.isArray(arret.types_perturbation)
? arret.types_perturbation ? arret.types_perturbation
: (arret.type_perturbation ? [arret.type_perturbation] : []); : (arret.type_perturbation ? [arret.type_perturbation] : []);
const hasPerturbation = types.length > 0;
const isSupp = types.includes('arret_supprime'); const isSupp = types.includes('arret_supprime');
const displayRetard = arret.retard_cumule || arret.retard_minutes || 0;
const hasPerturbation = types.length > 0 || (displayRetard > 0 && !isSupp) || arret.retard_rattrape;
const modClass = !hasPerturbation ? '' : isSupp ? 'supprimee' : 'modifiee'; const modClass = !hasPerturbation ? '' : isSupp ? 'supprimee' : 'modifiee';
const causeLabel = arret.cause_arret?.trim() || null; const causeLabel = arret.cause_arret?.trim() || null;
@@ -520,7 +629,22 @@ export function ParcoursPreview({ arrets }) {
</div> </div>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap', marginTop: '0.25rem' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap', marginTop: '0.25rem' }}>
{types.includes('retard') && <wcs-badge className="wcs-warning parcours-stop-badge">+{arret.retard_minutes} min</wcs-badge>} {displayRetard > 0 && !isSupp && (
<wcs-badge className="wcs-warning parcours-stop-badge">
+{displayRetard} min{arret.is_propagated_delay ? ' (propagé)' : ''}
</wcs-badge>
)}
{arret.reduction_retard_minutes > 0 && !isSupp && (
<wcs-badge className="wcs-success parcours-stop-badge">
-{arret.reduction_retard_minutes} min (rattrapé)
</wcs-badge>
)}
{arret.retard_rattrape && !isSupp && (
<wcs-badge className="wcs-info parcours-stop-badge" style={{ display: 'inline-flex', alignItems: 'center', gap: '3px' }}>
<wcs-mat-icon icon="info" style={{ fontSize: '11px' }}></wcs-mat-icon>
Arrivée à l'heure
</wcs-badge>
)}
{types.includes('arret_supprime') && <wcs-badge className="wcs-danger parcours-stop-badge">Supprimé</wcs-badge>} {types.includes('arret_supprime') && <wcs-badge className="wcs-danger parcours-stop-badge">Supprimé</wcs-badge>}
{types.includes('arret_prolonge') && <wcs-badge className="wcs-info parcours-stop-badge">+{arret.arret_prolonge_minutes} min</wcs-badge>} {types.includes('arret_prolonge') && <wcs-badge className="wcs-info parcours-stop-badge">+{arret.arret_prolonge_minutes} min</wcs-badge>}
{types.includes('modification_parcours') && ( {types.includes('modification_parcours') && (
+52
View File
@@ -643,3 +643,55 @@
overflow-x: auto; overflow-x: auto;
} }
} }
/* Dark Mode */
body.dark-mode .quais-sidebar,
[data-theme="dark"] .quais-sidebar {
background: #111a28;
border-color: #243247;
}
body.dark-mode .quais-sidebar-header,
[data-theme="dark"] .quais-sidebar-header {
background: #0e1726;
border-bottom-color: #243247;
}
body.dark-mode .quais-sidebar-header h2,
[data-theme="dark"] .quais-sidebar-header h2 {
color: #f8fafc;
}
body.dark-mode .quais-gare-nom,
[data-theme="dark"] .quais-gare-nom {
color: #f1f5f9;
}
body.dark-mode .quais-gare-item:hover,
[data-theme="dark"] .quais-gare-item:hover {
background: #1e293b;
}
body.dark-mode .quais-gare-item.selected,
[data-theme="dark"] .quais-gare-item.selected {
background: #0d2744;
}
body.dark-mode .quais-main,
body.dark-mode .quais-main-header,
body.dark-mode .quais-day-selector,
[data-theme="dark"] .quais-main,
[data-theme="dark"] .quais-main-header,
[data-theme="dark"] .quais-day-selector {
background: #151f30;
border-color: #243247;
color: #f1f5f9;
}
body.dark-mode .quais-horaire-card,
[data-theme="dark"] .quais-horaire-card {
background: #111a28;
border-color: #243247;
color: #f1f5f9;
}
+48 -9
View File
@@ -114,27 +114,66 @@
} }
/* Dark mode adjustments */ /* Dark mode adjustments */
body.dark-mode .maj-header { body.dark-mode .maj-content,
border-bottom-color: #333; [data-theme="dark"] .maj-content {
background: transparent;
} }
body.dark-mode .maj-title h3 { body.dark-mode .maj-card,
color: #e0e0e0; [data-theme="dark"] .maj-card {
background: #151f30;
border: 1px solid #243247;
color: #f8fafc;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
border-radius: 12px;
padding: 20px;
} }
body.dark-mode .alert-new-version { body.dark-mode .maj-header,
background: #451a03; [data-theme="dark"] .maj-header {
border-bottom-color: #243247;
}
body.dark-mode .maj-title h3,
[data-theme="dark"] .maj-title h3 {
color: #f8fafc;
}
body.dark-mode .maj-actions p,
[data-theme="dark"] .maj-actions p {
color: #94a3b8;
}
body.dark-mode .alert-new-version,
[data-theme="dark"] .alert-new-version {
background: rgba(245, 158, 11, 0.12);
border-color: #f59e0b; border-color: #f59e0b;
} }
body.dark-mode .alert-new-version strong { body.dark-mode .alert-new-version strong,
[data-theme="dark"] .alert-new-version strong {
color: #fde68a; color: #fde68a;
} }
body.dark-mode .alert-new-version p { body.dark-mode .alert-new-version p,
[data-theme="dark"] .alert-new-version p {
color: #fcd34d; color: #fcd34d;
} }
body.dark-mode .progress-text { body.dark-mode .progress-text,
[data-theme="dark"] .progress-text {
color: #38bdf8; color: #38bdf8;
} }
@media (max-width: 600px) {
.maj-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.alert-new-version {
flex-direction: column;
}
}
+136
View File
@@ -274,3 +274,139 @@
color: #12324e; color: #12324e;
margin-bottom: 10px; margin-bottom: 10px;
} }
/* ==========================================================================
DARK MODE
========================================================================== */
body.dark-mode .tarifs-page,
[data-theme="dark"] .tarifs-page {
background: #0b111e;
}
body.dark-mode .tarifs-page-header,
[data-theme="dark"] .tarifs-page-header {
background: linear-gradient(140deg, #151f30 0%, #1a2a42 60%, #203554 100%);
border: 1px solid #243247;
box-shadow: 0 14px 35px rgba(0, 0, 0, 0.4);
}
body.dark-mode .tarifs-main-card,
[data-theme="dark"] .tarifs-main-card {
background: #151f30;
border-color: #243247;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
body.dark-mode .tarif-card,
[data-theme="dark"] .tarif-card {
background: #151f30;
border-color: #243247;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
body.dark-mode .tarif-title h4,
[data-theme="dark"] .tarif-title h4 {
color: #f8fafc;
}
body.dark-mode .tarif-description,
[data-theme="dark"] .tarif-description {
color: #94a3b8;
}
body.dark-mode .meta-item,
[data-theme="dark"] .meta-item {
background: #0b111e;
border-color: #243247;
}
body.dark-mode .meta-label,
[data-theme="dark"] .meta-label {
color: #94a3b8;
}
body.dark-mode .meta-value,
[data-theme="dark"] .meta-value {
color: #f8fafc;
}
body.dark-mode .region-chip,
[data-theme="dark"] .region-chip {
background: rgba(0, 136, 206, 0.15);
color: #38bdf8;
border: 1px solid rgba(0, 136, 206, 0.3);
}
body.dark-mode .tarif-tab-button,
[data-theme="dark"] .tarif-tab-button {
background: #0b111e;
border-color: #243247;
color: #94a3b8;
}
body.dark-mode .tarif-tab-button.active,
[data-theme="dark"] .tarif-tab-button.active {
background: #0088ce;
border-color: #0088ce;
color: #ffffff;
}
body.dark-mode .tarif-details-card,
[data-theme="dark"] .tarif-details-card {
background: #0b111e;
border-color: #243247;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
body.dark-mode .tarif-details-section h4,
[data-theme="dark"] .tarif-details-section h4 {
color: #f8fafc;
}
body.dark-mode .empty-state-modern,
[data-theme="dark"] .empty-state-modern {
background: #0b111e;
border-color: #243247;
}
body.dark-mode .empty-state-modern h3,
[data-theme="dark"] .empty-state-modern h3 {
color: #f8fafc;
}
body.dark-mode .empty-state-modern p,
[data-theme="dark"] .empty-state-modern p {
color: #94a3b8;
}
body.dark-mode .tarifs-state-block,
[data-theme="dark"] .tarifs-state-block {
background: #151f30;
border-color: #243247;
}
body.dark-mode .tarif-add-button,
[data-theme="dark"] .tarif-add-button {
background: #151f30;
border-color: #243247;
color: #38bdf8;
}
body.dark-mode .tarif-add-button:hover,
[data-theme="dark"] .tarif-add-button:hover {
background: #1a2a42;
}
body.dark-mode .form-card input,
body.dark-mode .form-card textarea,
[data-theme="dark"] .form-card input,
[data-theme="dark"] .form-card textarea {
background: #0b111e;
border-color: #243247;
color: #f8fafc;
}
body.dark-mode .tarif-checkbox,
[data-theme="dark"] .tarif-checkbox {
color: #f8fafc;
}
+270 -81
View File
@@ -8,13 +8,13 @@ import './Travaux.css';
import RichTextEditor from '../gares/RichTextEditor'; import RichTextEditor from '../gares/RichTextEditor';
const JOURS_SEMAINE = [ const JOURS_SEMAINE = [
{ value: 'lundi', label: 'Lundi' }, { value: 'lundi', label: 'Lundi', short: 'Lun' },
{ value: 'mardi', label: 'Mardi' }, { value: 'mardi', label: 'Mardi', short: 'Mar' },
{ value: 'mercredi', label: 'Mercredi' }, { value: 'mercredi', label: 'Mercredi', short: 'Mer' },
{ value: 'jeudi', label: 'Jeudi' }, { value: 'jeudi', label: 'Jeudi', short: 'Jeu' },
{ value: 'vendredi', label: 'Vendredi' }, { value: 'vendredi', label: 'Vendredi', short: 'Ven' },
{ value: 'samedi', label: 'Samedi' }, { value: 'samedi', label: 'Samedi', short: 'Sam' },
{ value: 'dimanche', label: 'Dimanche' } { value: 'dimanche', label: 'Dimanche', short: 'Dim' }
]; ];
export default function CreerTravaux() { export default function CreerTravaux() {
@@ -25,9 +25,9 @@ export default function CreerTravaux() {
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const [dateDebut, setDateDebut] = useState(''); const [dateDebut, setDateDebut] = useState('');
const [dateFin, setDateFin] = useState(''); const [dateFin, setDateFin] = useState('');
const [joursTravaux, setJoursTravaux] = useState([]); const [joursTravaux, setJoursTravaux] = useState(['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche']);
const [heureDebut, setHeureDebut] = useState(''); const [heureDebut, setHeureDebut] = useState('22:00');
const [heureFin, setHeureFin] = useState(''); const [heureFin, setHeureFin] = useState('06:00');
const [actif, setActif] = useState(true); const [actif, setActif] = useState(true);
// Substitutions // Substitutions
@@ -96,21 +96,33 @@ export default function CreerTravaux() {
} }
}; };
const selectAllDays = () => {
setJoursTravaux(JOURS_SEMAINE.map(j => j.value));
};
const selectWeekdays = () => {
setJoursTravaux(['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi']);
};
const selectWeekend = () => {
setJoursTravaux(['samedi', 'dimanche']);
};
const formatHeure = (heure) => heure ? heure.substring(0, 5) : '—'; const formatHeure = (heure) => heure ? heure.substring(0, 5) : '—';
const navigate = (path) => { window.location.href = path; }; const navigate = (path) => { window.location.href = path; };
const handleCreate = async () => { const handleCreate = async () => {
if (!canCreateTravaux) { if (!canCreateTravaux) {
alert('Vous n\'avez pas les droits pour créer des travaux.'); alert("Vous n'avez pas les droits pour créer des travaux.");
return; return;
} }
if (!titre.trim() || !dateDebut || !dateFin || joursTravaux.length === 0 || !heureDebut || !heureFin) { if (!titre.trim() || !dateDebut || !dateFin || joursTravaux.length === 0 || !heureDebut || !heureFin) {
alert('Veuillez remplir tous les champs obligatoires'); alert('Veuillez remplir tous les champs obligatoires (*)');
return; return;
} }
if (new Date(dateFin) < new Date(dateDebut)) { if (new Date(dateFin) < new Date(dateDebut)) {
alert('La date de fin doit être postérieure à la date de début'); alert('La date de fin doit être postérieure ou égale à la date de début.');
return; return;
} }
@@ -134,45 +146,88 @@ export default function CreerTravaux() {
const { error } = await createTravaux(payload, substitutionsIds); const { error } = await createTravaux(payload, substitutionsIds);
if (error) { if (error) {
alert('Erreur lors de la création des travaux'); alert('Erreur lors de la création des travaux : ' + (error.message || 'Erreur'));
} else { } else {
navigate('/travaux'); navigate('/travaux');
} }
} catch (err) { } catch (err) {
alert('Une erreur inattendue s\'est produite'); alert("Une erreur inattendue s'est produite");
} finally { } finally {
setSaving(false); setSaving(false);
} }
}; };
return ( return (
<div className="sncf-groupe panel-layout"> <div className="sncf-groupe travaux-page-root">
<div className="panel-nav"><NavPanel /></div> <div className="panel-layout">
<div className="panel-content travaux-form-page-content"> <div className="panel-nav">
<NavPanel />
<header className="travaux-page-header" style={{ borderBottom: 'none', padding: '0 0 30px 0', background: 'transparent' }}>
<div>
<p className="travaux-page-kicker">Création</p>
<h1>Créer des travaux</h1>
<p className="travaux-page-description">Planifiez de nouveaux travaux et leurs substitutions.</p>
</div> </div>
<wcs-button className="wcs-secondary" onClick={() => navigate('/travaux')}>
<wcs-mat-icon icon="close"></wcs-mat-icon> Annuler <div className="panel-content travaux-form-page-content">
{/* Header Banner */}
<header className="travaux-form-header-modern">
<div className="form-header-main">
<button type="button" className="btn-back-link" onClick={() => navigate('/travaux')}>
<wcs-mat-icon icon="arrow_back"></wcs-mat-icon>
<span>Retour aux travaux</span>
</button>
<div className="form-title-group">
<span className="travaux-kicker-pill">
<wcs-mat-icon icon="add_circle"></wcs-mat-icon>
Nouveau Chantier
</span>
<h1>Déclarer des travaux sur le réseau</h1>
<p>Planifiez une coupure, des restrictions de circulation et les substitutions associées.</p>
</div>
</div>
<div className="form-header-actions">
<wcs-button className="wcs-secondary" mode="stroked" onClick={() => navigate('/travaux')}>
Annuler
</wcs-button> </wcs-button>
<wcs-button
className="wcs-primary"
onClick={handleCreate}
disabled={saving || !canCreateTravaux}
>
{saving ? (
<>
<wcs-spinner mode="border" style={{ width: '16px', height: '16px', marginRight: '6px' }}></wcs-spinner>
<span>Enregistrement...</span>
</>
) : (
<>
<wcs-mat-icon icon="save" style={{ marginRight: '6px' }}></wcs-mat-icon>
<span>Créer le chantier</span>
</>
)}
</wcs-button>
</div>
</header> </header>
<div className="travaux-form-grid">
{/* Section 1 : Informations générales */}
<div className="travaux-glass-card"> <div className="travaux-glass-card">
<h2 className="travaux-section-title"><wcs-mat-icon icon="info"></wcs-mat-icon> Informations Générales</h2> <div className="section-title-wrap">
<p className="travaux-section-subtitle">Saisissez un titre clair et une description détaillée pour ces travaux.</p> <div className="section-icon-box">
<wcs-mat-icon icon="info"></wcs-mat-icon>
</div>
<div>
<h2 className="travaux-section-title">Informations Générales</h2>
<p className="travaux-section-subtitle">Définissez l'intitulé, la ligne concernée et les consignes d'exploitation.</p>
</div>
</div>
<div className="premium-input-group"> <div className="premium-input-group">
<label className="premium-label">Titre des travaux *</label> <label className="premium-label">Titre des travaux *</label>
<input type="text" className="premium-input" placeholder="Ex: Maintenance des voies Paris-Lyon" value={titre} onChange={(e) => setTitre(e.target.value)} /> <input
</div> type="text"
className="premium-input"
<div className="premium-input-group"> placeholder="Ex: Rénovation caténaire et voies Paris vers Lyon"
<label className="premium-label">Description détaillée</label> value={titre}
<RichTextEditor value={description} onChange={setDescription} placeholder="Contexte opérationnel, impacts prévus..." /> onChange={(e) => setTitre(e.target.value)}
/>
</div> </div>
<div className="premium-input-group"> <div className="premium-input-group">
@@ -185,17 +240,34 @@ export default function CreerTravaux() {
> >
<option value="">Sélectionner une ligne (optionnel)</option> <option value="">Sélectionner une ligne (optionnel)</option>
{lignes.map(l => ( {lignes.map(l => (
<option key={l.id} value={l.id}>{l.nom}</option> <option key={l.id} value={l.id}>{l.nom || l.numero || `Ligne ${l.id}`}</option>
))} ))}
</select> </select>
</div> </div>
<div className="premium-input-group">
<label className="premium-label">Description détaillée & Informations voyageurs</label>
<RichTextEditor
value={description}
onChange={setDescription}
placeholder="Détaillez les impacts prévus, motifs techniques, gares concernées..."
/>
</div>
</div> </div>
{/* Section 2 : Planification & Calendrier */}
<div className="travaux-glass-card"> <div className="travaux-glass-card">
<h2 className="travaux-section-title"><wcs-mat-icon icon="event"></wcs-mat-icon> Planification</h2> <div className="section-title-wrap">
<p className="travaux-section-subtitle">Définissez la période, les jours concernés et la plage horaire d'intervention.</p> <div className="section-icon-box">
<wcs-mat-icon icon="calendar_month"></wcs-mat-icon>
</div>
<div>
<h2 className="travaux-section-title">Planification & Horaires</h2>
<p className="travaux-section-subtitle">Période de validité du chantier, jours impactés et créneaux journaliers.</p>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '20px' }}> <div className="form-columns-2">
<div className="premium-input-group"> <div className="premium-input-group">
<label className="premium-label">Date de début *</label> <label className="premium-label">Date de début *</label>
<input type="date" className="premium-input" value={dateDebut} onChange={(e) => setDateDebut(e.target.value)} /> <input type="date" className="premium-input" value={dateDebut} onChange={(e) => setDateDebut(e.target.value)} />
@@ -204,78 +276,169 @@ export default function CreerTravaux() {
<label className="premium-label">Date de fin *</label> <label className="premium-label">Date de fin *</label>
<input type="date" className="premium-input" value={dateFin} onChange={(e) => setDateFin(e.target.value)} /> <input type="date" className="premium-input" value={dateFin} onChange={(e) => setDateFin(e.target.value)} />
</div> </div>
</div>
<div className="form-columns-2">
<div className="premium-input-group"> <div className="premium-input-group">
<label className="premium-label">Heure de début *</label> <label className="premium-label">Heure de début journalière *</label>
<input type="time" className="premium-input" value={heureDebut} onChange={(e) => setHeureDebut(e.target.value)} /> <input type="time" className="premium-input" value={heureDebut} onChange={(e) => setHeureDebut(e.target.value)} />
</div> </div>
<div className="premium-input-group"> <div className="premium-input-group">
<label className="premium-label">Heure de fin *</label> <label className="premium-label">Heure de fin journalière *</label>
<input type="time" className="premium-input" value={heureFin} onChange={(e) => setHeureFin(e.target.value)} /> <input type="time" className="premium-input" value={heureFin} onChange={(e) => setHeureFin(e.target.value)} />
</div> </div>
</div> </div>
<div className="premium-input-group" style={{ marginTop: '10px' }}> <div className="premium-input-group" style={{ marginTop: '12px' }}>
<label className="premium-label">Jours de travaux *</label> <div className="days-header-row">
<label className="premium-label">Jours impactés dans la semaine *</label>
<div className="days-shortcuts">
<button type="button" className="btn-shortcut" onClick={selectAllDays}>Tous les jours</button>
<button type="button" className="btn-shortcut" onClick={selectWeekdays}>Semaine</button>
<button type="button" className="btn-shortcut" onClick={selectWeekend}>Week-end</button>
</div>
</div>
<div className="premium-checkbox-group"> <div className="premium-checkbox-group">
{JOURS_SEMAINE.map((jour) => ( {JOURS_SEMAINE.map((jour) => (
<label key={jour.value} className={`premium-checkbox-label ${joursTravaux.includes(jour.value) ? 'selected' : ''}`}> <label key={jour.value} className={`premium-checkbox-label ${joursTravaux.includes(jour.value) ? 'selected' : ''}`}>
<input type="checkbox" checked={joursTravaux.includes(jour.value)} onChange={() => toggleJour(jour.value)} /> <input type="checkbox" checked={joursTravaux.includes(jour.value)} onChange={() => toggleJour(jour.value)} />
{jour.label} <span>{jour.label}</span>
</label> </label>
))} ))}
</div> </div>
</div> </div>
</div> </div>
{/* Section 3 : Substitutions */}
<div className="travaux-glass-card"> <div className="travaux-glass-card">
<h2 className="travaux-section-title"><wcs-mat-icon icon="swap_horiz"></wcs-mat-icon> Substitutions (Optionnel)</h2> <div className="section-title-wrap">
<p className="travaux-section-subtitle">Gérez les trains ou cars de substitution associés à ces travaux.</p> <div className="section-icon-box">
<wcs-mat-icon icon="swap_horiz"></wcs-mat-icon>
</div>
<div>
<h2 className="travaux-section-title">Plan de Transport de Substitution</h2>
<p className="travaux-section-subtitle">Associez des cars ou trains spéciaux pour remplacer les circulations supprimées.</p>
</div>
</div>
<label className={`premium-checkbox-label ${substitutionsActive ? 'selected' : ''}`} style={{ display: 'inline-flex', marginBottom: '20px' }}> <label className={`premium-toggle-switch ${substitutionsActive ? 'active' : ''}`}>
<input type="checkbox" checked={substitutionsActive} onChange={(e) => setSubstitutionsActive(e.target.checked)} /> <input
Activer la recherche de substitutions type="checkbox"
checked={substitutionsActive}
onChange={(e) => setSubstitutionsActive(e.target.checked)}
/>
<span className="switch-slider"></span>
<span className="switch-text">
<strong>Activer le plan de substitution pour ce chantier</strong>
<small>Permet de relier des horaires de cars ou trains de substitution actifs</small>
</span>
</label> </label>
{substitutionsActive && ( {substitutionsActive && (
<div> <div className="substitutions-module-body">
<div className="premium-input-group" style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}> <div className="substitutions-search-bar">
<input type="text" className="premium-input" placeholder="Numéro de train, gare..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} onKeyPress={(e) => { if (e.key === 'Enter') handleSearchHoraires(); }} /> <div className="search-input-wrap">
<wcs-button className="wcs-primary" onClick={handleSearchHoraires} style={{ flexShrink: 0, height: '46px' }}>Rechercher</wcs-button> <wcs-mat-icon icon="search" className="search-icon"></wcs-mat-icon>
<input
type="text"
className="premium-input"
placeholder="Rechercher par numéro de train, gare de départ ou terminus..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleSearchHoraires(); }}
/>
</div>
<wcs-button
className="wcs-primary"
onClick={handleSearchHoraires}
disabled={loadingHoraires}
>
<wcs-mat-icon icon="search" style={{ marginRight: '6px' }}></wcs-mat-icon>
<span>Rechercher</span>
</wcs-button>
</div> </div>
{/* Horaires sélectionnés */}
{horairesSelectionnes.length > 0 && ( {horairesSelectionnes.length > 0 && (
<div style={{ marginTop: '20px', background: '#eff6ff', padding: '15px', borderRadius: '10px', border: '1px solid #bfdbfe' }}> <div className="selected-subs-container">
<h4 style={{ margin: '0 0 10px 0', color: '#1d4ed8' }}>{horairesSelectionnes.length} Substitution(s) Sélectionnée(s)</h4> <div className="selected-subs-header">
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> <h4>
{horairesSelectionnes.map(h => ( <wcs-mat-icon icon="check_circle" style={{ color: '#0088ce', marginRight: '6px' }}></wcs-mat-icon>
<div key={h.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#fff', padding: '10px', borderRadius: '8px', border: '1px solid #dbeafe' }}> {horairesSelectionnes.length} circulation{horairesSelectionnes.length > 1 ? 's' : ''} de substitution sélectionnée{horairesSelectionnes.length > 1 ? 's' : ''}
<div> </h4>
<strong style={{ color: '#1e293b' }}>{h.numero_train}</strong> <span style={{ color: '#64748b', fontSize: '0.9rem' }}>— {h.gare_depart_nom} → {h.gare_arrivee_nom}</span>
</div> </div>
<wcs-button className="wcs-danger wcs-small" onClick={() => toggleHoraireSelection(h)}><wcs-mat-icon icon="close"></wcs-mat-icon></wcs-button> <div className="selected-subs-list">
{horairesSelectionnes.map(h => (
<div key={h.id} className="selected-sub-card">
<div className="sub-main-col">
<div className="sub-num-badge">
<wcs-mat-icon icon="directions_bus"></wcs-mat-icon>
<strong>{h.numero_train || 'Train'}</strong>
{h.type_train && <wcs-badge className="wcs-info" shape="round">{h.type_train}</wcs-badge>}
</div>
<div className="sub-route">
<span>{h.gare_depart_nom} ({formatHeure(h.heure_depart)})</span>
<wcs-mat-icon icon="arrow_forward" className="arrow-icon"></wcs-mat-icon>
<span>{h.gare_arrivee_nom} ({formatHeure(h.heure_arrivee)})</span>
</div>
</div>
<button
type="button"
className="btn-remove-sub"
onClick={() => toggleHoraireSelection(h)}
title="Retirer cette substitution"
>
<wcs-mat-icon icon="close"></wcs-mat-icon>
</button>
</div> </div>
))} ))}
</div> </div>
</div> </div>
)} )}
{/* Résultats de recherche */}
{showResults && ( {showResults && (
<div style={{ marginTop: '20px' }}> <div className="search-results-section">
<h4 style={{ marginBottom: '10px', color: '#334155' }}>Résultats ({horairesDisponibles.length})</h4> <div className="search-results-header">
<h4>Résultats disponibles ({horairesDisponibles.length})</h4>
<span className="results-hint">Cliquez sur une circulation pour l'ajouter ou la retirer</span>
</div>
{loadingHoraires ? ( {loadingHoraires ? (
<div style={{ padding: '20px', textAlign: 'center' }}><wcs-spinner></wcs-spinner></div> <div className="search-loading-box">
<wcs-spinner mode="border"></wcs-spinner>
<p>Recherche des circulations en cours...</p>
</div>
) : horairesDisponibles.length === 0 ? (
<div className="search-empty-box">
<p>Aucun horaire de substitution trouvé pour « {searchQuery} ».</p>
</div>
) : ( ) : (
<div style={{ maxHeight: '300px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '8px' }}> <div className="subs-results-list">
{horairesDisponibles.map(h => { {horairesDisponibles.map(h => {
const isSelected = horairesSelectionnes.some(hs => hs.id === h.id); const isSelected = horairesSelectionnes.some(hs => hs.id === h.id);
return ( return (
<div key={h.id} onClick={() => toggleHoraireSelection(h)} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', background: isSelected ? '#eff6ff' : '#f8fafc', border: `1px solid ${isSelected ? '#3b82f6' : '#e2e8f0'}`, borderRadius: '8px', cursor: 'pointer' }}> <div
<div> key={h.id}
<strong style={{ color: '#1e293b', marginRight: '8px' }}>{h.numero_train}</strong> className={`sub-result-card ${isSelected ? 'is-selected' : ''}`}
{h.type_train && <span style={{ fontSize: '0.8rem', background: '#e2e8f0', padding: '2px 6px', borderRadius: '4px', marginRight: '8px' }}>{h.type_train}</span>} onClick={() => toggleHoraireSelection(h)}
<span style={{ color: '#64748b', fontSize: '0.9rem' }}>{h.gare_depart_nom} ({formatHeure(h.heure_depart)}) → {h.gare_arrivee_nom} ({formatHeure(h.heure_arrivee)})</span> >
<div className="result-info">
<div className="result-badge-row">
<strong>{h.numero_train}</strong>
{h.type_train && <wcs-badge className="wcs-info" shape="round">{h.type_train}</wcs-badge>}
{h.ligne_nom && <wcs-badge className="wcs-secondary" shape="round">{h.ligne_nom}</wcs-badge>}
</div>
<div className="result-route-row">
<span>{h.gare_depart_nom} ({formatHeure(h.heure_depart)})</span>
<wcs-mat-icon icon="arrow_forward" className="arrow-icon"></wcs-mat-icon>
<span>{h.gare_arrivee_nom} ({formatHeure(h.heure_arrivee)})</span>
</div>
</div>
<div className="result-checkbox">
<wcs-mat-icon icon={isSelected ? "check_box" : "check_box_outline_blank"} className={isSelected ? "icon-checked" : ""}></wcs-mat-icon>
</div> </div>
{isSelected && <wcs-mat-icon icon="check_circle" style={{ color: '#3b82f6' }}></wcs-mat-icon>}
</div> </div>
); );
})} })}
@@ -286,19 +449,45 @@ export default function CreerTravaux() {
</div> </div>
)} )}
</div> </div>
<div className="premium-actions">
<label className={`premium-checkbox-label ${actif ? 'selected' : ''}`} style={{ marginRight: 'auto' }}>
<input type="checkbox" checked={actif} onChange={(e) => setActif(e.target.checked)} />
{actif ? '✔ Travaux Actifs' : 'Travaux Inactifs'}
</label>
<wcs-button className="wcs-secondary" onClick={() => navigate('/travaux')}>Annuler</wcs-button>
<wcs-button className="wcs-primary" onClick={handleCreate} disabled={saving || !canCreateTravaux}>
{saving ? <wcs-spinner></wcs-spinner> : 'Enregistrer les travaux'}
</wcs-button>
</div> </div>
{/* Bottom Action Bar */}
<div className="form-sticky-bottom-bar">
<label className={`premium-checkbox-label ${actif ? 'selected' : ''}`}>
<input type="checkbox" checked={actif} onChange={(e) => setActif(e.target.checked)} />
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<wcs-badge className={actif ? "wcs-success" : "wcs-secondary"} shape="round">
<wcs-mat-icon icon={actif ? "visibility" : "visibility_off"} style={{ fontSize: '13px', marginRight: '4px' }}></wcs-mat-icon>
{actif ? 'Chantier Actif et visible' : 'Chantier Inactif / Masqué'}
</wcs-badge>
</span>
</label>
<div className="bottom-bar-btns">
<wcs-button className="wcs-secondary" mode="stroked" onClick={() => navigate('/travaux')}>
Annuler
</wcs-button>
<wcs-button
className="wcs-primary"
onClick={handleCreate}
disabled={saving || !canCreateTravaux}
>
{saving ? (
<>
<wcs-spinner mode="border" style={{ width: '16px', height: '16px', marginRight: '6px' }}></wcs-spinner>
<span>Enregistrement...</span>
</>
) : (
<>
<wcs-mat-icon icon="check" style={{ marginRight: '6px' }}></wcs-mat-icon>
<span>Enregistrer les travaux</span>
</>
)}
</wcs-button>
</div>
</div>
</div>
</div> </div>
</div> </div>
); );
+275 -82
View File
@@ -8,13 +8,13 @@ import './Travaux.css';
import RichTextEditor from '../gares/RichTextEditor'; import RichTextEditor from '../gares/RichTextEditor';
const JOURS_SEMAINE = [ const JOURS_SEMAINE = [
{ value: 'lundi', label: 'Lundi' }, { value: 'lundi', label: 'Lundi', short: 'Lun' },
{ value: 'mardi', label: 'Mardi' }, { value: 'mardi', label: 'Mardi', short: 'Mar' },
{ value: 'mercredi', label: 'Mercredi' }, { value: 'mercredi', label: 'Mercredi', short: 'Mer' },
{ value: 'jeudi', label: 'Jeudi' }, { value: 'jeudi', label: 'Jeudi', short: 'Jeu' },
{ value: 'vendredi', label: 'Vendredi' }, { value: 'vendredi', label: 'Vendredi', short: 'Ven' },
{ value: 'samedi', label: 'Samedi' }, { value: 'samedi', label: 'Samedi', short: 'Sam' },
{ value: 'dimanche', label: 'Dimanche' } { value: 'dimanche', label: 'Dimanche', short: 'Dim' }
]; ];
export default function ModifierTravaux() { export default function ModifierTravaux() {
@@ -141,23 +141,36 @@ export default function ModifierTravaux() {
} }
}; };
const formatHeure = (heure) => (heure ? heure.substring(0,5) : '—'); const selectAllDays = () => {
setJoursTravaux(JOURS_SEMAINE.map(j => j.value));
};
const selectWeekdays = () => {
setJoursTravaux(['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi']);
};
const selectWeekend = () => {
setJoursTravaux(['samedi', 'dimanche']);
};
const formatHeure = (heure) => (heure ? heure.substring(0, 5) : '—');
const navigate = (path) => { window.location.href = path; }; const navigate = (path) => { window.location.href = path; };
const handleUpdate = async () => { const handleUpdate = async () => {
if (!canUpdateTravaux) { if (!canUpdateTravaux) {
alert('Vous n\'avez pas les droits pour modifier des travaux.'); alert("Vous n'avez pas les droits pour modifier des travaux.");
return; return;
} }
if (!titre.trim() || !dateDebut || !dateFin || joursTravaux.length === 0 || !heureDebut || !heureFin) { if (!titre.trim() || !dateDebut || !dateFin || joursTravaux.length === 0 || !heureDebut || !heureFin) {
alert('Veuillez remplir tous les champs obligatoires'); alert('Veuillez remplir tous les champs obligatoires (*)');
return; return;
} }
if (new Date(dateFin) < new Date(dateDebut)) { if (new Date(dateFin) < new Date(dateDebut)) {
alert('La date de fin doit être postérieure à la date de début'); alert('La date de fin doit être postérieure ou égale à la date de début.');
return; return;
} }
setSaving(true); setSaving(true);
try { try {
const payload = { const payload = {
@@ -177,12 +190,12 @@ export default function ModifierTravaux() {
const subsIds = substitutionsActive ? horairesSelectionnes.map(h => h.id) : []; const subsIds = substitutionsActive ? horairesSelectionnes.map(h => h.id) : [];
const { error } = await updateTravaux(id, payload, subsIds); const { error } = await updateTravaux(id, payload, subsIds);
if (error) { if (error) {
alert('Erreur lors de la mise à jour'); alert('Erreur lors de la mise à jour : ' + (error.message || 'Erreur'));
} else { } else {
navigate('/travaux'); navigate('/travaux');
} }
} catch (err) { } catch (err) {
alert('Erreur inattendue'); alert("Une erreur inattendue s'est produite lors de la mise à jour.");
} finally { } finally {
setSaving(false); setSaving(false);
} }
@@ -190,43 +203,89 @@ export default function ModifierTravaux() {
if (loading) { if (loading) {
return ( return (
<div className="sncf-groupe panel-layout"> <div className="sncf-groupe travaux-page-root">
<div className="panel-layout">
<div className="panel-nav"><NavPanel /></div> <div className="panel-nav"><NavPanel /></div>
<div className="panel-content travaux-form-page-content" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <div className="panel-content travaux-form-page-content" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '60vh' }}>
<wcs-spinner mode="border" /> <wcs-spinner mode="border" style={{ width: '48px', height: '48px', color: '#0088ce' }}></wcs-spinner>
<p style={{ marginTop: '16px', color: '#64748b', fontWeight: 600 }}>Chargement des données du chantier...</p>
</div>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div className="sncf-groupe panel-layout"> <div className="sncf-groupe travaux-page-root">
<div className="panel-nav"><NavPanel /></div> <div className="panel-layout">
<div className="panel-content travaux-form-page-content"> <div className="panel-nav">
<NavPanel />
<header className="travaux-page-header" style={{ borderBottom: 'none', padding: '0 0 30px 0', background: 'transparent' }}>
<div>
<p className="travaux-page-kicker">Modification</p>
<h1>Modifier les travaux</h1>
<p className="travaux-page-description">Mettez à jour les informations et les substitutions.</p>
</div> </div>
<wcs-button className="wcs-secondary" onClick={() => navigate('/travaux')}>
<wcs-mat-icon icon="close"></wcs-mat-icon> Annuler <div className="panel-content travaux-form-page-content">
{/* Header Banner */}
<header className="travaux-form-header-modern">
<div className="form-header-main">
<button type="button" className="btn-back-link" onClick={() => navigate('/travaux')}>
<wcs-mat-icon icon="arrow_back"></wcs-mat-icon>
<span>Retour aux travaux</span>
</button>
<div className="form-title-group">
<span className="travaux-kicker-pill">
<wcs-mat-icon icon="edit"></wcs-mat-icon>
Modification Chantier
</span>
<h1>Modifier les travaux : {titre || 'Chantier'}</h1>
<p>Mettez à jour les caractéristiques, le calendrier et les substitutions associées.</p>
</div>
</div>
<div className="form-header-actions">
<wcs-button className="wcs-secondary" mode="stroked" onClick={() => navigate('/travaux')}>
Annuler
</wcs-button> </wcs-button>
<wcs-button
className="wcs-primary"
onClick={handleUpdate}
disabled={saving || !canUpdateTravaux}
>
{saving ? (
<>
<wcs-spinner mode="border" style={{ width: '16px', height: '16px', marginRight: '6px' }}></wcs-spinner>
<span>Enregistrement...</span>
</>
) : (
<>
<wcs-mat-icon icon="save" style={{ marginRight: '6px' }}></wcs-mat-icon>
<span>Enregistrer les modifications</span>
</>
)}
</wcs-button>
</div>
</header> </header>
<div className="travaux-form-grid">
{/* Section 1 : Informations générales */}
<div className="travaux-glass-card"> <div className="travaux-glass-card">
<h2 className="travaux-section-title"><wcs-mat-icon icon="info"></wcs-mat-icon> Informations Générales</h2> <div className="section-title-wrap">
<p className="travaux-section-subtitle">Mettez à jour le titre et la description détaillée pour ces travaux.</p> <div className="section-icon-box">
<wcs-mat-icon icon="info"></wcs-mat-icon>
</div>
<div>
<h2 className="travaux-section-title">Informations Générales</h2>
<p className="travaux-section-subtitle">Définissez l'intitulé, la ligne concernée et les consignes d'exploitation.</p>
</div>
</div>
<div className="premium-input-group"> <div className="premium-input-group">
<label className="premium-label">Titre des travaux *</label> <label className="premium-label">Titre des travaux *</label>
<input type="text" className="premium-input" placeholder="Ex: Maintenance des voies Paris-Lyon" value={titre} onChange={(e) => setTitre(e.target.value)} /> <input
</div> type="text"
className="premium-input"
<div className="premium-input-group"> placeholder="Ex: Rénovation caténaire et voies Paris vers Lyon"
<label className="premium-label">Description détaillée</label> value={titre}
<RichTextEditor value={description} onChange={setDescription} placeholder="Contexte opérationnel, impacts prévus..." /> onChange={(e) => setTitre(e.target.value)}
/>
</div> </div>
<div className="premium-input-group"> <div className="premium-input-group">
@@ -239,17 +298,34 @@ export default function ModifierTravaux() {
> >
<option value="">Sélectionner une ligne (optionnel)</option> <option value="">Sélectionner une ligne (optionnel)</option>
{lignes.map(l => ( {lignes.map(l => (
<option key={l.id} value={l.id}>{l.nom}</option> <option key={l.id} value={l.id}>{l.nom || l.numero || `Ligne ${l.id}`}</option>
))} ))}
</select> </select>
</div> </div>
<div className="premium-input-group">
<label className="premium-label">Description détaillée & Informations voyageurs</label>
<RichTextEditor
value={description}
onChange={setDescription}
placeholder="Détaillez les impacts prévus, motifs techniques, gares concernées..."
/>
</div>
</div> </div>
{/* Section 2 : Planification & Calendrier */}
<div className="travaux-glass-card"> <div className="travaux-glass-card">
<h2 className="travaux-section-title"><wcs-mat-icon icon="event"></wcs-mat-icon> Planification</h2> <div className="section-title-wrap">
<p className="travaux-section-subtitle">Définissez la période, les jours concernés et la plage horaire d'intervention.</p> <div className="section-icon-box">
<wcs-mat-icon icon="calendar_month"></wcs-mat-icon>
</div>
<div>
<h2 className="travaux-section-title">Planification & Horaires</h2>
<p className="travaux-section-subtitle">Période de validité du chantier, jours impactés et créneaux journaliers.</p>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '20px' }}> <div className="form-columns-2">
<div className="premium-input-group"> <div className="premium-input-group">
<label className="premium-label">Date de début *</label> <label className="premium-label">Date de début *</label>
<input type="date" className="premium-input" value={dateDebut} onChange={(e) => setDateDebut(e.target.value)} /> <input type="date" className="premium-input" value={dateDebut} onChange={(e) => setDateDebut(e.target.value)} />
@@ -258,78 +334,169 @@ export default function ModifierTravaux() {
<label className="premium-label">Date de fin *</label> <label className="premium-label">Date de fin *</label>
<input type="date" className="premium-input" value={dateFin} onChange={(e) => setDateFin(e.target.value)} /> <input type="date" className="premium-input" value={dateFin} onChange={(e) => setDateFin(e.target.value)} />
</div> </div>
</div>
<div className="form-columns-2">
<div className="premium-input-group"> <div className="premium-input-group">
<label className="premium-label">Heure de début *</label> <label className="premium-label">Heure de début journalière *</label>
<input type="time" className="premium-input" value={heureDebut} onChange={(e) => setHeureDebut(e.target.value)} /> <input type="time" className="premium-input" value={heureDebut} onChange={(e) => setHeureDebut(e.target.value)} />
</div> </div>
<div className="premium-input-group"> <div className="premium-input-group">
<label className="premium-label">Heure de fin *</label> <label className="premium-label">Heure de fin journalière *</label>
<input type="time" className="premium-input" value={heureFin} onChange={(e) => setHeureFin(e.target.value)} /> <input type="time" className="premium-input" value={heureFin} onChange={(e) => setHeureFin(e.target.value)} />
</div> </div>
</div> </div>
<div className="premium-input-group" style={{ marginTop: '10px' }}> <div className="premium-input-group" style={{ marginTop: '12px' }}>
<label className="premium-label">Jours de travaux *</label> <div className="days-header-row">
<label className="premium-label">Jours impactés dans la semaine *</label>
<div className="days-shortcuts">
<button type="button" className="btn-shortcut" onClick={selectAllDays}>Tous les jours</button>
<button type="button" className="btn-shortcut" onClick={selectWeekdays}>Semaine</button>
<button type="button" className="btn-shortcut" onClick={selectWeekend}>Week-end</button>
</div>
</div>
<div className="premium-checkbox-group"> <div className="premium-checkbox-group">
{JOURS_SEMAINE.map((jour) => ( {JOURS_SEMAINE.map((jour) => (
<label key={jour.value} className={`premium-checkbox-label ${joursTravaux.includes(jour.value) ? 'selected' : ''}`}> <label key={jour.value} className={`premium-checkbox-label ${joursTravaux.includes(jour.value) ? 'selected' : ''}`}>
<input type="checkbox" checked={joursTravaux.includes(jour.value)} onChange={() => toggleJour(jour.value)} /> <input type="checkbox" checked={joursTravaux.includes(jour.value)} onChange={() => toggleJour(jour.value)} />
{jour.label} <span>{jour.label}</span>
</label> </label>
))} ))}
</div> </div>
</div> </div>
</div> </div>
{/* Section 3 : Substitutions */}
<div className="travaux-glass-card"> <div className="travaux-glass-card">
<h2 className="travaux-section-title"><wcs-mat-icon icon="swap_horiz"></wcs-mat-icon> Substitutions (Optionnel)</h2> <div className="section-title-wrap">
<p className="travaux-section-subtitle">Gérez les trains ou cars de substitution associés à ces travaux.</p> <div className="section-icon-box">
<wcs-mat-icon icon="swap_horiz"></wcs-mat-icon>
</div>
<div>
<h2 className="travaux-section-title">Plan de Transport de Substitution</h2>
<p className="travaux-section-subtitle">Associez des cars ou trains spéciaux pour remplacer les circulations supprimées.</p>
</div>
</div>
<label className={`premium-checkbox-label ${substitutionsActive ? 'selected' : ''}`} style={{ display: 'inline-flex', marginBottom: '20px' }}> <label className={`premium-toggle-switch ${substitutionsActive ? 'active' : ''}`}>
<input type="checkbox" checked={substitutionsActive} onChange={(e) => setSubstitutionsActive(e.target.checked)} /> <input
Activer la recherche de substitutions type="checkbox"
checked={substitutionsActive}
onChange={(e) => setSubstitutionsActive(e.target.checked)}
/>
<span className="switch-slider"></span>
<span className="switch-text">
<strong>Activer le plan de substitution pour ce chantier</strong>
<small>Permet de relier des horaires de cars ou trains de substitution actifs</small>
</span>
</label> </label>
{substitutionsActive && ( {substitutionsActive && (
<div> <div className="substitutions-module-body">
<div className="premium-input-group" style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}> <div className="substitutions-search-bar">
<input type="text" className="premium-input" placeholder="Numéro de train, gare..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} onKeyPress={(e) => { if (e.key === 'Enter') handleSearchHoraires(); }} /> <div className="search-input-wrap">
<wcs-button className="wcs-primary" onClick={handleSearchHoraires} style={{ flexShrink: 0, height: '46px' }}>Rechercher</wcs-button> <wcs-mat-icon icon="search" className="search-icon"></wcs-mat-icon>
<input
type="text"
className="premium-input"
placeholder="Rechercher par numéro de train, gare de départ ou terminus..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleSearchHoraires(); }}
/>
</div>
<wcs-button
className="wcs-primary"
onClick={handleSearchHoraires}
disabled={loadingHoraires}
>
<wcs-mat-icon icon="search" style={{ marginRight: '6px' }}></wcs-mat-icon>
<span>Rechercher</span>
</wcs-button>
</div> </div>
{/* Horaires sélectionnés */}
{horairesSelectionnes.length > 0 && ( {horairesSelectionnes.length > 0 && (
<div style={{ marginTop: '20px', background: '#eff6ff', padding: '15px', borderRadius: '10px', border: '1px solid #bfdbfe' }}> <div className="selected-subs-container">
<h4 style={{ margin: '0 0 10px 0', color: '#1d4ed8' }}>{horairesSelectionnes.length} Substitution(s) Sélectionnée(s)</h4> <div className="selected-subs-header">
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> <h4>
{horairesSelectionnes.map(h => ( <wcs-mat-icon icon="check_circle" style={{ color: '#0088ce', marginRight: '6px' }}></wcs-mat-icon>
<div key={h.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#fff', padding: '10px', borderRadius: '8px', border: '1px solid #dbeafe' }}> {horairesSelectionnes.length} circulation{horairesSelectionnes.length > 1 ? 's' : ''} de substitution sélectionnée{horairesSelectionnes.length > 1 ? 's' : ''}
<div> </h4>
<strong style={{ color: '#1e293b' }}>{h.numero_train}</strong> <span style={{ color: '#64748b', fontSize: '0.9rem' }}>— {h.gare_depart_nom} → {h.gare_arrivee_nom}</span>
</div> </div>
<wcs-button className="wcs-danger wcs-small" onClick={() => toggleHoraireSelection(h)}><wcs-mat-icon icon="close"></wcs-mat-icon></wcs-button> <div className="selected-subs-list">
{horairesSelectionnes.map(h => (
<div key={h.id} className="selected-sub-card">
<div className="sub-main-col">
<div className="sub-num-badge">
<wcs-mat-icon icon="directions_bus"></wcs-mat-icon>
<strong>{h.numero_train || 'Train'}</strong>
{h.type_train && <wcs-badge className="wcs-info" shape="round">{h.type_train}</wcs-badge>}
</div>
<div className="sub-route">
<span>{h.gare_depart_nom} ({formatHeure(h.heure_depart)})</span>
<wcs-mat-icon icon="arrow_forward" className="arrow-icon"></wcs-mat-icon>
<span>{h.gare_arrivee_nom} ({formatHeure(h.heure_arrivee)})</span>
</div>
</div>
<button
type="button"
className="btn-remove-sub"
onClick={() => toggleHoraireSelection(h)}
title="Retirer cette substitution"
>
<wcs-mat-icon icon="close"></wcs-mat-icon>
</button>
</div> </div>
))} ))}
</div> </div>
</div> </div>
)} )}
{/* Résultats de recherche */}
{showResults && ( {showResults && (
<div style={{ marginTop: '20px' }}> <div className="search-results-section">
<h4 style={{ marginBottom: '10px', color: '#334155' }}>Résultats ({horairesDisponibles.length})</h4> <div className="search-results-header">
<h4>Résultats disponibles ({horairesDisponibles.length})</h4>
<span className="results-hint">Cliquez sur une circulation pour l'ajouter ou la retirer</span>
</div>
{loadingHoraires ? ( {loadingHoraires ? (
<div style={{ padding: '20px', textAlign: 'center' }}><wcs-spinner></wcs-spinner></div> <div className="search-loading-box">
<wcs-spinner mode="border"></wcs-spinner>
<p>Recherche des circulations en cours...</p>
</div>
) : horairesDisponibles.length === 0 ? (
<div className="search-empty-box">
<p>Aucun horaire de substitution trouvé pour « {searchQuery} ».</p>
</div>
) : ( ) : (
<div style={{ maxHeight: '300px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '8px' }}> <div className="subs-results-list">
{horairesDisponibles.map(h => { {horairesDisponibles.map(h => {
const isSelected = horairesSelectionnes.some(hs => hs.id === h.id); const isSelected = horairesSelectionnes.some(hs => hs.id === h.id);
return ( return (
<div key={h.id} onClick={() => toggleHoraireSelection(h)} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', background: isSelected ? '#eff6ff' : '#f8fafc', border: `1px solid ${isSelected ? '#3b82f6' : '#e2e8f0'}`, borderRadius: '8px', cursor: 'pointer' }}> <div
<div> key={h.id}
<strong style={{ color: '#1e293b', marginRight: '8px' }}>{h.numero_train}</strong> className={`sub-result-card ${isSelected ? 'is-selected' : ''}`}
{h.type_train && <span style={{ fontSize: '0.8rem', background: '#e2e8f0', padding: '2px 6px', borderRadius: '4px', marginRight: '8px' }}>{h.type_train}</span>} onClick={() => toggleHoraireSelection(h)}
<span style={{ color: '#64748b', fontSize: '0.9rem' }}>{h.gare_depart_nom} ({formatHeure(h.heure_depart)}) → {h.gare_arrivee_nom} ({formatHeure(h.heure_arrivee)})</span> >
<div className="result-info">
<div className="result-badge-row">
<strong>{h.numero_train}</strong>
{h.type_train && <wcs-badge className="wcs-info" shape="round">{h.type_train}</wcs-badge>}
{h.ligne_nom && <wcs-badge className="wcs-secondary" shape="round">{h.ligne_nom}</wcs-badge>}
</div>
<div className="result-route-row">
<span>{h.gare_depart_nom} ({formatHeure(h.heure_depart)})</span>
<wcs-mat-icon icon="arrow_forward" className="arrow-icon"></wcs-mat-icon>
<span>{h.gare_arrivee_nom} ({formatHeure(h.heure_arrivee)})</span>
</div>
</div>
<div className="result-checkbox">
<wcs-mat-icon icon={isSelected ? "check_box" : "check_box_outline_blank"} className={isSelected ? "icon-checked" : ""}></wcs-mat-icon>
</div> </div>
{isSelected && <wcs-mat-icon icon="check_circle" style={{ color: '#3b82f6' }}></wcs-mat-icon>}
</div> </div>
); );
})} })}
@@ -340,19 +507,45 @@ export default function ModifierTravaux() {
</div> </div>
)} )}
</div> </div>
<div className="premium-actions">
<label className={`premium-checkbox-label ${actif ? 'selected' : ''}`} style={{ marginRight: 'auto' }}>
<input type="checkbox" checked={actif} onChange={(e) => setActif(e.target.checked)} />
{actif ? '✔ Travaux Actifs' : 'Travaux Inactifs'}
</label>
<wcs-button className="wcs-secondary" onClick={() => navigate('/travaux')}>Annuler</wcs-button>
<wcs-button className="wcs-primary" onClick={handleUpdate} disabled={saving || !canUpdateTravaux}>
{saving ? <wcs-spinner></wcs-spinner> : 'Enregistrer les modifications'}
</wcs-button>
</div> </div>
{/* Bottom Action Bar */}
<div className="form-sticky-bottom-bar">
<label className={`premium-checkbox-label ${actif ? 'selected' : ''}`}>
<input type="checkbox" checked={actif} onChange={(e) => setActif(e.target.checked)} />
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<wcs-badge className={actif ? "wcs-success" : "wcs-secondary"} shape="round">
<wcs-mat-icon icon={actif ? "visibility" : "visibility_off"} style={{ fontSize: '13px', marginRight: '4px' }}></wcs-mat-icon>
{actif ? 'Chantier Actif et visible' : 'Chantier Inactif / Masqué'}
</wcs-badge>
</span>
</label>
<div className="bottom-bar-btns">
<wcs-button className="wcs-secondary" mode="stroked" onClick={() => navigate('/travaux')}>
Annuler
</wcs-button>
<wcs-button
className="wcs-primary"
onClick={handleUpdate}
disabled={saving || !canUpdateTravaux}
>
{saving ? (
<>
<wcs-spinner mode="border" style={{ width: '16px', height: '16px', marginRight: '6px' }}></wcs-spinner>
<span>Enregistrement...</span>
</>
) : (
<>
<wcs-mat-icon icon="check" style={{ marginRight: '6px' }}></wcs-mat-icon>
<span>Enregistrer les modifications</span>
</>
)}
</wcs-button>
</div>
</div>
</div>
</div> </div>
</div> </div>
); );
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff