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:
1 parent
d61422ddbf
commit
37ab831aac
44 files changed
+17250
-7625
No files matched your search
@@ -191,3 +191,85 @@
|
||||
transform: none !important;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -208,61 +208,48 @@ body.dark-mode .service-badge {
|
||||
}
|
||||
|
||||
/* Dashboard Overrides */
|
||||
body.dark-mode .dashboard-page {
|
||||
background-color: #121212 !important;
|
||||
body.dark-mode .dashboard-page-root,
|
||||
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 {
|
||||
background: linear-gradient(140deg, #1e1e1e 0%, #2a2a2a 70%, #333 100%) !important;
|
||||
color: #e0e0e0 !important;
|
||||
box-shadow: 0 14px 35px rgba(0,0,0,0.5) !important;
|
||||
body.dark-mode .dashboard-hero-card,
|
||||
[data-theme="dark"] .dashboard-hero-card {
|
||||
background: linear-gradient(135deg, #051829 0%, #0d2847 60%, #15416b 100%) !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-hero p,
|
||||
body.dark-mode .dashboard-hero strong,
|
||||
body.dark-mode .dashboard-subtitle,
|
||||
body.dark-mode .dashboard-kicker {
|
||||
color: #e0e0e0 !important;
|
||||
body.dark-mode .dashboard-kpi-card,
|
||||
body.dark-mode .dashboard-widget-card,
|
||||
body.dark-mode .dashboard-status-bar,
|
||||
[data-theme="dark"] .dashboard-kpi-card,
|
||||
[data-theme="dark"] .dashboard-widget-card,
|
||||
[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-widget,
|
||||
body.dark-mode .widget-header-panel,
|
||||
body.dark-mode .widget-content-panel {
|
||||
background-color: #1e1e1e !important;
|
||||
border-color: #333 !important;
|
||||
color: #e0e0e0 !important;
|
||||
body.dark-mode .dashboard-incident-item,
|
||||
body.dark-mode .dashboard-schedule-item,
|
||||
body.dark-mode .dashboard-action-tile,
|
||||
[data-theme="dark"] .dashboard-incident-item,
|
||||
[data-theme="dark"] .dashboard-schedule-item,
|
||||
[data-theme="dark"] .dashboard-action-tile {
|
||||
background-color: #0e1726 !important;
|
||||
border-color: #243247 !important;
|
||||
color: #f8fafc !important;
|
||||
}
|
||||
body.dark-mode .stat-card-title,
|
||||
body.dark-mode .stat-card-value,
|
||||
body.dark-mode .stat-card-subtitle,
|
||||
body.dark-mode .widget-item-text strong,
|
||||
body.dark-mode .perturbation-item-route,
|
||||
body.dark-mode .horaire-item-route,
|
||||
body.dark-mode .perturbation-item-details,
|
||||
body.dark-mode .quick-action-row-left {
|
||||
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 .dashboard-incident-item:hover,
|
||||
body.dark-mode .dashboard-schedule-item:hover,
|
||||
body.dark-mode .dashboard-action-tile:hover,
|
||||
[data-theme="dark"] .dashboard-incident-item:hover,
|
||||
[data-theme="dark"] .dashboard-schedule-item:hover,
|
||||
[data-theme="dark"] .dashboard-action-tile:hover {
|
||||
background-color: #152238 !important;
|
||||
border-color: #38bdf8 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .widget-header {
|
||||
background-color: #2a2a2a !important;
|
||||
border-color: #333 !important;
|
||||
@@ -565,6 +552,11 @@ body.dark-mode .materiel-roulant-card
|
||||
|
||||
body.dark-mode .dashboard-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 .tarifs-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 .acces-non-autorise-page,
|
||||
body.dark-mode .menu-page {
|
||||
background-color: #121212 !important;
|
||||
background-color: #0b111e !important;
|
||||
}
|
||||
|
||||
body.dark-mode .premium-checkbox-label.selected {
|
||||
@@ -586,3 +578,4 @@ body.dark-mode .premium-checkbox-label.selected {
|
||||
border-color: #0d47a1 !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,3 +31,25 @@
|
||||
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);
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
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;
|
||||
}
|
||||
+1053
-227
File diff suppressed because it is too large.
Load diff
+466
-214
@@ -16,6 +16,7 @@ export default function Dashboard() {
|
||||
|
||||
// État du chargement
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lastRefreshed, setLastRefreshed] = useState(new Date());
|
||||
|
||||
// Perturbations récentes
|
||||
const [recentPerturbations, setRecentPerturbations] = useState([]);
|
||||
@@ -23,12 +24,14 @@ export default function Dashboard() {
|
||||
// Horaires récents
|
||||
const [recentHoraires, setRecentHoraires] = useState([]);
|
||||
const [scopedRegionId, setScopedRegionId] = useState(null);
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
|
||||
// Fonction de chargement des données
|
||||
const loadDashboardData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const user = getCurrentUser();
|
||||
setCurrentUser(user);
|
||||
const regionId = getScopedRegionId(user);
|
||||
setScopedRegionId(regionId || null);
|
||||
|
||||
@@ -88,10 +91,11 @@ export default function Dashboard() {
|
||||
});
|
||||
|
||||
// Perturbations récentes (5 dernières)
|
||||
setRecentPerturbations(perturbations.slice(0, 5));
|
||||
setRecentPerturbations(perturbations.slice(0, 6));
|
||||
|
||||
// Horaires récents (5 derniers)
|
||||
setRecentHoraires(horaires.slice(0, 5));
|
||||
// Horaires récents (6 derniers)
|
||||
setRecentHoraires(horaires.slice(0, 6));
|
||||
setLastRefreshed(new Date());
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement du dashboard:', error);
|
||||
@@ -115,224 +119,472 @@ export default function Dashboard() {
|
||||
window.location.href = path;
|
||||
};
|
||||
|
||||
// Rendu des cartes de statistiques
|
||||
const renderStatCard = (title, value, icon, color = 'primary', subtitle = null) => (
|
||||
<div className={`stat-card stat-card-${color}`}>
|
||||
<div className="stat-card-header">
|
||||
<div className="stat-card-icon-wrap">
|
||||
<wcs-mat-icon icon={icon}></wcs-mat-icon>
|
||||
</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>
|
||||
);
|
||||
// Ponctualité théorique estimée
|
||||
const calculatePunctuality = () => {
|
||||
if (!stats.horaires.total) return 100;
|
||||
const impacted = stats.perturbations.actives;
|
||||
const rate = Math.max(0, Math.min(100, Math.round(((stats.horaires.total - impacted) / stats.horaires.total) * 100)));
|
||||
return rate;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sncf-groupe dashboard-page panel-layout">
|
||||
<div className="panel-content">
|
||||
<header className="dashboard-hero">
|
||||
<div>
|
||||
<p className="dashboard-kicker">Panel de gestion</p>
|
||||
<h1>Tableau de bord</h1>
|
||||
<p className="dashboard-subtitle">Suivez l'activite des horaires, perturbations et donnees d'exploitation en temps reel.</p>
|
||||
</div>
|
||||
<div className="dashboard-hero-side">
|
||||
<wcs-button className="wcs-primary" onClick={loadDashboardData}>
|
||||
<wcs-mat-icon icon="refresh"></wcs-mat-icon>
|
||||
Actualiser
|
||||
</wcs-button>
|
||||
{scopedRegionId && <p>Region courante : <strong>{scopedRegionId}</strong></p>}
|
||||
</div>
|
||||
</header>
|
||||
<div className="dashboard-page-root">
|
||||
<div className="dashboard-container">
|
||||
|
||||
<div className="stats-grid">
|
||||
{renderStatCard('Horaires', stats.horaires.total, 'schedule', 'primary', `${stats.horaires.actifs} actifs`)}
|
||||
{renderStatCard('Perturbations', stats.perturbations.total, 'warning', 'warning', `${stats.perturbations.actives} actives`)}
|
||||
{renderStatCard('Gares', stats.gares.total, 'location_on', 'info', 'Desservies')}
|
||||
{renderStatCard('Lignes', stats.lignes.total, 'route', 'secondary', 'Actives')}
|
||||
{renderStatCard('Types de trains', stats.typesTrains.total, 'train', 'success', 'Disponibles')}
|
||||
{renderStatCard('Retards', stats.perturbations.retards, 'schedule', 'warning', 'En cours')}
|
||||
{/* ================================================================
|
||||
HERO BANNER
|
||||
================================================================ */}
|
||||
<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>
|
||||
|
||||
<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>
|
||||
Actualiser
|
||||
</wcs-button>
|
||||
|
||||
<wcs-button
|
||||
className="wcs-secondary dashboard-quick-create-btn"
|
||||
onClick={() => navigate('/perturbations/creer')}
|
||||
>
|
||||
<wcs-mat-icon icon="warning"></wcs-mat-icon>
|
||||
Signaler un incident
|
||||
</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 className="dashboard-status-divider"></div>
|
||||
|
||||
<div className="dashboard-status-item">
|
||||
<div className="dashboard-status-indicator is-info">
|
||||
<wcs-mat-icon icon="analytics"></wcs-mat-icon>
|
||||
</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 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>
|
||||
|
||||
<div className="widgets-grid">
|
||||
<div className="widgets-column">
|
||||
{renderRecentPerturbations()}
|
||||
{renderQuickActions()}
|
||||
{/* ================================================================
|
||||
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 className="widgets-column">{renderRecentHoraires()}</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>
|
||||
);
|
||||
|
||||
+979
-331
File diff suppressed because it is too large.
Load diff
+905
-604
File diff suppressed because it is too large.
Load diff
@@ -219,12 +219,32 @@ export default function ImportGaresGTFS() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="panel-layout">
|
||||
<div className="panel-nav"><NavPanel /></div>
|
||||
<div className="panel-content">
|
||||
<wcs-card>
|
||||
<div style={{ padding: 20 }}>
|
||||
<h2>Import Gares depuis GTFS</h2>
|
||||
<div className="sncf-groupe gares-page-root">
|
||||
<div className="panel-layout">
|
||||
<div className="panel-nav"><NavPanel /></div>
|
||||
<div className="panel-content gares-modern-container">
|
||||
<section className="gares-hero-banner">
|
||||
<div className="gares-hero-content">
|
||||
<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'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 && (
|
||||
<div>
|
||||
@@ -292,7 +312,7 @@ export default function ImportGaresGTFS() {
|
||||
<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, 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>
|
||||
{expandedRoutes[routeName] && (
|
||||
@@ -398,7 +418,7 @@ export default function ImportGaresGTFS() {
|
||||
)}
|
||||
|
||||
</div>
|
||||
</wcs-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -237,3 +237,92 @@
|
||||
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;
|
||||
}
|
||||
@@ -285,17 +285,17 @@ const InfoGaresTab = forwardRef((props, ref) => {
|
||||
<div className="target-info-list">
|
||||
{targetGare && (
|
||||
<span className="target-badge-item target-gare">
|
||||
🚆 {targetGare}
|
||||
<wcs-mat-icon icon="train" size="s"></wcs-mat-icon> {targetGare}
|
||||
</span>
|
||||
)}
|
||||
{targetLigne && (
|
||||
<span className="target-badge-item target-ligne">
|
||||
🎫 Ligne {targetLigne}
|
||||
<wcs-mat-icon icon="confirmation_number" size="s"></wcs-mat-icon> Ligne {targetLigne}
|
||||
</span>
|
||||
)}
|
||||
{!targetGare && !targetLigne && (
|
||||
<span className="target-badge-item target-global">
|
||||
🌐 Toutes gares / lignes
|
||||
<wcs-mat-icon icon="public" size="s"></wcs-mat-icon> Toutes gares / lignes
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -100,3 +100,49 @@
|
||||
pointer-events: none;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
+1273
-651
File diff suppressed because it is too large.
Load diff
+2232
-2291
File diff suppressed because it is too large.
Load diff
@@ -337,3 +337,103 @@
|
||||
box-shadow: 0 6px 15px rgba(37, 99, 235, 0.4) !important;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -764,3 +764,38 @@ wcs-tabs[headers-only] {
|
||||
border-color: #0b4a7a;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -230,3 +230,52 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,169 +1,161 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
export default function MaterielCard({ materiel, onEdit, onDelete }) {
|
||||
const [index, setIndex] = useState(0);
|
||||
const imgContainerRef = useRef(null);
|
||||
const imgRef = useRef(null);
|
||||
const imgs = React.useMemo(() => {
|
||||
if (!materiel) return [];
|
||||
if (Array.isArray(materiel.images) && materiel.images.length > 0) return materiel.images;
|
||||
if (Array.isArray(materiel.image_url) && materiel.image_url.length > 0) return materiel.image_url;
|
||||
if (typeof materiel.image_url === 'string' && materiel.image_url) return [materiel.image_url];
|
||||
return [];
|
||||
}, [materiel]);
|
||||
const [index, setIndex] = useState(0);
|
||||
const imgContainerRef = useRef(null);
|
||||
const imgRef = useRef(null);
|
||||
const imgs = React.useMemo(() => {
|
||||
if (!materiel) return [];
|
||||
if (Array.isArray(materiel.images) && materiel.images.length > 0) return materiel.images;
|
||||
if (Array.isArray(materiel.image_url) && materiel.image_url.length > 0) return materiel.image_url;
|
||||
if (typeof materiel.image_url === 'string' && materiel.image_url) return [materiel.image_url];
|
||||
return [];
|
||||
}, [materiel]);
|
||||
|
||||
const [offsetX, setOffsetX] = useState(0);
|
||||
const [maxOffset, setMaxOffset] = useState(0);
|
||||
const [offsetX, setOffsetX] = useState(0);
|
||||
const [maxOffset, setMaxOffset] = useState(0);
|
||||
|
||||
const recomputeSizes = () => {
|
||||
const c = imgContainerRef.current;
|
||||
const i = imgRef.current;
|
||||
const cw = c ? c.clientWidth : 0;
|
||||
const iw = (i && i.naturalWidth && i.naturalHeight && c) ? Math.round(i.naturalWidth * (c.clientHeight / i.naturalHeight)) : (i ? i.clientWidth : 0);
|
||||
const computedMaxOffset = Math.min(0, Math.floor(cw - iw));
|
||||
setMaxOffset(computedMaxOffset);
|
||||
setOffsetX(prev => Math.max(computedMaxOffset, Math.min(0, prev)));
|
||||
};
|
||||
const recomputeSizes = () => {
|
||||
const c = imgContainerRef.current;
|
||||
const i = imgRef.current;
|
||||
const cw = c ? c.clientWidth : 0;
|
||||
const iw = (i && i.naturalWidth && i.naturalHeight && c) ? Math.round(i.naturalWidth * (c.clientHeight / i.naturalHeight)) : (i ? i.clientWidth : 0);
|
||||
const computedMaxOffset = Math.min(0, Math.floor(cw - iw));
|
||||
setMaxOffset(computedMaxOffset);
|
||||
setOffsetX(prev => Math.max(computedMaxOffset, Math.min(0, prev)));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
recomputeSizes();
|
||||
const onResize = () => recomputeSizes();
|
||||
window.addEventListener('resize', onResize);
|
||||
return () => window.removeEventListener('resize', onResize);
|
||||
}, [index, imgs.length]);
|
||||
useEffect(() => {
|
||||
recomputeSizes();
|
||||
const onResize = () => recomputeSizes();
|
||||
window.addEventListener('resize', onResize);
|
||||
return () => window.removeEventListener('resize', onResize);
|
||||
}, [index, imgs.length]);
|
||||
|
||||
const next = (e) => {
|
||||
e && e.stopPropagation();
|
||||
if (!imgs || imgs.length === 0) return;
|
||||
if (imgs.length > 1) {
|
||||
setIndex((i) => (i + 1) % imgs.length);
|
||||
} else {
|
||||
const c = imgContainerRef.current;
|
||||
if (!c) return;
|
||||
const step = Math.max(80, Math.floor(c.clientWidth / 2));
|
||||
setOffsetX(prev => Math.max(maxOffset, prev - step));
|
||||
}
|
||||
};
|
||||
const next = (e) => {
|
||||
e && e.stopPropagation();
|
||||
if (!imgs || imgs.length === 0) return;
|
||||
if (imgs.length > 1) {
|
||||
setIndex((i) => (i + 1) % imgs.length);
|
||||
} else {
|
||||
const c = imgContainerRef.current;
|
||||
if (!c) return;
|
||||
const step = Math.max(80, Math.floor(c.clientWidth / 2));
|
||||
setOffsetX(prev => Math.max(maxOffset, prev - step));
|
||||
}
|
||||
};
|
||||
|
||||
const prev = (e) => {
|
||||
e && e.stopPropagation();
|
||||
if (!imgs || imgs.length === 0) return;
|
||||
if (imgs.length > 1) {
|
||||
setIndex((i) => (i - 1 + imgs.length) % imgs.length);
|
||||
} else {
|
||||
const c = imgContainerRef.current;
|
||||
if (!c) return;
|
||||
const step = Math.max(80, Math.floor(c.clientWidth / 2));
|
||||
setOffsetX(prev => Math.min(0, prev + step));
|
||||
}
|
||||
};
|
||||
const prev = (e) => {
|
||||
e && e.stopPropagation();
|
||||
if (!imgs || imgs.length === 0) return;
|
||||
if (imgs.length > 1) {
|
||||
setIndex((i) => (i - 1 + imgs.length) % imgs.length);
|
||||
} else {
|
||||
const c = imgContainerRef.current;
|
||||
if (!c) return;
|
||||
const step = Math.max(80, Math.floor(c.clientWidth / 2));
|
||||
setOffsetX(prev => Math.min(0, prev + step));
|
||||
}
|
||||
};
|
||||
|
||||
const current = imgs && imgs.length > 0 ? imgs[index] : null;
|
||||
const current = imgs && imgs.length > 0 ? imgs[index] : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="card materiel-card"
|
||||
return (
|
||||
<div
|
||||
className="materiel-card-modern"
|
||||
style={{
|
||||
padding: '16px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div
|
||||
ref={imgContainerRef}
|
||||
style={{
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
|
||||
background: '#fff',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
height: '100%'
|
||||
width: '100%',
|
||||
height: 140,
|
||||
borderRadius: 10,
|
||||
marginBottom: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
background: '#f1f5f9'
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<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' }}>
|
||||
{current ? (
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={current}
|
||||
alt={materiel?.nom || ''}
|
||||
onLoad={() => recomputeSizes()}
|
||||
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>
|
||||
)}
|
||||
>
|
||||
{current ? (
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={current}
|
||||
alt={materiel?.nom || ''}
|
||||
onLoad={() => recomputeSizes()}
|
||||
style={{ height: '100%', width: 'auto', objectFit: 'contain', objectPosition: 'left center', display: 'block', transform: `translateX(${offsetX}px)` }}
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
|
||||
{(() => {
|
||||
const isMulti = imgs.length > 1;
|
||||
const canPrev = isMulti ? index > 0 : offsetX < 0;
|
||||
const canNext = isMulti ? index < imgs.length - 1 : offsetX > maxOffset;
|
||||
return (
|
||||
<>
|
||||
{canPrev && (
|
||||
<button
|
||||
aria-label="Précédent"
|
||||
onClick={prev}
|
||||
style={{
|
||||
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)'
|
||||
}}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
)}
|
||||
{(() => {
|
||||
const isMulti = imgs.length > 1;
|
||||
const canPrev = isMulti ? index > 0 : offsetX < 0;
|
||||
const canNext = isMulti ? index < imgs.length - 1 : offsetX > maxOffset;
|
||||
return (
|
||||
<>
|
||||
{canPrev && (
|
||||
<button
|
||||
aria-label="Précédent"
|
||||
onClick={prev}
|
||||
className="materiel-card-nav-btn prev"
|
||||
>
|
||||
<wcs-mat-icon icon="chevron_left" size="s"></wcs-mat-icon>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{canNext && (
|
||||
<button
|
||||
aria-label="Suivant"
|
||||
onClick={next}
|
||||
style={{
|
||||
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)'
|
||||
}}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
{canNext && (
|
||||
<button
|
||||
aria-label="Suivant"
|
||||
onClick={next}
|
||||
className="materiel-card-nav-btn next"
|
||||
>
|
||||
<wcs-mat-icon icon="chevron_right" size="s"></wcs-mat-icon>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 15 }}>{materiel?.nom ?? '-'}</div>
|
||||
<div style={{ fontSize: 12, color: '#666' }}>{materiel?.nom_technique ?? '-'}</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<div style={{ fontWeight: 800, fontSize: 16, color: '#0f172a' }}>{materiel?.nom ?? '-'}</div>
|
||||
<div style={{ fontSize: 12, color: '#64748b', fontWeight: 600 }}>{materiel?.nom_technique ?? '-'}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 13, color: '#333', marginBottom: 8 }}>
|
||||
<div>Capacité: <strong>{materiel?.capacite ?? '-'}</strong></div>
|
||||
<div>Type: <strong>{materiel?.type_train ?? '-'}</strong></div>
|
||||
<div>Numéro série: <strong>{materiel?.numero_serie ?? '-'}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 8 }}>
|
||||
<wcs-button onClick={onEdit}>Modifier</wcs-button>
|
||||
<wcs-button className="wcs-danger" onClick={onDelete}>Supprimer</wcs-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: '#334155', display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 12 }}>
|
||||
<div>Capacité : <strong>{materiel?.capacite ? `${materiel.capacite} places` : '-'}</strong></div>
|
||||
<div>Type de train : <strong>{materiel?.type_train ?? '-'}</strong></div>
|
||||
<div>N° de série : <strong>{materiel?.numero_serie ?? '-'}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 12, borderTop: '1px solid #f1f5f9', paddingTop: 10 }}>
|
||||
<wcs-button className="wcs-secondary" onClick={onEdit}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -645,3 +645,175 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -438,3 +438,174 @@ h1.preview-titre {
|
||||
font-size: 12px;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -52,3 +52,56 @@
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ export default function CreerPerturbation() {
|
||||
|
||||
const handleSelectSingleHoraire = useCallback((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);
|
||||
setPerturbationsArretsMap({ [h.id]: buildArretFromHoraire(h) });
|
||||
setOpenTrains({ [h.id]: true });
|
||||
@@ -184,13 +184,33 @@ export default function CreerPerturbation() {
|
||||
|
||||
if (checked) {
|
||||
if (!types.includes(typeValue)) types.push(typeValue);
|
||||
if (typeValue === 'retard') { a.manual_retard = true; a.retard_minutes = a.retard_minutes || 0; }
|
||||
if (typeValue === 'arret_prolonge') { a.manual_arret_prolonge = true; a.arret_prolonge_minutes = a.arret_prolonge_minutes || 0; }
|
||||
if (typeValue === 'retard') {
|
||||
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; }
|
||||
} else {
|
||||
types = types.filter(t => t !== typeValue);
|
||||
if (typeValue === 'retard') { a.manual_retard = false; a.retard_minutes = 0; }
|
||||
if (typeValue === 'arret_prolonge') { a.manual_arret_prolonge = false; a.arret_prolonge_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_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; }
|
||||
}
|
||||
@@ -209,10 +229,24 @@ export default function CreerPerturbation() {
|
||||
nextArr[index] = { ...nextArr[index], [field]: value };
|
||||
|
||||
if (field === 'retard_minutes') {
|
||||
nextArr[index].manual_retard = true;
|
||||
nextArr[index].retard_minutes = Number(value) || 0;
|
||||
const mins = Number(value) || 0;
|
||||
nextArr[index].retard_minutes = mins;
|
||||
nextArr[index].manual_retard = mins > 0;
|
||||
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].type_perturbation = getPrimaryType(types);
|
||||
}
|
||||
@@ -538,8 +572,10 @@ export default function CreerPerturbation() {
|
||||
</div>
|
||||
<div className="horaire-display-content" style={{ maxHeight: '200px', overflowY: 'auto' }}>
|
||||
{selectedHoraires.map(h => (
|
||||
<div key={h.id} style={{ padding: '8px 0', borderBottom: '1px solid #eee' }}>
|
||||
<strong>Train {h.numero_train}</strong> ({h.ligne_nom || 'N/A'}) - {h.gare_depart_nom} → {h.gare_arrivee_nom}
|
||||
<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}
|
||||
<wcs-mat-icon icon="arrow_forward" style={{ fontSize: '13px', margin: '0 4px', color: '#94a3b8' }}></wcs-mat-icon>
|
||||
{h.gare_arrivee_nom}
|
||||
</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' }}
|
||||
onClick={() => toggleTrainAccordion(h.id)}
|
||||
>
|
||||
<h4 style={{ margin: 0, fontSize: '1rem', color: '#333' }}>
|
||||
Train {h.numero_train} ({h.gare_depart_nom} → {h.gare_arrivee_nom})
|
||||
<h4 style={{ margin: 0, fontSize: '1rem', color: '#333', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
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>
|
||||
<wcs-mat-icon icon={openTrains[h.id] ? 'expand_less' : 'expand_more'} style={{ color: '#555' }} />
|
||||
</div>
|
||||
@@ -677,8 +713,10 @@ export default function CreerPerturbation() {
|
||||
Départ : {formatHeure(h.heure_depart)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{h.gare_depart_nom} → {h.gare_arrivee_nom}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<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 style={{ fontSize: '0.9rem', color: '#4b5563' }}>
|
||||
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">
|
||||
<option value="">Aucun</option>
|
||||
{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>
|
||||
</div>
|
||||
@@ -762,29 +800,76 @@ export default function CreerPerturbation() {
|
||||
|
||||
// ── Rendu principal ───────────────────────────────────────────────────────
|
||||
return (
|
||||
<div className="sncf-groupe">
|
||||
<div className="sncf-groupe perturbations-page-root">
|
||||
<div className="panel-layout">
|
||||
<div className="panel-nav"><NavPanel /></div>
|
||||
<div className="panel-content">
|
||||
<header className="page-header">
|
||||
<h1>Créer une perturbation</h1>
|
||||
<p className="page-description">
|
||||
Étape {currentStep === 'horaire' ? '1' : '2'} / 2 —{' '}
|
||||
{currentStep === 'horaire' ? 'Sélection des horaires' : 'Configuration'}
|
||||
</p>
|
||||
<div className="panel-content perturbations-modern-container">
|
||||
{/* Header Banner */}
|
||||
<header className="page-header-modern">
|
||||
<div className="header-info">
|
||||
<div className="perturbations-hero-tags" style={{ marginBottom: '8px' }}>
|
||||
<span className="perturbations-kicker-pill">
|
||||
<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>
|
||||
<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 === 'perturbation' && renderStepPerturbation()}
|
||||
</div>
|
||||
<div className="creation-preview">
|
||||
|
||||
<div className="sticky-sidebar">
|
||||
{selectedHoraires.length === 1 && (
|
||||
<ParcoursPreview arrets={perturbationsArretsMap[selectedHoraires[0].id]} />
|
||||
)}
|
||||
{selectedHoraires.length > 1 && (
|
||||
<div style={{ padding: '15px', color: '#666', fontStyle: 'italic', background: '#f9f9f9', borderRadius: '8px', border: '1px solid #ddd' }}>
|
||||
Aperçu du parcours non disponible lors de la sélection multiple. Veuillez vous référer aux accordéons détaillés.
|
||||
<div className="summary-card">
|
||||
<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>
|
||||
|
||||
@@ -106,12 +106,16 @@ export default function ModifierPerturbation() {
|
||||
|
||||
// Arrêts
|
||||
if (arretsData.length > 0) {
|
||||
setPerturbationsArrets(arretsData.map(a => ({
|
||||
const mapped = arretsData.map(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)
|
||||
? a.types_perturbation
|
||||
: (a.type_perturbation ? [a.type_perturbation] : [])
|
||||
})));
|
||||
}));
|
||||
setPerturbationsArrets(recomputePropagation(mapped));
|
||||
} else {
|
||||
setPerturbationsArrets(buildArretFromHoraire(horaire));
|
||||
}
|
||||
@@ -148,12 +152,23 @@ export default function ModifierPerturbation() {
|
||||
let types = [...(a.types_perturbation || [])];
|
||||
if (checked) {
|
||||
if (!types.includes(typeValue)) types.push(typeValue);
|
||||
if (typeValue === 'retard') { a.manual_retard = true; a.retard_minutes = a.retard_minutes || 0; }
|
||||
if (typeValue === 'arret_prolonge') { a.manual_arret_prolonge = true; a.arret_prolonge_minutes = a.arret_prolonge_minutes || 0; }
|
||||
if (typeValue === 'retard') {
|
||||
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; }
|
||||
} else {
|
||||
types = types.filter(t => t !== typeValue);
|
||||
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_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; }
|
||||
@@ -170,10 +185,24 @@ export default function ModifierPerturbation() {
|
||||
const next = [...prev];
|
||||
next[index] = { ...next[index], [field]: value };
|
||||
if (field === 'retard_minutes') {
|
||||
next[index].manual_retard = true;
|
||||
next[index].retard_minutes = Number(value) || 0;
|
||||
const mins = Number(value) || 0;
|
||||
next[index].retard_minutes = mins;
|
||||
next[index].manual_retard = mins > 0;
|
||||
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].type_perturbation = getPrimaryType(types);
|
||||
}
|
||||
@@ -512,8 +541,10 @@ export default function ModifierPerturbation() {
|
||||
Départ : {formatHeure(h.heure_depart)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{h.gare_depart_nom} → {h.gare_arrivee_nom}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<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 style={{ fontSize: '0.9rem', color: '#4b5563' }}>
|
||||
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">
|
||||
<option value="">Aucun</option>
|
||||
{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>
|
||||
</div>
|
||||
@@ -597,21 +628,22 @@ export default function ModifierPerturbation() {
|
||||
|
||||
// ── États de chargement / erreur ──────────────────────────────────────────
|
||||
if (loading) return (
|
||||
<div className="sncf-groupe">
|
||||
<div className="sncf-groupe perturbations-page-root">
|
||||
<div className="panel-layout">
|
||||
<div className="panel-nav"><NavPanel /></div>
|
||||
<div className="panel-content">
|
||||
<div className="loading-state"><wcs-spinner mode="border" /><p>Chargement de la perturbation...</p></div>
|
||||
<div className="panel-content perturbations-modern-container" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '60vh' }}>
|
||||
<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>
|
||||
);
|
||||
|
||||
if (error || !selectedHoraire) return (
|
||||
<div className="sncf-groupe">
|
||||
<div className="sncf-groupe perturbations-page-root">
|
||||
<div className="panel-layout">
|
||||
<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-button className="wcs-secondary" onClick={() => (window.location.href = '/perturbations')}>Retour à la liste</wcs-button>
|
||||
</div>
|
||||
@@ -621,23 +653,67 @@ export default function ModifierPerturbation() {
|
||||
|
||||
// ── Rendu principal ───────────────────────────────────────────────────────
|
||||
return (
|
||||
<div className="sncf-groupe">
|
||||
<div className="sncf-groupe perturbations-page-root">
|
||||
<div className="panel-layout">
|
||||
<div className="panel-nav"><NavPanel /></div>
|
||||
<div className="panel-content">
|
||||
<header className="page-header">
|
||||
<h1>Modifier une perturbation</h1>
|
||||
<p className="page-description">
|
||||
Étape {currentStep === 'horaire' ? '1' : '2'} / 2 —{' '}
|
||||
{currentStep === 'horaire' ? 'Horaire perturbé' : 'Configuration'}
|
||||
</p>
|
||||
<div className="panel-content perturbations-modern-container">
|
||||
{/* Header Banner */}
|
||||
<header className="page-header-modern">
|
||||
<div className="header-info">
|
||||
<div className="perturbations-hero-tags" style={{ marginBottom: '8px' }}>
|
||||
<span className="perturbations-kicker-pill">
|
||||
<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>
|
||||
<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 === 'perturbation' && renderStepPerturbation()}
|
||||
</div>
|
||||
<div className="creation-preview">
|
||||
|
||||
<div className="sticky-sidebar">
|
||||
<ParcoursPreview arrets={perturbationsArrets} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -27,10 +27,11 @@ export function doesHoraireCirculateOnDate(horaire, dateString) {
|
||||
|
||||
|
||||
export const TYPES_PERTURBATION_ARRET = [
|
||||
{ value: 'retard', label: 'Retard', color: '#f39c12', bg: '#fff8e6', icon: 'schedule' },
|
||||
{ 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: 'arret_prolonge', label: 'Arrêt Prolongé', color: '#3498db', bg: '#e8f4fd', icon: 'timelapse' },
|
||||
{ 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: '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' },
|
||||
];
|
||||
|
||||
export const CAUSES_PERTURBATION = [
|
||||
@@ -59,6 +60,7 @@ export function getPrimaryType(types) {
|
||||
if (types.includes('modification_parcours')) return 'modification_parcours';
|
||||
if (types.includes('arret_supprime')) return 'arret_supprime';
|
||||
if (types.includes('arret_prolonge')) return 'arret_prolonge';
|
||||
if (types.includes('reduction_retard')) return 'reduction_retard';
|
||||
if (types.includes('retard')) return 'retard';
|
||||
return types[0];
|
||||
}
|
||||
@@ -71,12 +73,15 @@ export function buildArretFromHoraire(horaire) {
|
||||
types_perturbation: [],
|
||||
type_perturbation: '',
|
||||
retard_minutes: 0,
|
||||
reduction_retard_minutes: 0,
|
||||
retard_cumule: 0,
|
||||
retard_entrant: 0,
|
||||
arret_prolonge_minutes: 0,
|
||||
cause_arret: '',
|
||||
is_nouveau_terminus: false,
|
||||
is_nouvelle_origine: false,
|
||||
manual_retard: false,
|
||||
manual_reduction: false,
|
||||
manual_arret_prolonge: false,
|
||||
};
|
||||
|
||||
@@ -94,24 +99,71 @@ export function recomputePropagation(arrets) {
|
||||
if (!arrets || arrets.length === 0) return arrets;
|
||||
|
||||
let cumulativeDelay = 0;
|
||||
let hadDelayEarlier = false;
|
||||
|
||||
return arrets.map((arret) => {
|
||||
const newArret = { ...arret };
|
||||
return arrets.map((arret, idx) => {
|
||||
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;
|
||||
|
||||
cumulativeDelay += currentDelay + prolongedStopTime;
|
||||
newArret.retard_cumule = cumulativeDelay;
|
||||
|
||||
const types = Array.isArray(newArret.types_perturbation) ? [...newArret.types_perturbation] : [];
|
||||
|
||||
if (currentDelay > 0 && !types.includes('retard')) {
|
||||
types.push('retard');
|
||||
// Enregistrer le retard entrant à cet arrêt
|
||||
const delayBeforeStop = cumulativeDelay;
|
||||
if (delayBeforeStop > 0 || manualRetard > 0 || prolongedStopTime > 0) {
|
||||
hadDelayEarlier = true;
|
||||
}
|
||||
|
||||
if (prolongedStopTime > 0 && !types.includes('arret_prolonge')) {
|
||||
types.push('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');
|
||||
}
|
||||
} else if (!newArret.manual_arret_prolonge) {
|
||||
types = types.filter(t => t !== 'arret_prolonge');
|
||||
}
|
||||
}
|
||||
|
||||
newArret.types_perturbation = types;
|
||||
@@ -123,7 +175,7 @@ export function recomputePropagation(arrets) {
|
||||
|
||||
/** 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 }) {
|
||||
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 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';
|
||||
else if (hasSuppression && !hasRetard) typePerturbation = 'suppression';
|
||||
|
||||
const retardTotal = perturbationsArrets.reduce((acc, a) => {
|
||||
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 maxDelay = Math.max(0, ...perturbationsArrets.map(a => Number(a.retard_cumule) || 0));
|
||||
|
||||
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 => ({
|
||||
gare_nom: a.gare_nom,
|
||||
type_modification: getPrimaryType(a.types_perturbation),
|
||||
types_perturbation: a.types_perturbation,
|
||||
retard_minutes: a.retard_minutes || null,
|
||||
arret_prolonge_minutes: a.arret_prolonge_minutes || null,
|
||||
cause: a.cause_arret,
|
||||
is_nouveau_terminus: a.is_nouveau_terminus,
|
||||
is_nouvelle_origine: a.is_nouvelle_origine,
|
||||
gare_nom: a.gare_nom,
|
||||
type_modification: getPrimaryType(a.types_perturbation) || (a.retard_cumule > 0 ? 'retard' : ''),
|
||||
types_perturbation: a.types_perturbation?.length > 0 ? a.types_perturbation : (a.retard_cumule > 0 ? ['retard'] : []),
|
||||
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,
|
||||
cause: a.cause_arret || cause,
|
||||
is_nouveau_terminus: a.is_nouveau_terminus,
|
||||
is_nouvelle_origine: a.is_nouvelle_origine,
|
||||
is_propagated_delay: !!a.is_propagated_delay,
|
||||
}));
|
||||
|
||||
const allDates = (joursPerturbation || []).flatMap(j =>
|
||||
@@ -161,7 +212,7 @@ export function buildPayload({ selectedHoraire, perturbationsArrets, cause, caus
|
||||
const payload = {
|
||||
...(selectedHoraire ? { horaire_id: selectedHoraire.id } : {}),
|
||||
type_perturbation: typePerturbation,
|
||||
duree_retard_minutes: retardTotal > 0 ? retardTotal : null,
|
||||
duree_retard_minutes: maxDelay > 0 ? maxDelay : null,
|
||||
cause,
|
||||
cause_details: causeDetails || null,
|
||||
sillon_substitution_id: sillonSubstitutionId || null,
|
||||
@@ -200,18 +251,19 @@ export function ArretCard({ arret, index, isOpen, onToggle, onToggleType, onArre
|
||||
? arret.types_perturbation
|
||||
: (arret.type_perturbation ? [arret.type_perturbation] : []);
|
||||
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 borderColor = primaryType === 'retard' ? '#f39c12'
|
||||
: primaryType === 'reduction_retard' ? '#059669'
|
||||
: primaryType === 'arret_supprime' ? '#e74c3c'
|
||||
: primaryType === 'arret_prolonge' ? '#3498db'
|
||||
: primaryType === 'modification_parcours'? '#9b59b6'
|
||||
: '#ddd';
|
||||
: (arret.retard_cumule > 0 ? '#f39c12' : '#ddd');
|
||||
|
||||
const inputStyle = (border) => ({
|
||||
padding: '0.6rem 0.75rem', fontSize: '1rem',
|
||||
border: `1px solid ${border}`, borderRadius: '4px', background: 'white',
|
||||
padding: '0.6rem 0.75rem', fontSize: '0.95rem',
|
||||
border: `1px solid ${border}`, borderRadius: '6px', background: 'white',
|
||||
});
|
||||
|
||||
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' }}>
|
||||
{arret.heure_arrivee && <span>Arr: {formatHeure(arret.heure_arrivee)}</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>
|
||||
{hasPerturbation && (
|
||||
<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_prolonge') && <wcs-badge className="wcs-info">Prolongé +{arret.arret_prolonge_minutes} min</wcs-badge>}
|
||||
{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 de départ', arret.heure_depart ? formatHeure(arret.heure_depart) : '—', 'heure_depart'],
|
||||
['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]) => (
|
||||
<div key={lbl}>
|
||||
<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 */}
|
||||
{types.includes('retard') && (
|
||||
<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' }}>
|
||||
Temps de retard (minutes)
|
||||
{arret.is_propagated_delay ? 'Retard supplémentaire à cet arrêt (minutes)' : 'Temps de retard (minutes)'}
|
||||
</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)}
|
||||
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>
|
||||
<input type="text" placeholder="Ex : Incident technique, travaux..." value={arret.cause_arret || ''}
|
||||
onChange={e => onArretChange(index, 'cause_arret', e.target.value)}
|
||||
@@ -356,6 +429,41 @@ export function ArretCard({ arret, index, isOpen, onToggle, onToggleType, onArre
|
||||
</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é */}
|
||||
{types.includes('arret_prolonge') && (
|
||||
<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>
|
||||
{(arret.is_nouveau_terminus || arret.is_nouvelle_origine) && (
|
||||
<p style={{ fontSize: '0.83rem', color: '#27ae60', marginTop: '0.6rem', marginBottom: 0 }}>
|
||||
✓ {arret.is_nouveau_terminus ? 'Terminus' : 'Origine'} désigné — les arrêts {arret.is_nouveau_terminus ? 'suivants' : 'précédents'} seront supprimés.
|
||||
<p style={{ fontSize: '0.83rem', color: '#27ae60', marginTop: '0.6rem', marginBottom: 0, display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
@@ -505,8 +613,9 @@ export function ParcoursPreview({ arrets }) {
|
||||
const types = Array.isArray(arret.types_perturbation)
|
||||
? arret.types_perturbation
|
||||
: (arret.type_perturbation ? [arret.type_perturbation] : []);
|
||||
const hasPerturbation = types.length > 0;
|
||||
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 causeLabel = arret.cause_arret?.trim() || null;
|
||||
|
||||
@@ -520,7 +629,22 @@ export function ParcoursPreview({ arrets }) {
|
||||
</div>
|
||||
</div>
|
||||
<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_prolonge') && <wcs-badge className="wcs-info parcours-stop-badge">+{arret.arret_prolonge_minutes} min</wcs-badge>}
|
||||
{types.includes('modification_parcours') && (
|
||||
|
||||
@@ -643,3 +643,55 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -114,27 +114,66 @@
|
||||
}
|
||||
|
||||
/* Dark mode adjustments */
|
||||
body.dark-mode .maj-header {
|
||||
border-bottom-color: #333;
|
||||
body.dark-mode .maj-content,
|
||||
[data-theme="dark"] .maj-content {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
body.dark-mode .maj-title h3 {
|
||||
color: #e0e0e0;
|
||||
body.dark-mode .maj-card,
|
||||
[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 {
|
||||
background: #451a03;
|
||||
body.dark-mode .maj-header,
|
||||
[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;
|
||||
}
|
||||
|
||||
body.dark-mode .alert-new-version strong {
|
||||
body.dark-mode .alert-new-version strong,
|
||||
[data-theme="dark"] .alert-new-version strong {
|
||||
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;
|
||||
}
|
||||
|
||||
body.dark-mode .progress-text {
|
||||
body.dark-mode .progress-text,
|
||||
[data-theme="dark"] .progress-text {
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.maj-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.alert-new-version {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,3 +274,139 @@
|
||||
color: #12324e;
|
||||
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;
|
||||
}
|
||||
+338
-149
@@ -8,13 +8,13 @@ import './Travaux.css';
|
||||
import RichTextEditor from '../gares/RichTextEditor';
|
||||
|
||||
const JOURS_SEMAINE = [
|
||||
{ value: 'lundi', label: 'Lundi' },
|
||||
{ value: 'mardi', label: 'Mardi' },
|
||||
{ value: 'mercredi', label: 'Mercredi' },
|
||||
{ value: 'jeudi', label: 'Jeudi' },
|
||||
{ value: 'vendredi', label: 'Vendredi' },
|
||||
{ value: 'samedi', label: 'Samedi' },
|
||||
{ value: 'dimanche', label: 'Dimanche' }
|
||||
{ value: 'lundi', label: 'Lundi', short: 'Lun' },
|
||||
{ value: 'mardi', label: 'Mardi', short: 'Mar' },
|
||||
{ value: 'mercredi', label: 'Mercredi', short: 'Mer' },
|
||||
{ value: 'jeudi', label: 'Jeudi', short: 'Jeu' },
|
||||
{ value: 'vendredi', label: 'Vendredi', short: 'Ven' },
|
||||
{ value: 'samedi', label: 'Samedi', short: 'Sam' },
|
||||
{ value: 'dimanche', label: 'Dimanche', short: 'Dim' }
|
||||
];
|
||||
|
||||
export default function CreerTravaux() {
|
||||
@@ -25,9 +25,9 @@ export default function CreerTravaux() {
|
||||
const [description, setDescription] = useState('');
|
||||
const [dateDebut, setDateDebut] = useState('');
|
||||
const [dateFin, setDateFin] = useState('');
|
||||
const [joursTravaux, setJoursTravaux] = useState([]);
|
||||
const [heureDebut, setHeureDebut] = useState('');
|
||||
const [heureFin, setHeureFin] = useState('');
|
||||
const [joursTravaux, setJoursTravaux] = useState(['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche']);
|
||||
const [heureDebut, setHeureDebut] = useState('22:00');
|
||||
const [heureFin, setHeureFin] = useState('06:00');
|
||||
const [actif, setActif] = useState(true);
|
||||
|
||||
// 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 navigate = (path) => { window.location.href = path; };
|
||||
|
||||
const handleCreate = async () => {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -134,171 +146,348 @@ export default function CreerTravaux() {
|
||||
const { error } = await createTravaux(payload, substitutionsIds);
|
||||
|
||||
if (error) {
|
||||
alert('Erreur lors de la création des travaux');
|
||||
alert('Erreur lors de la création des travaux : ' + (error.message || 'Erreur'));
|
||||
} else {
|
||||
navigate('/travaux');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Une erreur inattendue s\'est produite');
|
||||
alert("Une erreur inattendue s'est produite");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sncf-groupe panel-layout">
|
||||
<div className="panel-nav"><NavPanel /></div>
|
||||
<div className="panel-content travaux-form-page-content">
|
||||
|
||||
<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>
|
||||
<wcs-button className="wcs-secondary" onClick={() => navigate('/travaux')}>
|
||||
<wcs-mat-icon icon="close"></wcs-mat-icon> Annuler
|
||||
</wcs-button>
|
||||
</header>
|
||||
|
||||
<div className="travaux-glass-card">
|
||||
<h2 className="travaux-section-title"><wcs-mat-icon icon="info"></wcs-mat-icon> Informations Générales</h2>
|
||||
<p className="travaux-section-subtitle">Saisissez un titre clair et une description détaillée pour ces travaux.</p>
|
||||
|
||||
<div className="premium-input-group">
|
||||
<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)} />
|
||||
</div>
|
||||
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Description détaillée</label>
|
||||
<RichTextEditor value={description} onChange={setDescription} placeholder="Contexte opérationnel, impacts prévus..." />
|
||||
</div>
|
||||
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Ligne ferroviaire associée</label>
|
||||
<select
|
||||
className="premium-input"
|
||||
value={selectedLigneId}
|
||||
onChange={(e) => setSelectedLigneId(e.target.value)}
|
||||
disabled={loadingLignes}
|
||||
>
|
||||
<option value="">Sélectionner une ligne (optionnel)</option>
|
||||
{lignes.map(l => (
|
||||
<option key={l.id} value={l.id}>{l.nom}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="sncf-groupe travaux-page-root">
|
||||
<div className="panel-layout">
|
||||
<div className="panel-nav">
|
||||
<NavPanel />
|
||||
</div>
|
||||
|
||||
<div className="travaux-glass-card">
|
||||
<h2 className="travaux-section-title"><wcs-mat-icon icon="event"></wcs-mat-icon> Planification</h2>
|
||||
<p className="travaux-section-subtitle">Définissez la période, les jours concernés et la plage horaire d'intervention.</p>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '20px' }}>
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Date de début *</label>
|
||||
<input type="date" className="premium-input" value={dateDebut} onChange={(e) => setDateDebut(e.target.value)} />
|
||||
<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="premium-input-group">
|
||||
<label className="premium-label">Date de fin *</label>
|
||||
<input type="date" className="premium-input" value={dateFin} onChange={(e) => setDateFin(e.target.value)} />
|
||||
</div>
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Heure de début *</label>
|
||||
<input type="time" className="premium-input" value={heureDebut} onChange={(e) => setHeureDebut(e.target.value)} />
|
||||
</div>
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Heure de fin *</label>
|
||||
<input type="time" className="premium-input" value={heureFin} onChange={(e) => setHeureFin(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="premium-input-group" style={{ marginTop: '10px' }}>
|
||||
<label className="premium-label">Jours de travaux *</label>
|
||||
<div className="premium-checkbox-group">
|
||||
{JOURS_SEMAINE.map((jour) => (
|
||||
<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)} />
|
||||
{jour.label}
|
||||
</label>
|
||||
))}
|
||||
<div className="form-header-actions">
|
||||
<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="save" style={{ marginRight: '6px' }}></wcs-mat-icon>
|
||||
<span>Créer le chantier</span>
|
||||
</>
|
||||
)}
|
||||
</wcs-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="travaux-glass-card">
|
||||
<h2 className="travaux-section-title"><wcs-mat-icon icon="swap_horiz"></wcs-mat-icon> Substitutions (Optionnel)</h2>
|
||||
<p className="travaux-section-subtitle">Gérez les trains ou cars de substitution associés à ces travaux.</p>
|
||||
|
||||
<label className={`premium-checkbox-label ${substitutionsActive ? 'selected' : ''}`} style={{ display: 'inline-flex', marginBottom: '20px' }}>
|
||||
<input type="checkbox" checked={substitutionsActive} onChange={(e) => setSubstitutionsActive(e.target.checked)} />
|
||||
Activer la recherche de substitutions
|
||||
</label>
|
||||
|
||||
{substitutionsActive && (
|
||||
<div>
|
||||
<div className="premium-input-group" style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<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(); }} />
|
||||
<wcs-button className="wcs-primary" onClick={handleSearchHoraires} style={{ flexShrink: 0, height: '46px' }}>Rechercher</wcs-button>
|
||||
<div className="travaux-form-grid">
|
||||
{/* Section 1 : Informations générales */}
|
||||
<div className="travaux-glass-card">
|
||||
<div className="section-title-wrap">
|
||||
<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>
|
||||
|
||||
{horairesSelectionnes.length > 0 && (
|
||||
<div style={{ marginTop: '20px', background: '#eff6ff', padding: '15px', borderRadius: '10px', border: '1px solid #bfdbfe' }}>
|
||||
<h4 style={{ margin: '0 0 10px 0', color: '#1d4ed8' }}>{horairesSelectionnes.length} Substitution(s) Sélectionnée(s)</h4>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{horairesSelectionnes.map(h => (
|
||||
<div key={h.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#fff', padding: '10px', borderRadius: '8px', border: '1px solid #dbeafe' }}>
|
||||
<div>
|
||||
<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>
|
||||
<wcs-button className="wcs-danger wcs-small" onClick={() => toggleHoraireSelection(h)}><wcs-mat-icon icon="close"></wcs-mat-icon></wcs-button>
|
||||
</div>
|
||||
))}
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Titre des travaux *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="premium-input"
|
||||
placeholder="Ex: Rénovation caténaire et voies Paris vers Lyon"
|
||||
value={titre}
|
||||
onChange={(e) => setTitre(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Ligne ferroviaire associée</label>
|
||||
<select
|
||||
className="premium-input"
|
||||
value={selectedLigneId}
|
||||
onChange={(e) => setSelectedLigneId(e.target.value)}
|
||||
disabled={loadingLignes}
|
||||
>
|
||||
<option value="">Sélectionner une ligne (optionnel)</option>
|
||||
{lignes.map(l => (
|
||||
<option key={l.id} value={l.id}>{l.nom || l.numero || `Ligne ${l.id}`}</option>
|
||||
))}
|
||||
</select>
|
||||
</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>
|
||||
|
||||
{/* Section 2 : Planification & Calendrier */}
|
||||
<div className="travaux-glass-card">
|
||||
<div className="section-title-wrap">
|
||||
<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 className="form-columns-2">
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Date de début *</label>
|
||||
<input type="date" className="premium-input" value={dateDebut} onChange={(e) => setDateDebut(e.target.value)} />
|
||||
</div>
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Date de fin *</label>
|
||||
<input type="date" className="premium-input" value={dateFin} onChange={(e) => setDateFin(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-columns-2">
|
||||
<div className="premium-input-group">
|
||||
<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)} />
|
||||
</div>
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Heure de fin journalière *</label>
|
||||
<input type="time" className="premium-input" value={heureFin} onChange={(e) => setHeureFin(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="premium-input-group" style={{ marginTop: '12px' }}>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{showResults && (
|
||||
<div style={{ marginTop: '20px' }}>
|
||||
<h4 style={{ marginBottom: '10px', color: '#334155' }}>Résultats ({horairesDisponibles.length})</h4>
|
||||
{loadingHoraires ? (
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}><wcs-spinner></wcs-spinner></div>
|
||||
) : (
|
||||
<div style={{ maxHeight: '300px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{horairesDisponibles.map(h => {
|
||||
const isSelected = horairesSelectionnes.some(hs => hs.id === h.id);
|
||||
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>
|
||||
<strong style={{ color: '#1e293b', marginRight: '8px' }}>{h.numero_train}</strong>
|
||||
{h.type_train && <span style={{ fontSize: '0.8rem', background: '#e2e8f0', padding: '2px 6px', borderRadius: '4px', marginRight: '8px' }}>{h.type_train}</span>}
|
||||
<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="premium-checkbox-group">
|
||||
{JOURS_SEMAINE.map((jour) => (
|
||||
<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)} />
|
||||
<span>{jour.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3 : Substitutions */}
|
||||
<div className="travaux-glass-card">
|
||||
<div className="section-title-wrap">
|
||||
<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-toggle-switch ${substitutionsActive ? 'active' : ''}`}>
|
||||
<input
|
||||
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>
|
||||
|
||||
{substitutionsActive && (
|
||||
<div className="substitutions-module-body">
|
||||
<div className="substitutions-search-bar">
|
||||
<div className="search-input-wrap">
|
||||
<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>
|
||||
|
||||
{/* Horaires sélectionnés */}
|
||||
{horairesSelectionnes.length > 0 && (
|
||||
<div className="selected-subs-container">
|
||||
<div className="selected-subs-header">
|
||||
<h4>
|
||||
<wcs-mat-icon icon="check_circle" style={{ color: '#0088ce', marginRight: '6px' }}></wcs-mat-icon>
|
||||
{horairesSelectionnes.length} circulation{horairesSelectionnes.length > 1 ? 's' : ''} de substitution sélectionnée{horairesSelectionnes.length > 1 ? 's' : ''}
|
||||
</h4>
|
||||
</div>
|
||||
<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>
|
||||
{isSelected && <wcs-mat-icon icon="check_circle" style={{ color: '#3b82f6' }}></wcs-mat-icon>}
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* Résultats de recherche */}
|
||||
{showResults && (
|
||||
<div className="search-results-section">
|
||||
<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 ? (
|
||||
<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 className="subs-results-list">
|
||||
{horairesDisponibles.map(h => {
|
||||
const isSelected = horairesSelectionnes.some(hs => hs.id === h.id);
|
||||
return (
|
||||
<div
|
||||
key={h.id}
|
||||
className={`sub-result-card ${isSelected ? 'is-selected' : ''}`}
|
||||
onClick={() => toggleHoraireSelection(h)}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</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 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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,13 +8,13 @@ import './Travaux.css';
|
||||
import RichTextEditor from '../gares/RichTextEditor';
|
||||
|
||||
const JOURS_SEMAINE = [
|
||||
{ value: 'lundi', label: 'Lundi' },
|
||||
{ value: 'mardi', label: 'Mardi' },
|
||||
{ value: 'mercredi', label: 'Mercredi' },
|
||||
{ value: 'jeudi', label: 'Jeudi' },
|
||||
{ value: 'vendredi', label: 'Vendredi' },
|
||||
{ value: 'samedi', label: 'Samedi' },
|
||||
{ value: 'dimanche', label: 'Dimanche' }
|
||||
{ value: 'lundi', label: 'Lundi', short: 'Lun' },
|
||||
{ value: 'mardi', label: 'Mardi', short: 'Mar' },
|
||||
{ value: 'mercredi', label: 'Mercredi', short: 'Mer' },
|
||||
{ value: 'jeudi', label: 'Jeudi', short: 'Jeu' },
|
||||
{ value: 'vendredi', label: 'Vendredi', short: 'Ven' },
|
||||
{ value: 'samedi', label: 'Samedi', short: 'Sam' },
|
||||
{ value: 'dimanche', label: 'Dimanche', short: 'Dim' }
|
||||
];
|
||||
|
||||
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 handleUpdate = async () => {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = {
|
||||
@@ -177,12 +190,12 @@ export default function ModifierTravaux() {
|
||||
const subsIds = substitutionsActive ? horairesSelectionnes.map(h => h.id) : [];
|
||||
const { error } = await updateTravaux(id, payload, subsIds);
|
||||
if (error) {
|
||||
alert('Erreur lors de la mise à jour');
|
||||
alert('Erreur lors de la mise à jour : ' + (error.message || 'Erreur'));
|
||||
} else {
|
||||
navigate('/travaux');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Erreur inattendue');
|
||||
alert("Une erreur inattendue s'est produite lors de la mise à jour.");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -190,169 +203,349 @@ export default function ModifierTravaux() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="sncf-groupe panel-layout">
|
||||
<div className="panel-nav"><NavPanel /></div>
|
||||
<div className="panel-content travaux-form-page-content" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<wcs-spinner mode="border" />
|
||||
<div className="sncf-groupe travaux-page-root">
|
||||
<div className="panel-layout">
|
||||
<div className="panel-nav"><NavPanel /></div>
|
||||
<div className="panel-content travaux-form-page-content" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '60vh' }}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sncf-groupe panel-layout">
|
||||
<div className="panel-nav"><NavPanel /></div>
|
||||
<div className="panel-content travaux-form-page-content">
|
||||
|
||||
<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>
|
||||
<wcs-button className="wcs-secondary" onClick={() => navigate('/travaux')}>
|
||||
<wcs-mat-icon icon="close"></wcs-mat-icon> Annuler
|
||||
</wcs-button>
|
||||
</header>
|
||||
|
||||
<div className="travaux-glass-card">
|
||||
<h2 className="travaux-section-title"><wcs-mat-icon icon="info"></wcs-mat-icon> Informations Générales</h2>
|
||||
<p className="travaux-section-subtitle">Mettez à jour le titre et la description détaillée pour ces travaux.</p>
|
||||
|
||||
<div className="premium-input-group">
|
||||
<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)} />
|
||||
</div>
|
||||
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Description détaillée</label>
|
||||
<RichTextEditor value={description} onChange={setDescription} placeholder="Contexte opérationnel, impacts prévus..." />
|
||||
</div>
|
||||
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Ligne ferroviaire associée</label>
|
||||
<select
|
||||
className="premium-input"
|
||||
value={selectedLigneId}
|
||||
onChange={(e) => setSelectedLigneId(e.target.value)}
|
||||
disabled={loadingLignes}
|
||||
>
|
||||
<option value="">Sélectionner une ligne (optionnel)</option>
|
||||
{lignes.map(l => (
|
||||
<option key={l.id} value={l.id}>{l.nom}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="sncf-groupe travaux-page-root">
|
||||
<div className="panel-layout">
|
||||
<div className="panel-nav">
|
||||
<NavPanel />
|
||||
</div>
|
||||
|
||||
<div className="travaux-glass-card">
|
||||
<h2 className="travaux-section-title"><wcs-mat-icon icon="event"></wcs-mat-icon> Planification</h2>
|
||||
<p className="travaux-section-subtitle">Définissez la période, les jours concernés et la plage horaire d'intervention.</p>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '20px' }}>
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Date de début *</label>
|
||||
<input type="date" className="premium-input" value={dateDebut} onChange={(e) => setDateDebut(e.target.value)} />
|
||||
<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="premium-input-group">
|
||||
<label className="premium-label">Date de fin *</label>
|
||||
<input type="date" className="premium-input" value={dateFin} onChange={(e) => setDateFin(e.target.value)} />
|
||||
</div>
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Heure de début *</label>
|
||||
<input type="time" className="premium-input" value={heureDebut} onChange={(e) => setHeureDebut(e.target.value)} />
|
||||
</div>
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Heure de fin *</label>
|
||||
<input type="time" className="premium-input" value={heureFin} onChange={(e) => setHeureFin(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="premium-input-group" style={{ marginTop: '10px' }}>
|
||||
<label className="premium-label">Jours de travaux *</label>
|
||||
<div className="premium-checkbox-group">
|
||||
{JOURS_SEMAINE.map((jour) => (
|
||||
<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)} />
|
||||
{jour.label}
|
||||
</label>
|
||||
))}
|
||||
<div className="form-header-actions">
|
||||
<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="save" style={{ marginRight: '6px' }}></wcs-mat-icon>
|
||||
<span>Enregistrer les modifications</span>
|
||||
</>
|
||||
)}
|
||||
</wcs-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="travaux-glass-card">
|
||||
<h2 className="travaux-section-title"><wcs-mat-icon icon="swap_horiz"></wcs-mat-icon> Substitutions (Optionnel)</h2>
|
||||
<p className="travaux-section-subtitle">Gérez les trains ou cars de substitution associés à ces travaux.</p>
|
||||
|
||||
<label className={`premium-checkbox-label ${substitutionsActive ? 'selected' : ''}`} style={{ display: 'inline-flex', marginBottom: '20px' }}>
|
||||
<input type="checkbox" checked={substitutionsActive} onChange={(e) => setSubstitutionsActive(e.target.checked)} />
|
||||
Activer la recherche de substitutions
|
||||
</label>
|
||||
|
||||
{substitutionsActive && (
|
||||
<div>
|
||||
<div className="premium-input-group" style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<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(); }} />
|
||||
<wcs-button className="wcs-primary" onClick={handleSearchHoraires} style={{ flexShrink: 0, height: '46px' }}>Rechercher</wcs-button>
|
||||
<div className="travaux-form-grid">
|
||||
{/* Section 1 : Informations générales */}
|
||||
<div className="travaux-glass-card">
|
||||
<div className="section-title-wrap">
|
||||
<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>
|
||||
|
||||
{horairesSelectionnes.length > 0 && (
|
||||
<div style={{ marginTop: '20px', background: '#eff6ff', padding: '15px', borderRadius: '10px', border: '1px solid #bfdbfe' }}>
|
||||
<h4 style={{ margin: '0 0 10px 0', color: '#1d4ed8' }}>{horairesSelectionnes.length} Substitution(s) Sélectionnée(s)</h4>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{horairesSelectionnes.map(h => (
|
||||
<div key={h.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#fff', padding: '10px', borderRadius: '8px', border: '1px solid #dbeafe' }}>
|
||||
<div>
|
||||
<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>
|
||||
<wcs-button className="wcs-danger wcs-small" onClick={() => toggleHoraireSelection(h)}><wcs-mat-icon icon="close"></wcs-mat-icon></wcs-button>
|
||||
</div>
|
||||
))}
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Titre des travaux *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="premium-input"
|
||||
placeholder="Ex: Rénovation caténaire et voies Paris vers Lyon"
|
||||
value={titre}
|
||||
onChange={(e) => setTitre(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Ligne ferroviaire associée</label>
|
||||
<select
|
||||
className="premium-input"
|
||||
value={selectedLigneId}
|
||||
onChange={(e) => setSelectedLigneId(e.target.value)}
|
||||
disabled={loadingLignes}
|
||||
>
|
||||
<option value="">Sélectionner une ligne (optionnel)</option>
|
||||
{lignes.map(l => (
|
||||
<option key={l.id} value={l.id}>{l.nom || l.numero || `Ligne ${l.id}`}</option>
|
||||
))}
|
||||
</select>
|
||||
</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>
|
||||
|
||||
{/* Section 2 : Planification & Calendrier */}
|
||||
<div className="travaux-glass-card">
|
||||
<div className="section-title-wrap">
|
||||
<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 className="form-columns-2">
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Date de début *</label>
|
||||
<input type="date" className="premium-input" value={dateDebut} onChange={(e) => setDateDebut(e.target.value)} />
|
||||
</div>
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Date de fin *</label>
|
||||
<input type="date" className="premium-input" value={dateFin} onChange={(e) => setDateFin(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-columns-2">
|
||||
<div className="premium-input-group">
|
||||
<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)} />
|
||||
</div>
|
||||
<div className="premium-input-group">
|
||||
<label className="premium-label">Heure de fin journalière *</label>
|
||||
<input type="time" className="premium-input" value={heureFin} onChange={(e) => setHeureFin(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="premium-input-group" style={{ marginTop: '12px' }}>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{showResults && (
|
||||
<div style={{ marginTop: '20px' }}>
|
||||
<h4 style={{ marginBottom: '10px', color: '#334155' }}>Résultats ({horairesDisponibles.length})</h4>
|
||||
{loadingHoraires ? (
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}><wcs-spinner></wcs-spinner></div>
|
||||
) : (
|
||||
<div style={{ maxHeight: '300px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{horairesDisponibles.map(h => {
|
||||
const isSelected = horairesSelectionnes.some(hs => hs.id === h.id);
|
||||
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>
|
||||
<strong style={{ color: '#1e293b', marginRight: '8px' }}>{h.numero_train}</strong>
|
||||
{h.type_train && <span style={{ fontSize: '0.8rem', background: '#e2e8f0', padding: '2px 6px', borderRadius: '4px', marginRight: '8px' }}>{h.type_train}</span>}
|
||||
<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="premium-checkbox-group">
|
||||
{JOURS_SEMAINE.map((jour) => (
|
||||
<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)} />
|
||||
<span>{jour.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3 : Substitutions */}
|
||||
<div className="travaux-glass-card">
|
||||
<div className="section-title-wrap">
|
||||
<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-toggle-switch ${substitutionsActive ? 'active' : ''}`}>
|
||||
<input
|
||||
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>
|
||||
|
||||
{substitutionsActive && (
|
||||
<div className="substitutions-module-body">
|
||||
<div className="substitutions-search-bar">
|
||||
<div className="search-input-wrap">
|
||||
<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>
|
||||
|
||||
{/* Horaires sélectionnés */}
|
||||
{horairesSelectionnes.length > 0 && (
|
||||
<div className="selected-subs-container">
|
||||
<div className="selected-subs-header">
|
||||
<h4>
|
||||
<wcs-mat-icon icon="check_circle" style={{ color: '#0088ce', marginRight: '6px' }}></wcs-mat-icon>
|
||||
{horairesSelectionnes.length} circulation{horairesSelectionnes.length > 1 ? 's' : ''} de substitution sélectionnée{horairesSelectionnes.length > 1 ? 's' : ''}
|
||||
</h4>
|
||||
</div>
|
||||
<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>
|
||||
{isSelected && <wcs-mat-icon icon="check_circle" style={{ color: '#3b82f6' }}></wcs-mat-icon>}
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* Résultats de recherche */}
|
||||
{showResults && (
|
||||
<div className="search-results-section">
|
||||
<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 ? (
|
||||
<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 className="subs-results-list">
|
||||
{horairesDisponibles.map(h => {
|
||||
const isSelected = horairesSelectionnes.some(hs => hs.id === h.id);
|
||||
return (
|
||||
<div
|
||||
key={h.id}
|
||||
className={`sub-result-card ${isSelected ? 'is-selected' : ''}`}
|
||||
onClick={() => toggleHoraireSelection(h)}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</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 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>
|
||||
</div>
|
||||
);
|
||||
|
||||
+2337
-611
File diff suppressed because it is too large.
Load diff
+944
-178
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user