From 37ab831aac8c30444971c0965a7f144afa7af445 Mon Sep 17 00:00:00 2001 From: mathis Date: Tue, 22 Sep 2026 22:14:21 +0200 Subject: [PATCH] =?UTF-8?q?RELEASE=20V.1.2.1=20-=20Voici=20les=20fonctionn?= =?UTF-8?q?alit=C3=A9s=20:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ->Refonte visuelle + Prise en charge du mode sombre par l'ensemble des composants. --- .../HoraireModal/CirculationCalendar.css | 82 + src/components/header/AppHeader.css | 161 + src/dark-mode.css | 95 +- .../acces-non-autorise/AccesNonAutorise.css | 22 + src/pages/activer-compte/ActiverCompte.css | 25 + .../annonces-sonores/AnnoncesSonores.css | 56 + .../AttributionMaterielRoulant.css | 77 + .../VueRoulement.css | 51 + src/pages/dashboard/Dashboard.css | 1280 ++++- src/pages/dashboard/Dashboard.js | 678 ++- src/pages/gares/Gares.css | 1310 +++-- src/pages/gares/Gares.js | 1513 +++--- src/pages/gares/ImportGaresGTFS.js | 36 +- src/pages/gares/InfoGaresTab.css | 89 + src/pages/gares/InfoGaresTab.js | 6 +- src/pages/gares/RichTextEditor.css | 46 + .../AttributionQuaisDepartArrivee.css | 44 + .../horaires/AttributionQuaisSchematique.css | 73 + src/pages/horaires/Horaires.css | 1924 ++++--- src/pages/horaires/Horaires.js | 4527 ++++++++--------- src/pages/horaires/ImportsPage.css | 100 + src/pages/informations/Informations.css | 35 + src/pages/login/Login.css | 49 + src/pages/materiel-roulant/MaterielCard.js | 294 +- .../materiel-roulant/MaterielRoulant.css | 1123 +++- src/pages/materiel-roulant/MaterielRoulant.js | 1269 +++-- src/pages/menu/MenuRegional.css | 172 + src/pages/messagerie/Messagerie.css | 58 + src/pages/mon-compte/MonCompte.css | 66 + src/pages/pages-gestion/EditeurPage.css | 171 + src/pages/pages-gestion/PagesGestion.css | 53 + src/pages/parametres/Parametres.css | 139 + src/pages/perturbations/CreerPerturbation.js | 141 +- .../perturbations/ModifierPerturbation.js | 128 +- src/pages/perturbations/Perturbations.css | 2458 +++++---- src/pages/perturbations/Perturbations.js | 1027 +++- src/pages/perturbations/perturbationsUtils.js | 218 +- src/pages/quais/AttributionQuais.css | 52 + src/pages/systeme-maj/SystemeMaj.css | 57 +- src/pages/tarifs/Tarifs.css | 136 + src/pages/travaux/CreerTravaux.js | 489 +- src/pages/travaux/ModifierTravaux.js | 497 +- src/pages/travaux/Travaux.css | 2948 ++++++++--- src/pages/travaux/Travaux.js | 1122 +++- 44 files changed, 17261 insertions(+), 7636 deletions(-) diff --git a/src/components/HoraireModal/CirculationCalendar.css b/src/components/HoraireModal/CirculationCalendar.css index 4cc20a7..eb17da4 100644 --- a/src/components/HoraireModal/CirculationCalendar.css +++ b/src/components/HoraireModal/CirculationCalendar.css @@ -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; +} + diff --git a/src/components/header/AppHeader.css b/src/components/header/AppHeader.css index aaca718..6ee96aa 100644 --- a/src/components/header/AppHeader.css +++ b/src/components/header/AppHeader.css @@ -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; + } +} + + diff --git a/src/dark-mode.css b/src/dark-mode.css index eaf0767..3ccf383 100644 --- a/src/dark-mode.css +++ b/src/dark-mode.css @@ -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; } + diff --git a/src/pages/acces-non-autorise/AccesNonAutorise.css b/src/pages/acces-non-autorise/AccesNonAutorise.css index e44586c..4cf734e 100644 --- a/src/pages/acces-non-autorise/AccesNonAutorise.css +++ b/src/pages/acces-non-autorise/AccesNonAutorise.css @@ -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; +} + + diff --git a/src/pages/activer-compte/ActiverCompte.css b/src/pages/activer-compte/ActiverCompte.css index c415219..87ef398 100644 --- a/src/pages/activer-compte/ActiverCompte.css +++ b/src/pages/activer-compte/ActiverCompte.css @@ -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; +} + + diff --git a/src/pages/annonces-sonores/AnnoncesSonores.css b/src/pages/annonces-sonores/AnnoncesSonores.css index 57f9e2d..c74e06b 100644 --- a/src/pages/annonces-sonores/AnnoncesSonores.css +++ b/src/pages/annonces-sonores/AnnoncesSonores.css @@ -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; +} + + diff --git a/src/pages/attribution-materiel-roulant/AttributionMaterielRoulant.css b/src/pages/attribution-materiel-roulant/AttributionMaterielRoulant.css index 283a3ba..c22e8a6 100644 --- a/src/pages/attribution-materiel-roulant/AttributionMaterielRoulant.css +++ b/src/pages/attribution-materiel-roulant/AttributionMaterielRoulant.css @@ -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; +} + + diff --git a/src/pages/attribution-materiel-roulant/VueRoulement.css b/src/pages/attribution-materiel-roulant/VueRoulement.css index 21f509c..304ccfd 100644 --- a/src/pages/attribution-materiel-roulant/VueRoulement.css +++ b/src/pages/attribution-materiel-roulant/VueRoulement.css @@ -196,4 +196,55 @@ .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; } \ No newline at end of file diff --git a/src/pages/dashboard/Dashboard.css b/src/pages/dashboard/Dashboard.css index d97036d..ba42cbd 100644 --- a/src/pages/dashboard/Dashboard.css +++ b/src/pages/dashboard/Dashboard.css @@ -1,384 +1,1210 @@ -.dashboard-page { - background: #f3f7fc; +/* ========================================================================== + DASHBOARD PAGE — MODERN SNCF DESIGN SYSTEM + ========================================================================== */ + +.dashboard-page-root { + min-height: calc(100vh - 72px); + background-color: #f1f5f9; + padding: 24px 20px 48px; + font-family: 'Avenir', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + color: #0f172a; } -.dashboard-page .panel-content { - width: min(1380px, 100%); +.dashboard-container { + max-width: 1380px; margin: 0 auto; } -.dashboard-hero { - background: linear-gradient(140deg, #082e4f 0%, #11507e 70%, #1d5f90 100%); - border-radius: 16px; - color: #f8fbff; - padding: 28px; +/* ========================================================================== + 1. HERO BANNER + ========================================================================== */ +.dashboard-hero-card { + background: linear-gradient(135deg, #062743 0%, #0d3b66 50%, #0088ce 100%); + border-radius: 18px; + padding: 28px 32px; + color: #ffffff; display: flex; justify-content: space-between; - gap: 18px; - align-items: flex-start; - margin-bottom: 22px; - box-shadow: 0 14px 35px rgba(8, 46, 79, 0.22); + align-items: center; + gap: 24px; + margin-bottom: 24px; + box-shadow: 0 16px 36px rgba(6, 39, 67, 0.22); + position: relative; + overflow: hidden; } -.dashboard-kicker { - margin: 0 0 8px; +.dashboard-hero-card::after { + content: ''; + position: absolute; + top: -60px; + right: -60px; + width: 240px; + height: 240px; + border-radius: 50%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0) 70%); + pointer-events: none; +} + +.dashboard-hero-content { + max-width: 760px; + z-index: 1; +} + +.dashboard-hero-kicker-row { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 10px; +} + +.dashboard-hero-kicker { + display: inline-flex; + align-items: center; + gap: 6px; + background: rgba(255, 255, 255, 0.14); + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 4px 12px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; + color: #e0f2fe; +} + +.dashboard-hero-kicker wcs-mat-icon { + font-size: 14px; + color: #38bdf8; +} + +.dashboard-region-badge { + display: inline-flex; + align-items: center; + gap: 6px; + background: rgba(56, 189, 248, 0.2); + border: 1px solid rgba(56, 189, 248, 0.4); + padding: 4px 12px; + border-radius: 999px; font-size: 12px; + font-weight: 600; + color: #f0f9ff; +} + +.dashboard-region-badge wcs-mat-icon { + font-size: 14px; + color: #7dd3fc; +} + +.dashboard-hero-title { + margin: 0 0 10px; + font-size: 32px; + font-weight: 800; + letter-spacing: -0.02em; + color: #ffffff; + line-height: 1.2; +} + +.dashboard-user-greeting { + font-weight: 400; opacity: 0.9; } -.dashboard-hero h1 { +.dashboard-hero-desc { margin: 0; - font-size: 32px; - color: white; + font-size: 14px; + line-height: 1.6; + color: rgba(240, 249, 255, 0.88); } -.dashboard-subtitle { - margin: 10px 0 0; - max-width: 760px; - color: rgba(247, 251, 255, 0.9); -} - -.dashboard-hero-side { +.dashboard-hero-actions { display: flex; flex-direction: column; align-items: flex-end; - gap: 10px; -} - -.dashboard-hero-side p { - margin: 0; - font-size: 13px; - color: rgba(247, 251, 255, 0.92); -} - -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; - margin-bottom: 18px; + flex-shrink: 0; + z-index: 1; } -.stat-card { - background: #ffffff; - border-radius: 12px; - padding: 16px; - border: 1px solid #dbe3ed; - box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06); +.dashboard-sync-status { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: rgba(240, 249, 255, 0.85); + background: rgba(0, 0, 0, 0.18); + padding: 4px 10px; + border-radius: 999px; } -.stat-card-header { +.dashboard-pulse-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #22c55e; + box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7); + animation: pulseGreen 2s infinite; +} + +@keyframes pulseGreen { + 0% { + box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7); + } + 70% { + box-shadow: 0 0 0 6px rgba(34, 197, 94, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); + } +} + +.dashboard-btn-group { display: flex; - align-items: flex-start; + align-items: center; gap: 10px; } -.stat-card-icon-wrap { - width: 38px; - height: 38px; +.dashboard-btn-group wcs-button { + --wcs-button-border-radius: 8px; +} + +.dashboard-quick-create-btn { + --wcs-button-background-color: rgba(255, 255, 255, 0.15); + --wcs-button-color: #ffffff; + --wcs-button-border-color: rgba(255, 255, 255, 0.3); +} + +.dashboard-quick-create-btn:hover { + --wcs-button-background-color: rgba(255, 255, 255, 0.25); +} + +/* ========================================================================== + 2. SUPERVISION STATUS BAR + ========================================================================== */ +.dashboard-status-bar { + background: #ffffff; + border: 1px solid #dbe3ed; + border-radius: 14px; + padding: 14px 20px; + margin-bottom: 24px; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04); +} + +.dashboard-status-item { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} + +.dashboard-status-indicator { + width: 36px; + height: 36px; border-radius: 10px; - display: inline-flex; + display: flex; align-items: center; justify-content: center; flex-shrink: 0; } -.stat-card-primary .stat-card-icon-wrap { background: #e9f5ff; color: #0d5f97; } -.stat-card-warning .stat-card-icon-wrap { background: #fff6e8; color: #b15d06; } -.stat-card-info .stat-card-icon-wrap { background: #e8f7f8; color: #0f7575; } -.stat-card-secondary .stat-card-icon-wrap { background: #f4efff; color: #5c3bb8; } -.stat-card-success .stat-card-icon-wrap { background: #e9f8ef; color: #1f7f3f; } - -.stat-card-content { - display: flex; - flex-direction: column; - min-width: 0; +.dashboard-status-indicator.is-success { + background: #ecfdf5; + color: #10b981; } -.stat-card-title { - font-size: 12px; - color: #556277; +.dashboard-status-indicator.is-warning { + background: #fef2f2; + color: #ef4444; +} + +.dashboard-status-indicator.is-info { + background: #f0f9ff; + color: #0284c7; +} + +.dashboard-status-indicator.is-secondary { + background: #f5f3ff; + color: #7c3aed; +} + +.dashboard-status-indicator wcs-mat-icon { + font-size: 20px; +} + +.dashboard-status-text { + display: flex; + flex-direction: column; + gap: 2px; +} + +.dashboard-status-label { + font-size: 11px; font-weight: 700; text-transform: uppercase; - letter-spacing: 0.06em; + letter-spacing: 0.04em; + color: #64748b; } -.stat-card-value { - font-size: 30px; - line-height: 1.2; - font-weight: 700; +.dashboard-status-text strong { + font-size: 14px; color: #0f172a; - margin-top: 2px; } -.stat-card-subtitle { - font-size: 13px; - color: #637287; +.dashboard-status-divider { + width: 1px; + height: 32px; + background: #e2e8f0; + margin: 0 16px; } -.widgets-grid { +/* ========================================================================== + 3. KPI CARDS + ========================================================================== */ +.dashboard-kpis-grid { display: grid; - grid-template-columns: 1.15fr 1fr; - gap: 14px; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-bottom: 24px; } -.widgets-column { +.dashboard-kpi-card { + background: #ffffff; + border: 1px solid #dbe3ed; + border-radius: 14px; + padding: 18px 20px; + box-shadow: 0 6px 20px rgba(15, 23, 42, 0.04); + cursor: pointer; display: flex; flex-direction: column; - gap: 14px; -} - -.dashboard-widget { - background: #ffffff; - border-radius: 12px; - border: 1px solid #dbe3ed; - box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06); + transition: all 0.2s ease; + position: relative; overflow: hidden; } -.widget-panel { - transition: transform 0.18s ease, box-shadow 0.18s ease; +.dashboard-kpi-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: #cbd5e1; + transition: background 0.2s ease; } -.widget-panel:hover { - transform: translateY(-1px); - box-shadow: 0 12px 28px rgba(15, 23, 42, 0.1); +.dashboard-kpi-card.card-horaires::before { background: #0088ce; } +.dashboard-kpi-card.card-perturbations::before { background: #f59e0b; } +.dashboard-kpi-card.card-gares::before { background: #10b981; } +.dashboard-kpi-card.card-lignes::before { background: #8b5cf6; } + +.dashboard-kpi-card:hover { + transform: translateY(-3px); + box-shadow: 0 12px 28px rgba(15, 23, 42, 0.08); + border-color: #cbd5e1; } -.widget-header { +.dashboard-kpi-top { display: flex; align-items: center; justify-content: space-between; - padding: 14px 16px; - border-bottom: 1px solid #e8eef6; - background: #f8fbff; + margin-bottom: 12px; } -.widget-header-panel { - background: #ffffff; -} - -.widget-title-icon { - width: 28px; - height: 28px; - border-radius: 8px; - background: #eef6ff; - color: #094074; - display: inline-flex; +.dashboard-kpi-icon-wrapper { + width: 40px; + height: 40px; + border-radius: 10px; + display: flex; align-items: center; justify-content: center; } -.widget-content-panel { +.card-horaires .dashboard-kpi-icon-wrapper { background: #e0f2fe; color: #0284c7; } +.card-perturbations .dashboard-kpi-icon-wrapper { background: #fef3c7; color: #d97706; } +.card-gares .dashboard-kpi-icon-wrapper { background: #dcfce7; color: #16a34a; } +.card-lignes .dashboard-kpi-icon-wrapper { background: #ede9fe; color: #7c3aed; } + +.dashboard-kpi-badge { + font-size: 11px; + font-weight: 700; + padding: 3px 8px; + border-radius: 999px; + background: #f1f5f9; + color: #475569; +} + +.dashboard-kpi-badge.badge-danger { + background: #fee2e2; + color: #dc2626; +} + +.dashboard-kpi-badge.badge-neutral { + background: #ecfdf5; + color: #16a34a; +} + +.dashboard-kpi-number { + font-size: 32px; + font-weight: 800; + color: #0f172a; + line-height: 1.1; + margin-bottom: 4px; +} + +.dashboard-kpi-title { + font-size: 14px; + font-weight: 600; + color: #475569; + margin-bottom: 12px; +} + +.dashboard-kpi-footer { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; + color: #64748b; + border-top: 1px solid #f1f5f9; + padding-top: 10px; + margin-top: auto; +} + +.dashboard-kpi-footer wcs-mat-icon { + font-size: 14px; + transition: transform 0.2s ease; +} + +.dashboard-kpi-card:hover .dashboard-kpi-footer wcs-mat-icon { + transform: translateX(3px); + color: #0088ce; +} + +/* ========================================================================== + 4. MAIN GRID LAYOUT + ========================================================================== */ +.dashboard-main-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + align-items: flex-start; +} + +.dashboard-column { + display: flex; + flex-direction: column; + gap: 20px; +} + +.dashboard-widget-card { background: #ffffff; + border: 1px solid #dbe3ed; + border-radius: 16px; + box-shadow: 0 6px 20px rgba(15, 23, 42, 0.04); + overflow: hidden; } -.widget-link-btn { - --wcs-button-font-size: 13px; +.dashboard-widget-header { + padding: 16px 20px; + border-bottom: 1px solid #eef2f6; + background: #ffffff; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; } -.widget-list-panel { - padding: 8px; +.dashboard-widget-title-wrap { + display: flex; + align-items: center; + gap: 12px; } -.widget-header h3 { +.dashboard-widget-icon { + width: 38px; + height: 38px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.dashboard-widget-icon.icon-warning { + background: #fef3c7; + color: #d97706; +} + +.dashboard-widget-icon.icon-info { + background: #e0f2fe; + color: #0284c7; +} + +.dashboard-widget-icon.icon-primary { + background: #e0e7ff; + color: #4338ca; +} + +.dashboard-widget-title { margin: 0; font-size: 16px; font-weight: 700; color: #0f172a; - display: inline-flex; - align-items: center; - gap: 8px; } -.perturbation-item, -.horaire-item { - padding: 12px; - border: 1px solid transparent; - border-radius: 10px; - margin-bottom: 6px; +.dashboard-widget-subtitle { + font-size: 12px; + color: #64748b; } -.perturbation-item:last-child, -.horaire-item:last-child { - margin-bottom: 0; +.dashboard-widget-body { + padding: 16px; } -.perturbation-item:hover, -.horaire-item:hover { - background: #f7fbff; - border-color: #dbe3ed; -} - -.widget-item-main { +/* Incident Items */ +.dashboard-incident-list { display: flex; - justify-content: space-between; - align-items: flex-start; + flex-direction: column; gap: 10px; } -.widget-item-text { - min-width: 0; +.dashboard-incident-item { + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 14px 16px; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + flex-direction: column; + gap: 8px; } -.widget-item-text strong { - font-size: 14px; +.dashboard-incident-item:hover { + background: #ffffff; + border-color: #cbd5e1; + box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06); + transform: translateY(-1px); +} + +.dashboard-incident-item.item-suppression { + border-left: 4px solid #ef4444; +} + +.dashboard-incident-item.item-retard { + border-left: 4px solid #f59e0b; +} + +.dashboard-incident-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.dashboard-incident-train-badge { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 700; color: #0f172a; } -.perturbation-item-header, -.horaire-item-header { - display: flex; - justify-content: space-between; - gap: 10px; - align-items: flex-start; -} - -.perturbation-item-route, -.horaire-item-route { - margin-top: 4px; - color: #576579; - font-size: 13px; -} - -.perturbation-item-details { - margin-top: 8px; - display: flex; - justify-content: space-between; - font-size: 13px; - color: #576579; - gap: 10px; -} - -.perturbation-item-details span { +.dashboard-badge-pill { display: inline-flex; align-items: center; gap: 4px; + padding: 3px 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; } -.status-active { - color: #1f7f3f; - font-weight: 600; +.dashboard-badge-pill.pill-warning { + background: #fef3c7; + color: #b45309; } -.status-inactive { - color: #7b8798; +.dashboard-badge-pill.pill-danger { + background: #fee2e2; + color: #b91c1c; } -.horaire-item-route { +.dashboard-badge-pill.pill-info { + background: #e0f2fe; + color: #0369a1; +} + +.dashboard-incident-route { display: flex; align-items: center; gap: 8px; + font-size: 13px; + font-weight: 600; + color: #334155; } -.horaire-type-badge { - margin-left: 8px; +.dashboard-incident-route wcs-mat-icon { + font-size: 14px; + color: #94a3b8; } -.quick-actions { - padding: 10px; - display: grid; - gap: 8px; -} - -.quick-action-row { - text-decoration: none; - color: #1f2937; - width: 100%; - border: 1px solid #dbe3ed; - border-radius: 10px; - background: #ffffff; - padding: 10px 12px; +.dashboard-incident-footer { display: flex; align-items: center; justify-content: space-between; - gap: 10px; - cursor: pointer; + font-size: 12px; + color: #64748b; + margin-top: 2px; } -.quick-action-row:hover { - background: #eef6ff; - color: #094074; -} - -.quick-action-row-left { +.dashboard-incident-cause { display: inline-flex; align-items: center; - gap: 8px; - font-size: 14px; - font-weight: 600; + gap: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 80%; } -.quick-action-icon { - width: 24px; - height: 24px; - border-radius: 7px; - background: #f3f8ff; +.dashboard-incident-cause wcs-mat-icon { + font-size: 14px; +} + +.dashboard-status-tag { + font-size: 11px; + font-weight: 700; + padding: 2px 6px; + border-radius: 4px; +} + +.dashboard-status-tag.tag-active { + color: #dc2626; + background: #fef2f2; +} + +.dashboard-status-tag.tag-inactive { + color: #64748b; + background: #f1f5f9; +} + +/* Quick Action Tiles */ +.dashboard-quick-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.dashboard-action-tile { + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 14px 16px; + text-decoration: none; + display: flex; + align-items: center; + gap: 12px; + transition: all 0.15s ease; + color: #0f172a; +} + +.dashboard-action-tile:hover { + background: #ffffff; + border-color: #0088ce; + box-shadow: 0 4px 14px rgba(0, 136, 206, 0.1); + transform: translateY(-1px); +} + +.dashboard-action-icon { + width: 40px; + height: 40px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.dashboard-action-icon.bg-amber { background: #fef3c7; color: #d97706; } +.dashboard-action-icon.bg-orange { background: #ffedd5; color: #ea580c; } +.dashboard-action-icon.bg-sky { background: #e0f2fe; color: #0284c7; } +.dashboard-action-icon.bg-indigo { background: #e0e7ff; color: #4f46e5; } + +.dashboard-action-info { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + flex: 1; +} + +.dashboard-action-info strong { + font-size: 13px; + color: #0f172a; +} + +.dashboard-action-info span { + font-size: 11px; + color: #64748b; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.action-chevron { + font-size: 18px; + color: #94a3b8; + transition: transform 0.2s ease, color 0.2s ease; +} + +.dashboard-action-tile:hover .action-chevron { + transform: translateX(3px); + color: #0088ce; +} + +/* Schedule Items */ +.dashboard-schedule-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.dashboard-schedule-item { + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 14px 16px; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + flex-direction: column; + gap: 10px; +} + +.dashboard-schedule-item:hover { + background: #ffffff; + border-color: #cbd5e1; + box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06); + transform: translateY(-1px); +} + +.dashboard-schedule-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.dashboard-schedule-badges { + display: flex; + align-items: center; + gap: 8px; +} + +.train-id-badge { display: inline-flex; align-items: center; + gap: 5px; + font-size: 13px; + font-weight: 800; + color: #0f172a; + background: #ffffff; + border: 1px solid #cbd5e1; + padding: 3px 8px; + border-radius: 6px; +} + +.train-id-badge wcs-mat-icon { + font-size: 14px; + color: #0088ce; +} + +.train-type-pill { + font-size: 11px; + font-weight: 700; + color: #0284c7; + background: #e0f2fe; + padding: 3px 8px; + border-radius: 999px; +} + +.line-pill { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + font-weight: 600; + color: #475569; + background: #f1f5f9; + padding: 3px 8px; + border-radius: 6px; +} + +.line-pill wcs-mat-icon { + font-size: 13px; + color: #64748b; +} + +.dashboard-schedule-timeline { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 4px 0; +} + +.timeline-station { + display: flex; + flex-direction: column; + gap: 2px; +} + +.timeline-station.arrival { + align-items: flex-end; +} + +.station-time { + font-size: 16px; + font-weight: 800; + color: #0f172a; +} + +.station-title { + font-size: 12px; + font-weight: 600; + color: #475569; +} + +.timeline-connector { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.connector-line { + position: absolute; + left: 0; + right: 0; + height: 2px; + background: #cbd5e1; +} + +.timeline-connector wcs-mat-icon { + position: relative; + background: #f8fafc; + padding: 0 4px; + font-size: 16px; + color: #0088ce; +} + +.dashboard-schedule-stops { + display: flex; + align-items: center; + gap: 4px; + font-size: 11px; + color: #64748b; + border-top: 1px solid #f1f5f9; + padding-top: 6px; +} + +.dashboard-schedule-stops wcs-mat-icon { + font-size: 14px; +} + +/* System summary card */ +.dashboard-system-card { + background: linear-gradient(135deg, #0e1726 0%, #151f30 100%); + border-radius: 14px; + padding: 16px 20px; + color: #ffffff; +} + +.dashboard-system-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 14px; +} + +.dashboard-system-icon { + width: 36px; + height: 36px; + border-radius: 8px; + background: rgba(56, 189, 248, 0.15); + color: #38bdf8; + display: flex; + align-items: center; justify-content: center; } -.quick-action-row-refresh { - font: inherit; +.dashboard-system-header h4 { + margin: 0; + font-size: 14px; + font-weight: 700; + color: #f8fafc; } -.dashboard-state { - min-height: 190px; +.dashboard-system-header p { + margin: 2px 0 0; + font-size: 12px; + color: #94a3b8; +} + +.dashboard-system-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + padding-top: 12px; +} + +.system-stat-col { + display: flex; + flex-direction: column; + gap: 2px; +} + +.system-stat-col span { + font-size: 11px; + color: #94a3b8; +} + +.system-stat-col strong { + font-size: 18px; + color: #f8fafc; +} + +.system-stat-col strong.text-warning { + color: #fbbf24; +} + +.system-stat-col strong.text-success { + color: #4ade80; +} + +/* States */ +.dashboard-loading-box, +.dashboard-empty-box { + padding: 36px 20px; display: flex; flex-direction: column; align-items: center; justify-content: center; - color: #637287; + text-align: center; gap: 10px; } -.dashboard-state-icon { - font-size: 44px; - opacity: 0.4; +.dashboard-loading-box span { + font-size: 13px; + color: #64748b; } -@media (max-width: 1080px) { - .widgets-grid { +.dashboard-empty-icon { + width: 48px; + height: 48px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 4px; +} + +.dashboard-empty-icon.is-success { + background: #ecfdf5; + color: #10b981; +} + +.dashboard-empty-icon.is-info { + background: #f0f9ff; + color: #0284c7; +} + +.dashboard-empty-icon wcs-mat-icon { + font-size: 28px; +} + +.dashboard-empty-box h4 { + margin: 0; + font-size: 15px; + color: #0f172a; +} + +.dashboard-empty-box p { + margin: 0; + font-size: 13px; + color: #64748b; + max-width: 320px; +} + +/* ========================================================================== + 5. RESPONSIVE BREAKPOINTS + ========================================================================== */ +@media (max-width: 1200px) { + .dashboard-kpis-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 980px) { + .dashboard-hero-card { + flex-direction: column; + align-items: stretch; + padding: 24px; + } + + .dashboard-hero-actions { + align-items: flex-start; + width: 100%; + } + + .dashboard-btn-group { + width: 100%; + } + + .dashboard-btn-group wcs-button { + flex: 1; + } + + .dashboard-main-grid { grid-template-columns: 1fr; } - .dashboard-hero { + .dashboard-status-bar { flex-direction: column; + align-items: stretch; + gap: 12px; } - .dashboard-hero-side { - align-items: flex-start; + .dashboard-status-divider { + display: none; } } -@media (max-width: 700px) { - .dashboard-hero { - border-radius: 12px; - padding: 20px; +@media (max-width: 640px) { + .dashboard-page-root { + padding: 14px 12px 36px; } - .dashboard-hero h1 { + .dashboard-hero-card { + border-radius: 14px; + padding: 18px 16px; + } + + .dashboard-hero-title { font-size: 24px; } - .stat-card-value { - font-size: 24px; + .dashboard-kpis-grid { + grid-template-columns: 1fr; } - .widget-header { + .dashboard-quick-grid { + grid-template-columns: 1fr; + } + + .dashboard-btn-group { flex-direction: column; - align-items: flex-start; - gap: 8px; } - .quick-action-row { - padding: 10px; - } - - .horaire-item-route { - flex-wrap: wrap; + .dashboard-btn-group wcs-button { + width: 100%; } } +/* ========================================================================== + 6. DARK MODE + ========================================================================== */ +body.dark-mode .dashboard-page-root, +[data-theme="dark"] .dashboard-page-root { + background-color: #0b111e; + color: #f8fafc; +} + +body.dark-mode .dashboard-hero-card, +[data-theme="dark"] .dashboard-hero-card { + background: linear-gradient(135deg, #051829 0%, #0d2847 60%, #15416b 100%); + border: 1px solid #243247; + box-shadow: 0 16px 36px rgba(0, 0, 0, 0.45); +} + +body.dark-mode .dashboard-status-bar, +[data-theme="dark"] .dashboard-status-bar { + background: #151f30; + border-color: #243247; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25); +} + +body.dark-mode .dashboard-status-text strong, +[data-theme="dark"] .dashboard-status-text strong { + color: #f8fafc; +} + +body.dark-mode .dashboard-status-label, +[data-theme="dark"] .dashboard-status-label { + color: #94a3b8; +} + +body.dark-mode .dashboard-status-divider, +[data-theme="dark"] .dashboard-status-divider { + background: #243247; +} + +body.dark-mode .dashboard-kpi-card, +[data-theme="dark"] .dashboard-kpi-card { + background: #151f30; + border-color: #243247; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); +} + +body.dark-mode .dashboard-kpi-card:hover, +[data-theme="dark"] .dashboard-kpi-card:hover { + border-color: #38bdf8; + box-shadow: 0 12px 28px rgba(0, 0, 0, 0.4); +} + +body.dark-mode .dashboard-kpi-number, +[data-theme="dark"] .dashboard-kpi-number { + color: #f8fafc; +} + +body.dark-mode .dashboard-kpi-title, +[data-theme="dark"] .dashboard-kpi-title { + color: #94a3b8; +} + +body.dark-mode .dashboard-kpi-footer, +[data-theme="dark"] .dashboard-kpi-footer { + border-top-color: #243247; + color: #64748b; +} + +body.dark-mode .dashboard-widget-card, +[data-theme="dark"] .dashboard-widget-card { + background: #151f30; + border-color: #243247; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); +} + +body.dark-mode .dashboard-widget-header, +[data-theme="dark"] .dashboard-widget-header { + background: #151f30; + border-bottom-color: #243247; +} + +body.dark-mode .dashboard-widget-title, +[data-theme="dark"] .dashboard-widget-title { + color: #f8fafc; +} + +body.dark-mode .dashboard-widget-subtitle, +[data-theme="dark"] .dashboard-widget-subtitle { + color: #94a3b8; +} + +body.dark-mode .dashboard-incident-item, +body.dark-mode .dashboard-schedule-item, +[data-theme="dark"] .dashboard-incident-item, +[data-theme="dark"] .dashboard-schedule-item { + background: #0e1726; + border-color: #243247; +} + +body.dark-mode .dashboard-incident-item:hover, +body.dark-mode .dashboard-schedule-item:hover, +[data-theme="dark"] .dashboard-incident-item:hover, +[data-theme="dark"] .dashboard-schedule-item:hover { + background: #152238; + border-color: #334e68; +} + +body.dark-mode .dashboard-incident-train-badge, +[data-theme="dark"] .dashboard-incident-train-badge { + color: #f8fafc; +} + +body.dark-mode .dashboard-incident-route, +[data-theme="dark"] .dashboard-incident-route { + color: #cbd5e1; +} + +body.dark-mode .dashboard-incident-cause, +[data-theme="dark"] .dashboard-incident-cause { + color: #94a3b8; +} + +body.dark-mode .dashboard-action-tile, +[data-theme="dark"] .dashboard-action-tile { + background: #0e1726; + border-color: #243247; + color: #f8fafc; +} + +body.dark-mode .dashboard-action-tile:hover, +[data-theme="dark"] .dashboard-action-tile:hover { + background: #152238; + border-color: #38bdf8; +} + +body.dark-mode .dashboard-action-info strong, +[data-theme="dark"] .dashboard-action-info strong { + color: #f8fafc; +} + +body.dark-mode .dashboard-action-info span, +[data-theme="dark"] .dashboard-action-info span { + color: #94a3b8; +} + +body.dark-mode .train-id-badge, +[data-theme="dark"] .train-id-badge { + background: #151f30; + border-color: #243247; + color: #f8fafc; +} + +body.dark-mode .line-pill, +[data-theme="dark"] .line-pill { + background: #151f30; + color: #cbd5e1; +} + +body.dark-mode .station-time, +[data-theme="dark"] .station-time { + color: #f8fafc; +} + +body.dark-mode .station-title, +[data-theme="dark"] .station-title { + color: #94a3b8; +} + +body.dark-mode .connector-line, +[data-theme="dark"] .connector-line { + background: #243247; +} + +body.dark-mode .timeline-connector wcs-mat-icon, +[data-theme="dark"] .timeline-connector wcs-mat-icon { + background: #0e1726; +} + +body.dark-mode .dashboard-schedule-stops, +[data-theme="dark"] .dashboard-schedule-stops { + border-top-color: #1a2a42; + color: #94a3b8; +} + +body.dark-mode .dashboard-empty-box h4, +[data-theme="dark"] .dashboard-empty-box h4 { + color: #f8fafc; +} + +body.dark-mode .dashboard-empty-box p, +[data-theme="dark"] .dashboard-empty-box p { + color: #94a3b8; +} diff --git a/src/pages/dashboard/Dashboard.js b/src/pages/dashboard/Dashboard.js index 0d84640..e93655c 100644 --- a/src/pages/dashboard/Dashboard.js +++ b/src/pages/dashboard/Dashboard.js @@ -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) => ( -
-
-
- -
-
- {title} - {loading ? '...' : value} - {subtitle && {subtitle}} -
-
-
- ); - - // Rendu de la liste des perturbations récentes - const renderRecentPerturbations = () => ( -
-
-

- - Perturbations récentes -

- navigate('/perturbations')} - > - Voir tout - -
-
- {loading ? ( -
- -

Chargement...

-
- ) : recentPerturbations.length === 0 ? ( -
- -

Aucune perturbation

-
- ) : ( -
- {recentPerturbations.map(p => ( -
-
-
- {p.horaire?.numero_train || 'N/A'} - - {p.horaire?.gare_depart_nom} → {p.horaire?.gare_arrivee_nom} - -
- - {p.type_perturbation === 'retard' ? `Retard +${p.duree_retard_minutes || 0} min` : - p.type_perturbation === 'suppression' ? 'Supprime' : - 'Modifié'} - -
-
- - - {p.cause || 'Non spécifiée'} - - - {p.actif ? '● Active' : '○ Inactive'} - -
-
- ))} -
- )} -
-
- ); - - // Rendu de la liste des horaires récents - const renderRecentHoraires = () => ( -
-
-

- - Horaires récents -

- navigate('/horaires')} - > - Voir tout - -
-
- {loading ? ( -
- -

Chargement...

-
- ) : recentHoraires.length === 0 ? ( -
- -

Aucun horaire

-
- ) : ( -
- {recentHoraires.map(h => ( -
-
-
- {h.numero_train} - {h.type_train && ( - - {h.type_train} - - )} -
- {h.ligne_nom && ( - {h.ligne_nom} - )} -
-
- - {h.gare_depart_nom} ({formatHeure(h.heure_depart)}) - - - - {h.gare_arrivee_nom} ({formatHeure(h.heure_arrivee)}) - -
-
- ))} -
- )} -
-
- ); - - // Rendu des actions rapides - const renderQuickActions = () => ( -
-
-

- - Actions rapides -

-
-
-
- - - - Créer une perturbation - - - - - - - Gérer les horaires - - - - - - - Gérer les gares - - - - -
-
-
- ); + // 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 ( -
-
-
-
-

Panel de gestion

-

Tableau de bord

-

Suivez l'activite des horaires, perturbations et donnees d'exploitation en temps reel.

+
+
+ + {/* ================================================================ + HERO BANNER + ================================================================ */} +
+
+
+ + + Centre de supervision ferroviaire + + {scopedRegionId && ( + + + Région : {scopedRegionId} + + )} +
+ +

+ Tableau de bord + {currentUser?.nom ? , {currentUser.prenom || currentUser.nom} : null} +

+ +

+ Pilotage centralisé des circulations, gestion du plan de transport, détection des aléas et état des infrastructures en temps réel. +

-
- - - Actualiser - - {scopedRegionId &&

Region courante : {scopedRegionId}

} -
-
-
- {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')} +
+
+ + Dernière synchro : {lastRefreshed.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })} +
+ +
+ + + Actualiser + + + navigate('/perturbations/creer')} + > + + Signaler un incident + +
+
+ + + {/* ================================================================ + SUPERVISION STATUS BAR + ================================================================ */} +
+
+
0 ? 'is-warning' : 'is-success'}`}> + 0 ? 'warning' : 'check_circle'}> +
+
+ État du Réseau + {stats.perturbations.actives > 0 ? `${stats.perturbations.actives} incident(s) en cours` : 'Circulation normale et fluide'} +
+
+ +
+ +
+
+ +
+
+ Taux de régularité théorique + {calculatePunctuality()}% à l'heure +
+
+ +
+ +
+
+ +
+
+ Sillons actifs + {stats.horaires.total} circulations programmées +
+
-
-
- {renderRecentPerturbations()} - {renderQuickActions()} + {/* ================================================================ + KPI CARDS + ================================================================ */} +
+ {/* Sillons / Horaires */} +
navigate('/horaires')}> +
+
+ +
+ {stats.horaires.actifs} actifs +
+
{loading ? '...' : stats.horaires.total}
+
Horaires & Sillons
+
+ Gérer les plans de transport + +
+
+ + {/* Perturbations */} +
navigate('/perturbations')}> +
+
+ +
+ 0 ? 'badge-danger' : 'badge-neutral'}`}> + {stats.perturbations.actives} actives + +
+
{loading ? '...' : stats.perturbations.total}
+
Perturbations & Aléas
+
+ {stats.perturbations.retards} retards · {stats.perturbations.suppressions} suppressions + +
+
+ + {/* Gares */} +
navigate('/gares')}> +
+
+ +
+ Réseau +
+
{loading ? '...' : stats.gares.total}
+
Gares & Points d'arrêt
+
+ Configuration des quais & services + +
+
+ + {/* Lignes */} +
navigate('/horaires')}> +
+
+ +
+ Maillage +
+
{loading ? '...' : stats.lignes.total}
+
Lignes Commerciales
+
+ {stats.typesTrains.total} matériels types + +
-
{renderRecentHoraires()}
+ + {/* ================================================================ + MAIN PANELS GRID + ================================================================ */} +
+ + {/* Colonne gauche : Perturbations et Actions */} +
+ + {/* Widget Perturbations récentes */} +
+
+
+
+ +
+
+

Incidents & Perturbations

+ Derniers aléas déclarés sur le réseau +
+
+ navigate('/perturbations')} + > + Tout voir ({stats.perturbations.total}) + +
+ +
+ {loading ? ( +
+ + Chargement des incidents... +
+ ) : recentPerturbations.length === 0 ? ( +
+
+ +
+

Aucune perturbation active

+

Toutes les circulations ferroviaires s'effectuent conformément au plan de transport.

+
+ ) : ( +
+ {recentPerturbations.map(p => ( +
navigate(`/perturbations/modifier/${p.id}`)} + > +
+
+ + {p.horaire?.numero_train || 'Train'} +
+ + + + {p.type_perturbation === 'retard' ? `+${p.duree_retard_minutes || 0} min` : + p.type_perturbation === 'suppression' ? 'Supprimé' : + 'Modifié'} + +
+ +
+ {p.horaire?.gare_depart_nom || 'Départ'} + + {p.horaire?.gare_arrivee_nom || 'Arrivée'} +
+ +
+ + + {p.cause || 'Motif d\'exploitation non spécifié'} + + + + {p.actif ? 'En cours' : 'Résolu'} + +
+
+ ))} +
+ )} +
+
+ + {/* Widget Actions Rapides */} + + +
+ + {/* Colonne droite : Horaires & Circulation */} +
+ +
+
+
+
+ +
+
+

Plan de transport récent

+ Circulations programmées au référentiel +
+
+ navigate('/horaires')} + > + Tous les horaires ({stats.horaires.total}) + +
+ +
+ {loading ? ( +
+ + Chargement des horaires... +
+ ) : recentHoraires.length === 0 ? ( +
+
+ +
+

Aucun horaire enregistré

+

Commencez par créer des circulations ou importez une fiche horaire.

+ navigate('/horaires')} + > + Ajouter un horaire + +
+ ) : ( +
+ {recentHoraires.map(h => ( +
navigate('/horaires')} + > +
+
+ + + {h.numero_train} + + {h.type_train && ( + {h.type_train} + )} +
+ {h.ligne_nom && ( + + + {h.ligne_nom} + + )} +
+ +
+
+ {formatHeure(h.heure_depart)} + + {h.gare_depart_nom || 'Départ'} +
+ +
+ + +
+ +
+ {formatHeure(h.heure_arrivee)} + + {h.gare_arrivee_nom || 'Arrivée'} +
+
+ + {h.gares_desservies && h.gares_desservies.length > 0 && ( +
+ + {h.gares_desservies.length} arrêt(s) intermédiaire(s) +
+ )} +
+ ))} +
+ )} +
+
+ + {/* Carte état du système / infos d'exploitation */} +
+
+
+ +
+
+

État des services d'exploitation

+

Système opérationnel & données à jour

+
+
+
+
+ Circulations + {stats.horaires.total} +
+
+ Gares reliées + {stats.gares.total} +
+
+ Incidents + 0 ? 'text-warning' : 'text-success'}> + {stats.perturbations.actives} + +
+
+
+ +
+ +
+
); diff --git a/src/pages/gares/Gares.css b/src/pages/gares/Gares.css index b9159ed..08d37f6 100644 --- a/src/pages/gares/Gares.css +++ b/src/pages/gares/Gares.css @@ -1,206 +1,676 @@ -.gares-page { - background: #f3f7fc; +/* ═══════════════════════════════════════════════════════════════════════════ + GARES & HUBS VOYAGEURS - STYLES MODERNES & DESIGN SYSTEM SNCF + ═══════════════════════════════════════════════════════════════════════════ */ + +.gares-page-root { + min-height: 100vh; + background-color: #f4f7fb; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } -.gares-page-content { - width: min(1380px, 100%); +.gares-modern-container { + width: min(1420px, 100%); margin: 0 auto; + padding: 24px 28px 60px; + box-sizing: border-box; } -.gares-page-header { - background: linear-gradient(140deg, #082e4f 0%, #11507e 70%, #1d5f90 100%); - border-radius: 16px; +/* ═══════════════════════════════════════════════════════════════════════════ + 1. HERO BANNER + ═══════════════════════════════════════════════════════════════════════════ */ + +.gares-hero-banner { + background: linear-gradient(135deg, #001f3f 0%, #004080 50%, #0074d9 100%); + border-radius: 20px; color: #f8fbff; - padding: 24px; - margin-bottom: 14px; + padding: 32px 36px; + margin-bottom: 24px; display: flex; justify-content: space-between; - gap: 14px; - align-items: flex-start; - box-shadow: 0 14px 35px rgba(8, 46, 79, 0.22); + align-items: center; + gap: 24px; + box-shadow: 0 16px 36px -10px rgba(0, 64, 128, 0.35); + position: relative; + overflow: hidden; } -.gares-page-kicker { - margin: 0 0 8px; +.gares-hero-banner::after { + content: ''; + position: absolute; + top: -40px; + right: -40px; + width: 260px; + height: 260px; + background: radial-gradient(circle, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0) 70%); + border-radius: 50%; + pointer-events: none; +} + +.gares-hero-content { + max-width: 820px; + z-index: 1; +} + +.gares-hero-tags { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-bottom: 12px; +} + +.gares-kicker-pill { + display: inline-flex; + align-items: center; + gap: 6px; + background: rgba(255, 255, 255, 0.15); + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.2); + color: #fdf2f8; + font-size: 11px; + font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; - font-size: 12px; + padding: 5px 12px; + border-radius: 999px; } -.gares-page-header h2 { +.gares-hero-title { margin: 0; - font-size: 30px; - color: white; - + font-size: 32px; + font-weight: 800; + color: #ffffff; + letter-spacing: -0.02em; + line-height: 1.2; } -.gares-page-description { +.gares-hero-subtitle { margin: 10px 0 0; - color: rgba(247, 251, 255, 0.9); + font-size: 14px; + line-height: 1.55; + color: rgba(240, 248, 255, 0.9); } -.gares-header-actions { - display: inline-flex; - gap: 8px; -} - -.gares-header-import-btn { - --wcs-button-background-color: rgba(255, 255, 255, 0.16); - --wcs-button-color: #ffffff; - --wcs-button-border-color: rgba(255, 255, 255, 0.4); -} - -.gares-toolbar { - margin-bottom: 14px; - padding: 10px; - border-radius: 12px; - border: 1px solid #dbe3ed; - background: #ffffff; - box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05); +.gares-hero-actions { display: flex; - gap: 10px; + align-items: center; + gap: 12px; + z-index: 1; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + 2. KPI CARDS + ═══════════════════════════════════════════════════════════════════════════ */ + +.gares-kpi-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-bottom: 22px; +} + +.gares-kpi-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 16px; + padding: 18px 20px; + display: flex; + align-items: center; + gap: 16px; + cursor: pointer; + transition: all 0.22s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04); + position: relative; +} + +.gares-kpi-card:hover { + transform: translateY(-2px); + border-color: #cbd5e1; + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08); +} + +.gares-kpi-card.active { + border-color: #0074d9; + background: #f0f7ff; + box-shadow: 0 8px 24px rgba(0, 116, 217, 0.15); +} + +.gares-kpi-card.active::before { + content: ''; + position: absolute; + left: 0; + top: 14px; + bottom: 14px; + width: 4px; + background: #0074d9; + border-radius: 0 4px 4px 0; +} + +.gares-kpi-icon { + width: 48px; + height: 48px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + flex-shrink: 0; +} + +.kpi-blue { background: #e0f2fe; color: #0284c7; } +.kpi-purple { background: #f3e8ff; color: #9333ea; } +.kpi-emerald { background: #d1fae5; color: #059669; } +.kpi-amber { background: #fef3c7; color: #d97706; } + +.gares-kpi-info { + display: flex; + flex-direction: column; +} + +.gares-kpi-label { + font-size: 12px; + font-weight: 600; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.gares-kpi-value { + font-size: 26px; + font-weight: 800; + color: #0f172a; + line-height: 1.1; + margin-top: 2px; +} + +.gares-kpi-subtext { + font-size: 11px; + color: #94a3b8; + margin-top: 2px; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + 3. CONTROLS & FILTERS + ═══════════════════════════════════════════════════════════════════════════ */ + +.gares-controls-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 16px; + padding: 16px 20px; + margin-bottom: 20px; + box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04); + display: flex; + flex-direction: column; + gap: 14px; +} + +.gares-search-bar { + display: flex; + align-items: center; + gap: 12px; + background: #f8fafc; + border: 1px solid #cbd5e1; + border-radius: 12px; + padding: 0 16px; + height: 46px; + transition: all 0.2s ease; +} + +.gares-search-bar:focus-within { + background: #ffffff; + border-color: #0074d9; + box-shadow: 0 0 0 3px rgba(0, 116, 217, 0.15); +} + +.gares-modern-input { + width: 100%; + border: none; + background: transparent; + font-size: 14px; + color: #1e293b; + outline: none; + box-sizing: border-box; +} + +.gares-filters-row { + display: flex; + justify-content: space-between; align-items: center; flex-wrap: wrap; + gap: 14px; } -.gares-toolbar-input, -.gares-toolbar-select { - min-height: 40px; - border: 1px solid #d1dae6; +.gares-tab-group { + display: flex; + align-items: center; + background: #f1f5f9; + padding: 4px; border-radius: 10px; - padding: 0 12px; - font-size: 14px; - color: #1f2937; - background: #ffffff; + gap: 4px; + overflow-x: auto; } -.gares-toolbar-input { - min-width: 240px; +.gares-tab-btn { + display: inline-flex; + align-items: center; + gap: 6px; + background: transparent; + border: none; + color: #64748b; + font-size: 13px; + font-weight: 600; + padding: 7px 14px; + border-radius: 8px; + cursor: pointer; + transition: all 0.18s ease; + white-space: nowrap; +} + +.gares-tab-btn:hover { + color: #1e293b; + background: rgba(255, 255, 255, 0.6); +} + +.gares-tab-btn.active { + background: #ffffff; + color: #0f172a; + box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08); } .gares-toolbar-reset { - min-height: 40px; - border: 1px solid #dbe3ed; - border-radius: 10px; - background: #f8fbff; - color: #334155; - padding: 0 12px; + padding: 6px 14px; + border-radius: 8px; + border: 1px solid #cbd5e1; + background: #f8fafc; + color: #475569; + font-size: 12px; + font-weight: 600; cursor: pointer; + transition: all 0.15s ease; } -.gares-toolbar-count { - margin-left: auto; - border: 1px solid #dbe3ed; - border-radius: 999px; - padding: 6px 12px; - font-size: 13px; - color: #1f2937; - background: #f8fbff; +.gares-toolbar-reset:hover { + background: #f1f5f9; + color: #0f172a; + border-color: #94a3b8; } -.gares-page .table-container { - border: 1px solid #dbe3ed; +.view-mode-group { + display: flex; + background: #f1f5f9; + padding: 4px; + border-radius: 10px; + gap: 4px; +} + +.view-mode-btn { + border: none; + background: transparent; + color: #64748b; + padding: 6px 10px; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +.view-mode-btn.active { + background: #ffffff; + color: #0074d9; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + 4. CARDS GRID VIEW + ═══════════════════════════════════════════════════════════════════════════ */ + +.gares-cards-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: 18px; +} + +.gare-card-modern { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 16px; + padding: 20px; + display: flex; + flex-direction: column; + justify-content: space-between; + box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04); + transition: all 0.22s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; +} + +.gare-card-modern:hover { + transform: translateY(-3px); + border-color: #cbd5e1; + box-shadow: 0 12px 28px rgba(15, 23, 42, 0.08); +} + +.gare-card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.gare-card-title-box { + display: flex; + align-items: center; + gap: 12px; +} + +.gare-icon-avatar { + width: 42px; + height: 42px; border-radius: 12px; - box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06); + background: #e0f2fe; + color: #0369a1; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + flex-shrink: 0; } -.gares-page .wcs-table th { - background: #f8fbff; +.gare-card-name { + margin: 0 0 4px; + font-size: 16px; + font-weight: 700; + color: #0f172a; + line-height: 1.25; +} + +.gare-type-badge { + display: inline-block; + font-size: 11px; + font-weight: 700; + padding: 3px 8px; + border-radius: 6px; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.badge-ville { background: #dbeafe; color: #1e40af; } +.badge-interurbaine { background: #fef3c7; color: #92400e; } + +.gare-card-details { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 16px; +} + +.gare-info-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: #475569; +} + +.gare-info-item wcs-mat-icon { + color: #94a3b8; + font-size: 16px; +} + +.gare-transports-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 4px; +} + +.transport-chip-modern { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + font-weight: 700; + padding: 3px 8px; + border-radius: 6px; + color: #ffffff; +} + +.transport-chip-modern.bus { background: #16a34a; } +.transport-chip-modern.tram { background: #9333ea; } +.transport-chip-modern.metro { background: #0284c7; } +.transport-chip-modern.tram-train { background: #ea580c; } +.transport-chip-modern.train { background: #475569; } + +.gare-services-icons { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + padding-top: 10px; + border-top: 1px dashed #e2e8f0; +} + +.service-icon-pill { + width: 30px; + height: 30px; + border-radius: 8px; + background: #f1f5f9; + color: #64748b; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + transition: all 0.15s ease; +} + +.service-icon-pill.active { + background: #e0f2fe; + color: #0284c7; +} + +.gare-card-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + padding-top: 14px; + border-top: 1px solid #f1f5f9; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + 5. TABLE VIEW + ═══════════════════════════════════════════════════════════════════════════ */ + +.gares-table-wrapper { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 4px 18px rgba(15, 23, 42, 0.05); +} + +.gares-modern-table { + width: 100%; + border-collapse: collapse; + text-align: left; +} + +.gares-modern-table thead { + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; +} + +.gares-modern-table th { + padding: 14px 16px; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + color: #64748b; + letter-spacing: 0.04em; +} + +.gares-modern-table td { + padding: 14px 16px; + border-bottom: 1px solid #f1f5f9; + font-size: 13px; + color: #334155; + vertical-align: middle; +} + +.gares-modern-table tbody tr:hover { + background-color: #f8fafc; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + 6. MODALS + ═══════════════════════════════════════════════════════════════════════════ */ + +.gares-modal-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(4px); + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.gares-modal-dialog { + background: #ffffff; + border-radius: 20px; + width: min(920px, 100%); + max-height: 90vh; + display: flex; + flex-direction: column; + box-shadow: 0 25px 60px -12px rgba(15, 23, 42, 0.35); + overflow: hidden; +} + +.gares-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 18px 24px; + border-bottom: 1px solid #e2e8f0; + background: #f8fafc; +} + +.gares-modal-header h3 { + margin: 0; + font-size: 18px; + font-weight: 800; color: #0f172a; } -.gares-page .wcs-table tr:hover { - background: #f7fbff; +.modal-close { + width: 32px; + height: 32px; + border-radius: 8px; + border: 1px solid #cbd5e1; + background: #ffffff; + color: #64748b; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; } -.gares-empty-icon { - font-size: 48px; -} - -.gares-action-btn { - min-width: 36px; -} - -.gares-modal { - max-width: 980px; - border: 1px solid #dbe3ed; - border-radius: 14px; - box-shadow: 0 20px 50px rgba(15, 23, 42, 0.25); -} - -.gares-modal-body { - padding: 0; -} - -.gares-modal-alert { - margin: 12px 20px 0; +.modal-close:hover { + background: #f1f5f9; + color: #0f172a; } .gares-modal-tabs { display: flex; - border-bottom: 1px solid #e5edf7; - padding: 0 20px; - gap: 4px; + background: #f1f5f9; + padding: 6px; + border-radius: 12px; + margin: 16px 24px 0; + gap: 6px; + border: 1px solid #e2e8f0; + overflow-x: auto; } -.gares-modal-tab { +.gares-modal-tab-btn { + flex: 1; display: inline-flex; align-items: center; - gap: 6px; - padding: 12px 16px; - border: 0; + justify-content: center; + gap: 8px; + padding: 10px 16px; + border: none; background: transparent; - color: #6b7280; + color: #64748b; + font-size: 13px; + font-weight: 700; + border-radius: 8px; cursor: pointer; - font-size: 14px; - font-weight: 500; - border-bottom: 2px solid transparent; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + white-space: nowrap; } -.gares-modal-tab.active { - color: #0b4a7a; - border-bottom-color: #0b4a7a; +.gares-modal-tab-btn:hover { + color: #0f172a; + background: rgba(255, 255, 255, 0.6); } -.gares-modal-content { - padding: 20px 24px; +.gares-modal-tab-btn.active { + background: #ffffff; + color: #0074d9; + box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08); } -.gares-form-group-spaced { - margin-bottom: 16px; +.gares-modal-tab-btn wcs-mat-icon { + font-size: 18px; } -.gares-form-group-spaced-lg { - margin-bottom: 20px; +.gares-modal-body { + padding: 24px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 20px; +} + +.gares-modal-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 24px; + border-top: 1px solid #e2e8f0; + background: #f8fafc; +} + +/* Form controls */ +.form-group { + display: flex; + flex-direction: column; + gap: 6px; } .gares-form-label { - display: block; - font-weight: 600; font-size: 13px; - color: #374151; - margin-bottom: 6px; -} - -.gares-transports-select { - width: 420px; -} - -.gares-info-layout { - display: flex; - flex-direction: column; - gap: 28px; + font-weight: 700; + color: #334155; + margin-bottom: 2px; } .gares-info-section-title { - font-size: 14px; - font-weight: 700; - color: #1f2937; margin: 0 0 12px; + font-size: 14px; + font-weight: 800; + color: #0f172a; + display: flex; + align-items: center; + gap: 8px; padding-bottom: 8px; - border-bottom: 1px solid #e5e7eb; - text-transform: uppercase; - letter-spacing: 0.05em; + border-bottom: 1px solid #e2e8f0; } .gares-info-grid { @@ -210,270 +680,448 @@ } .gares-info-grid.compact { - gap: 8px; + grid-template-columns: 1fr 1fr; + gap: 10px; } .gares-col-full { grid-column: 1 / -1; } -.gares-text-input { - width: 100%; - padding: 8px 10px; - border: 1px solid #d1d5db; - border-radius: 8px; - font-size: 14px; - box-sizing: border-box; -} - +/* Bool toggle field */ .gares-bool-field { display: flex; align-items: center; justify-content: space-between; - padding: 8px 12px; - background: #f9fafb; - border-radius: 8px; - border: 1px solid #e5e7eb; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 10px; + padding: 10px 14px; + gap: 12px; } .gares-bool-label { font-size: 13px; - color: #374151; + font-weight: 600; + color: #334155; } .gares-bool-actions { - display: inline-flex; - gap: 6px; + display: flex; + background: #e2e8f0; + padding: 2px; + border-radius: 8px; + gap: 2px; } .gares-bool-btn { - padding: 4px 12px; - border-radius: 6px; + display: inline-flex; + align-items: center; + gap: 4px; + padding: 5px 12px; border: none; - cursor: pointer; + background: transparent; + color: #64748b; font-size: 12px; - font-weight: 600; - background: #e5e7eb; - color: #6b7280; + font-weight: 700; + border-radius: 6px; + cursor: pointer; + transition: all 0.15s ease; } .gares-bool-btn.is-yes { background: #16a34a; - color: #fff; + color: #ffffff; + box-shadow: 0 2px 4px rgba(22, 163, 74, 0.2); } .gares-bool-btn.is-no { - background: #dc2626; - color: #fff; + background: #e11d48; + color: #ffffff; + box-shadow: 0 2px 4px rgba(225, 29, 72, 0.2); } -.gares-transport-extended-list { +/* Quai list */ +.quais-list-modern { display: flex; flex-direction: column; - gap: 20px; + gap: 10px; + margin-top: 6px; } -.gares-extended-header { +.quai-row-modern { display: flex; align-items: center; - justify-content: space-between; - margin-bottom: 8px; -} - -.gares-inline-label { - margin-bottom: 0; -} - -.gares-extended-count { - margin-left: 8px; - background: #0088ce; - color: #fff; - border-radius: 10px; - padding: 1px 8px; - font-size: 11px; - font-weight: 700; -} - -.gares-extended-add-btn { - background: #0088ce; - color: #fff; - border: none; - border-radius: 8px; - padding: 6px 12px; - cursor: pointer; - font-size: 13px; -} - -.gares-extended-empty { + gap: 12px; + background: #f8fafc; padding: 10px 14px; - background: #f9fafb; - border-radius: 8px; - border: 1px dashed #d1d5db; - color: #9ca3af; - font-size: 13px; -} - -.gares-extended-items { - display: flex; - flex-direction: column; - gap: 8px; -} - -.gares-extended-row { - display: grid; - grid-template-columns: 1fr 1fr 1fr auto; - gap: 8px; - align-items: center; - background: #f9fafb; - padding: 10px 12px; border-radius: 10px; - border: 1px solid #e5e7eb; + border: 1px solid #e2e8f0; } -.gares-mini-label { - font-size: 11px; - color: #6b7280; - display: block; - margin-bottom: 3px; -} - -.gares-extended-remove-btn { - background: #dc2626; - color: #fff; - border: none; - border-radius: 6px; - width: 32px; - height: 32px; - cursor: pointer; - font-size: 16px; -} - -@media (max-width: 900px) { - .gares-page-header { - flex-direction: column; - } - - .gares-header-actions { - width: 100%; - } - - .gares-transports-select { - width: 100%; - } - - .gares-info-grid, - .gares-extended-row { - grid-template-columns: 1fr; - } -} - -/* Styling for Hall Management System */ -.halls-list { - display: flex; - flex-direction: column; - gap: 16px; - margin-top: 10px; -} - -.hall-card { - background: #ffffff; - border: 1px solid #dbe3ed; +.hall-card-modern { + background: #f8fafc; + border: 1px solid #e2e8f0; border-radius: 12px; - padding: 16px; - box-shadow: 0 4px 12px rgba(15, 23, 42, 0.03); + padding: 14px 16px; display: flex; flex-direction: column; - gap: 12px; + gap: 10px; } -.hall-header-row { - display: flex; - align-items: center; - gap: 12px; -} - -.hall-name-input { - flex: 1; - font-weight: 600; - color: #0b4a7a; -} - -.delete-hall-btn-raw { - background: #fee2e2; - color: #b91c1c; - border: 1px solid #fca5a5; - border-radius: 8px; - padding: 6px 12px; - cursor: pointer; - font-size: 13px; - font-weight: 500; - transition: all 0.2s ease; -} - -.delete-hall-btn-raw:hover { - background: #fca5a5; - color: #991b1b; -} - -.hall-quais-container { - display: flex; - flex-direction: column; - gap: 8px; - padding-top: 8px; - border-top: 1px dashed #e5edf7; -} - -.hall-quais-title { - font-size: 12px; - font-weight: 600; - color: #6b7280; -} - -.hall-quais-chips { +.hall-chips-container { display: flex; flex-wrap: wrap; - gap: 8px; + gap: 6px; } -.quai-chip-btn { - background: #f8fbff; - border: 1px solid #dbe3ed; - color: #475569; +.quai-select-chip { + padding: 5px 12px; border-radius: 8px; - padding: 6px 14px; - font-size: 13px; + border: 1px solid #cbd5e1; + background: #ffffff; + color: #334155; + font-size: 12px; + font-weight: 600; cursor: pointer; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - font-weight: 500; + transition: all 0.15s ease; } -.quai-chip-btn:hover { - background: #f1f5f9; - border-color: #cbd5e1; +.quai-select-chip:hover { + border-color: #0074d9; + color: #0074d9; } -.quai-chip-btn.selected { - background: #0b4a7a; - border-color: #0b4a7a; +.quai-select-chip.selected { + background: #0074d9; color: #ffffff; - box-shadow: 0 4px 10px rgba(11, 74, 122, 0.25); + border-color: #0074d9; + box-shadow: 0 2px 6px rgba(0, 116, 217, 0.25); } -.quai-chip-btn.selected:hover { - background: #08375b; - border-color: #08375b; +/* Responsive */ +@media (max-width: 1024px) { + .gares-modern-container { + padding: 20px 18px 48px; + } + .gares-kpi-grid { + grid-template-columns: repeat(2, 1fr); + } + .gares-hero-banner { + padding: 26px 28px; + } + .gares-cards-grid { + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + } } -.no-quais-hint { - font-size: 13px; +@media (max-width: 768px) { + .gares-modern-container { + padding: 16px 12px 40px; + } + .gares-hero-banner { + flex-direction: column; + align-items: flex-start; + padding: 20px 18px; + gap: 16px; + } + .gares-hero-actions { + width: 100%; + display: flex; + gap: 8px; + } + .gares-hero-actions > * { + flex: 1; + justify-content: center; + } + .gares-kpi-grid { + grid-template-columns: 1fr; + gap: 12px; + } + .gares-controls-card { + padding: 14px; + } + .gares-filters-row { + flex-direction: column; + align-items: stretch; + gap: 10px; + } + .gares-filter-group { + width: 100%; + } + .gares-cards-grid { + grid-template-columns: 1fr; + } + .gares-info-grid, + .gares-info-grid.compact { + grid-template-columns: 1fr; + } + .gares-modal-dialog { + width: 95vw; + max-height: 95vh; + } + .gares-modal-header { + padding: 14px 18px; + } + .gares-modal-body { + padding: 16px 18px; + } + .gares-modal-tabs { + margin: 12px 16px 0; + } +} + +@media (max-width: 480px) { + .gares-hero-title { + font-size: 24px; + } + .gares-hero-subtitle { + font-size: 13px; + } + .gare-card-header { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } +} + +/* ═══════════════════════════════════════════════════════════════════════════ + 7. DARK MODE SUPPORT + ═══════════════════════════════════════════════════════════════════════════ */ + +body.dark-mode .gares-page-root, +[data-theme="dark"] .gares-page-root { + background-color: #0b111e; + color: #f1f5f9; +} + +body.dark-mode .gares-hero-banner, +[data-theme="dark"] .gares-hero-banner { + background: linear-gradient(135deg, #05162a 0%, #082d4d 50%, #0d4b78 100%); + border-color: rgba(255, 255, 255, 0.1); + box-shadow: 0 16px 36px -10px rgba(0, 0, 0, 0.6); +} + +body.dark-mode .gares-kpi-card, +[data-theme="dark"] .gares-kpi-card { + background: #151f30; + border-color: #243247; + color: #f1f5f9; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); +} + +body.dark-mode .gares-kpi-card:hover, +[data-theme="dark"] .gares-kpi-card:hover { + border-color: #3b82f6; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); +} + +body.dark-mode .gares-kpi-card.active, +[data-theme="dark"] .gares-kpi-card.active { + background: #0d2744; + border-color: #0088ce; + box-shadow: 0 8px 24px rgba(0, 136, 206, 0.25); +} + +body.dark-mode .gares-kpi-label, +[data-theme="dark"] .gares-kpi-label { color: #94a3b8; - font-style: italic; } -.add-hall-btn { - margin-top: 4px; - align-self: flex-start; +body.dark-mode .gares-kpi-value, +[data-theme="dark"] .gares-kpi-value { + color: #f8fafc; } +body.dark-mode .gares-kpi-subtext, +[data-theme="dark"] .gares-kpi-subtext { + color: #64748b; +} +body.dark-mode .kpi-blue, +[data-theme="dark"] .kpi-blue { + background: rgba(2, 132, 199, 0.2); + color: #38bdf8; +} + +body.dark-mode .kpi-emerald, +[data-theme="dark"] .kpi-emerald { + background: rgba(22, 163, 74, 0.2); + color: #4ade80; +} + +body.dark-mode .kpi-amber, +[data-theme="dark"] .kpi-amber { + background: rgba(217, 119, 6, 0.2); + color: #fbbf24; +} + +body.dark-mode .kpi-purple, +[data-theme="dark"] .kpi-purple { + background: rgba(147, 51, 234, 0.2); + color: #c084fc; +} + +body.dark-mode .gares-controls-card, +body.dark-mode .gare-card-modern, +body.dark-mode .gares-table-wrapper, +body.dark-mode .gares-modal-dialog, +body.dark-mode .gares-section-box, +[data-theme="dark"] .gares-controls-card, +[data-theme="dark"] .gare-card-modern, +[data-theme="dark"] .gares-table-wrapper, +[data-theme="dark"] .gares-modal-dialog, +[data-theme="dark"] .gares-section-box { + background: #151f30; + border-color: #243247; + color: #f1f5f9; + box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35); +} + +body.dark-mode .gare-card-modern:hover, +[data-theme="dark"] .gare-card-modern:hover { + border-color: #3b82f6; + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.45); +} + +body.dark-mode .gare-name-title, +[data-theme="dark"] .gare-name-title { + color: #f8fafc; +} + +body.dark-mode .gares-modern-table thead, +[data-theme="dark"] .gares-modern-table thead { + background: #0f172a; + border-bottom-color: #243247; +} + +body.dark-mode .gares-modern-table th, +[data-theme="dark"] .gares-modern-table th { + color: #94a3b8; +} + +body.dark-mode .gares-modern-table td, +[data-theme="dark"] .gares-modern-table td { + border-bottom-color: #1e293b; + color: #e2e8f0; +} + +body.dark-mode .gares-modern-table tbody tr:hover, +[data-theme="dark"] .gares-modern-table tbody tr:hover { + background-color: #1a273c; +} + +body.dark-mode .gares-modern-input, +body.dark-mode .gares-modern-select, +body.dark-mode .gares-form-input, +body.dark-mode .gares-form-select, +body.dark-mode .gares-form-textarea, +[data-theme="dark"] .gares-modern-input, +[data-theme="dark"] .gares-modern-select, +[data-theme="dark"] .gares-form-input, +[data-theme="dark"] .gares-form-select, +[data-theme="dark"] .gares-form-textarea { + background-color: #0b111e; + border-color: #243247; + color: #f8fafc; +} + +body.dark-mode .gares-modern-input:focus, +body.dark-mode .gares-modern-select:focus, +body.dark-mode .gares-form-input:focus, +body.dark-mode .gares-form-select:focus, +[data-theme="dark"] .gares-modern-input:focus, +[data-theme="dark"] .gares-modern-select:focus, +[data-theme="dark"] .gares-form-input:focus, +[data-theme="dark"] .gares-form-select:focus { + border-color: #0088ce; + box-shadow: 0 0 0 3px rgba(0, 136, 206, 0.25); +} + +body.dark-mode .gares-modal-header, +body.dark-mode .gares-modal-footer, +[data-theme="dark"] .gares-modal-header, +[data-theme="dark"] .gares-modal-footer { + background: #0e1726; + border-color: #243247; +} + +body.dark-mode .gares-modal-header h3, +[data-theme="dark"] .gares-modal-header h3 { + color: #f8fafc; +} + +body.dark-mode .gares-modal-tabs, +[data-theme="dark"] .gares-modal-tabs { + background: #0e1726; + border-color: #243247; +} + +body.dark-mode .gares-modal-tab-btn, +[data-theme="dark"] .gares-modal-tab-btn { + color: #94a3b8; +} + +body.dark-mode .gares-modal-tab-btn.active, +[data-theme="dark"] .gares-modal-tab-btn.active { + background: #1e293b; + color: #38bdf8; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +body.dark-mode .quai-select-chip, +[data-theme="dark"] .quai-select-chip { + background: #0b111e; + border-color: #243247; + color: #cbd5e1; +} + +body.dark-mode .quai-select-chip:hover, +[data-theme="dark"] .quai-select-chip:hover { + border-color: #38bdf8; + color: #38bdf8; +} + +body.dark-mode .quai-select-chip.selected, +[data-theme="dark"] .quai-select-chip.selected { + background: #0284c7; + color: #ffffff; + border-color: #0284c7; +} + +body.dark-mode .service-icon-pill, +[data-theme="dark"] .service-icon-pill { + background: #1e293b; + color: #94a3b8; +} + +body.dark-mode .service-icon-pill.active, +[data-theme="dark"] .service-icon-pill.active { + background: rgba(2, 132, 199, 0.25); + color: #38bdf8; +} + +body.dark-mode .modal-close, +[data-theme="dark"] .modal-close { + background: #1e293b; + border-color: #334155; + color: #94a3b8; +} + +body.dark-mode .modal-close:hover, +[data-theme="dark"] .modal-close:hover { + background: #334155; + color: #f8fafc; +} + +body.dark-mode .gares-empty-state, +[data-theme="dark"] .gares-empty-state { + background: #151f30; + border-color: #243247; +} + +body.dark-mode .gares-empty-text, +[data-theme="dark"] .gares-empty-text { + color: #94a3b8; +} diff --git a/src/pages/gares/Gares.js b/src/pages/gares/Gares.js index 7c6752d..c5ff48a 100644 --- a/src/pages/gares/Gares.js +++ b/src/pages/gares/Gares.js @@ -1,4 +1,5 @@ import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react'; +import NavPanel from '../../components/nav-panel/NavPanel'; import { isSupabaseConfigured } from '../../supabaseClient'; import { getAllGares, createGare, updateGare, deleteGare, mergeGares } from '../../api/gares'; import { getHorairesCountByQuai } from '../../api/horaires'; @@ -6,15 +7,15 @@ import { canPerform, getCurrentUser } from '../../utils/userUtils'; import './Gares.css'; // ============================================================================ -// CONSTANTES +// CONSTANTES & CONFIGURATION // ============================================================================ -const TRANSPORT_COLORS = { - bus: '#8BC34A', - tram: '#9C27B0', - metro: '#2196F3', - 'tram-train': '#FF9800', - train: null +const TRANSPORT_CONFIG = { + bus: { label: 'Bus', icon: 'directions_bus', className: 'bus' }, + tram: { label: 'Tram', icon: 'tram', className: 'tram' }, + metro: { label: 'Métro', icon: 'subway', className: 'metro' }, + 'tram-train': { label: 'Tram-train', icon: 'commute', className: 'tram-train' }, + train: { label: 'Train', icon: 'train', className: 'train' } }; const TRANSPORT_OPTIONS = ['bus', 'tram', 'metro', 'tram-train', 'train']; @@ -75,24 +76,30 @@ const INITIAL_FORM_STATE = { quais_hall: [] }; -// Composant toggle Oui/Non déclaré au niveau supérieur pour éviter de le créer lors du render +// Composant toggle Oui/Non moderne pour les services function BoolField({ label, value, onChange }) { return ( -
- {label} -
- - -
+
+ {label} +
+ +
+
); } @@ -110,11 +117,13 @@ export default function Gares() { const [gares, setGares] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); - // Filtrage + + // Filtrage et vue const [searchFilter, setSearchFilter] = useState(''); const [typeFilter, setTypeFilter] = useState(''); + const [viewMode, setViewMode] = useState('grid'); // 'grid' | 'table' - // État du formulaire + // État du formulaire modal const [showModal, setShowModal] = useState(false); const [editingGare, setEditingGare] = useState(null); const [formData, setFormData] = useState(INITIAL_FORM_STATE); @@ -122,7 +131,7 @@ export default function Gares() { const [saving, setSaving] = useState(false); const [activeTab, setActiveTab] = useState('general'); - // État de suppression et fusion + // Suppression et fusion const [deleteConfirmId, setDeleteConfirmId] = useState(null); const [quaisCounts, setQuaisCounts] = useState({}); const [showMergeModal, setShowMergeModal] = useState(false); @@ -172,10 +181,9 @@ export default function Gares() { const targetQuai = prev.quais.find(q => String(q.clientId) === String(quaiId)); const oldQuaiName = targetQuai?.nom; const newQuais = prev.quais.map(q => - String(q.clientId) === String(quaiId) ? { ...q, [field]: value } : q + String(q.clientId) === String(quaiId) ? { ...q, [field]: value } : q ); - // Si le nom du quai a changé, on met également à jour son nom dans quais_hall let newQuaisHall = prev.quais_hall || []; if (field === 'nom' && oldQuaiName && oldQuaiName !== value) { newQuaisHall = newQuaisHall.map(h => { @@ -301,7 +309,6 @@ export default function Gares() { }; }, [showModal, formData.quais.length, updateField, updateQuai]); - // ============================================================================ // CHARGEMENT DES DONNÉES // ============================================================================ @@ -324,18 +331,30 @@ export default function Gares() { }, []); useEffect(() => { - // Avoid calling setState synchronously inside an effect: defer execution to next tick const id = setTimeout(() => { loadGares(); }, 0); return () => clearTimeout(id); }, [loadGares]); + // ============================================================================ + // STATISTIQUES GLOBALES + // ============================================================================ + + const stats = useMemo(() => { + const total = gares.length; + const villeCount = gares.filter(g => g.type === 'ville').length; + const interurbaineCount = gares.filter(g => g.type === 'interurbaine').length; + const totalQuais = gares.reduce((sum, g) => sum + (g.gare_quais?.length || 0), 0); + + return { total, villeCount, interurbaineCount, totalQuais }; + }, [gares]); + // ============================================================================ // GESTION DU FORMULAIRE // ============================================================================ const openCreateModal = () => { if (!canCreateGares) { - setFormError('Vous n\'avez pas les droits pour créer une gare.'); + setFormError("Vous n'avez pas les droits pour créer une gare."); return; } setEditingGare(null); @@ -349,10 +368,10 @@ export default function Gares() { const openEditModal = async (gare) => { if (!canUpdateGares) { - setFormError('Vous n\'avez pas les droits pour modifier une gare.'); + setFormError("Vous n'avez pas les droits pour modifier une gare."); return; } - + try { const { data } = await getHorairesCountByQuai(gare.id, gare.nom); setQuaisCounts(data || {}); @@ -365,17 +384,17 @@ export default function Gares() { nom: gare.nom || '', type: gare.type || 'ville', quais: gare.gare_quais?.length > 0 - ? gare.gare_quais.map(q => ({ - clientId: q.id || `quai-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`, - id: q.id, - nom: q.nom || '', - distance_metres: q.distance_metres || 0 - })) - : [createQuaiRow()], + ? gare.gare_quais.map(q => ({ + clientId: q.id || `quai-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`, + id: q.id, + nom: q.nom || '', + distance_metres: q.distance_metres || 0 + })) + : [createQuaiRow()], transports: gare.gare_transports?.map(t => t.type) || [], informations: gare.informations - ? { ...INITIAL_INFORMATIONS_STATE, ...gare.informations } - : { ...INITIAL_INFORMATIONS_STATE }, + ? { ...INITIAL_INFORMATIONS_STATE, ...gare.informations } + : { ...INITIAL_INFORMATIONS_STATE }, quais_hall: (() => { const rawHalls = gare.quais_hall?.hall || {}; return Object.keys(rawHalls).map((name, idx) => ({ @@ -389,12 +408,12 @@ export default function Gares() { setActiveTab('general'); setShowModal(true); const initialQuais = gare.gare_quais?.length > 0 - ? gare.gare_quais.map(q => ({ - id: q.id, - nom: q.nom || '', - distance_metres: q.distance_metres || 0 - })) - : []; + ? gare.gare_quais.map(q => ({ + id: q.id, + nom: q.nom || '', + distance_metres: q.distance_metres || 0 + })) + : []; originalTransportsRef.current = gare.gare_transports?.map(t => t.type) || []; originalQuaisRef.current = initialQuais; }; @@ -423,7 +442,6 @@ export default function Gares() { setFormData(prev => { const newQuais = prev.quais.filter(q => String(q.clientId) !== String(quaiId)); - // Retirer le quai de tous les halls let newQuaisHall = prev.quais_hall || []; if (quaiName) { newQuaisHall = newQuaisHall.map(h => { @@ -446,7 +464,7 @@ export default function Gares() { }); }; - // Gestion des halls de la gare + // Gestion des halls const addHall = () => { setFormData(prev => { const currentHalls = prev.quais_hall || []; @@ -529,7 +547,7 @@ export default function Gares() { informations: { ...prev.informations, [type]: (prev.informations[type] || []).map((l, i) => - i === index ? { ...l, [field]: value } : l + i === index ? { ...l, [field]: value } : l ) } })); @@ -551,11 +569,11 @@ export default function Gares() { const handleSave = async () => { if (!editingGare && !canCreateGares) { - setFormError('Vous n\'avez pas les droits pour créer une gare.'); + setFormError("Vous n'avez pas les droits pour créer une gare."); return; } if (editingGare && !canUpdateGares) { - setFormError('Vous n\'avez pas les droits pour modifier une gare.'); + setFormError("Vous n'avez pas les droits pour modifier une gare."); return; } setFormError(''); @@ -575,7 +593,6 @@ export default function Gares() { setSaving(true); - // Formater quais_hall au format BDD {"hall": {"Nom du Hall": [...]}} const formattedQuaisHall = { hall: {} }; if (Array.isArray(source.quais_hall)) { source.quais_hall.forEach(h => { @@ -585,7 +602,6 @@ export default function Gares() { }); } - // Préparer les données const dataToSave = { nom: nom, type: source.type, @@ -601,7 +617,7 @@ export default function Gares() { if (!areStringArraysEqual(currentTransports, originalTransportsRef.current || [])) { dataToSave.transports = currentTransports; } - + const currentQuais = source.quais.map(q => ({ id: q.id, nom: q.nom || '', @@ -612,10 +628,8 @@ export default function Gares() { } } - try { let result; - if (editingGare) { result = await updateGare(editingGare.id, dataToSave); } else { @@ -629,24 +643,18 @@ export default function Gares() { return; } - - // Mettre à jour l'état local immédiatement avec la gare retournée const savedGare = result.data; setGares(prev => { if (!prev) return [savedGare]; if (editingGare) { return prev.map(g => (g.id === savedGare.id ? savedGare : g)); } - // création : insérer en tête return [savedGare, ...prev]; }); originalTransportsRef.current = null; originalQuaisRef.current = null; - // Succès closeModal(); - // refresh légère si souhaité : loadGares(); - } catch (err) { console.error('Exception sauvegarde:', err); setFormError('Erreur inattendue lors de la sauvegarde'); @@ -656,12 +664,12 @@ export default function Gares() { }; // ============================================================================ - // SUPPRESSION + // SUPPRESSION & FUSION // ============================================================================ const handleDelete = async (gareId) => { if (!canDeleteGares) { - alert('Vous n\'avez pas les droits pour supprimer une gare.'); + alert("Vous n'avez pas les droits pour supprimer une gare."); return; } const { error: deleteError } = await deleteGare(gareId); @@ -699,27 +707,14 @@ export default function Gares() { return type === 'interurbaine' ? '12 heures' : '30 minutes'; }; - const renderTransportBadge = (transport) => { - const type = transport.type; - const key = transport.id || `${type}-${transport.ligne || 'default'}`; - - if (type === 'train') { - return ( - - 🚆 - - ); - } + const renderTransportChip = (transport, idx) => { + const type = typeof transport === 'string' ? transport : transport.type; + const config = TRANSPORT_CONFIG[type] || { label: type, icon: 'commute', className: 'train' }; return ( - - {type.charAt(0).toUpperCase()} - {transport.ligne && {transport.ligne}} + + + {config.label} ); }; @@ -728,558 +723,864 @@ export default function Gares() { // Gares filtrées const filteredGares = useMemo(() => ( - gares.filter(g => { - const matchSearch = !searchFilter || (g.nom || '').toLowerCase().includes(searchFilter.toLowerCase()); - const matchType = !typeFilter || g.type === typeFilter; - return matchSearch && matchType; - }) + gares.filter(g => { + const matchSearch = !searchFilter || (g.nom || '').toLowerCase().includes(searchFilter.toLowerCase()); + const matchType = !typeFilter || g.type === typeFilter; + return matchSearch && matchType; + }) ), [gares, searchFilter, typeFilter]); - const hasSearch = searchFilter.trim().length > 0; - const displayedGares = useMemo(() => ( - hasSearch ? filteredGares : filteredGares.slice(0, 10) - ), [hasSearch, filteredGares]); - const hiddenByDefault = hasSearch ? 0 : Math.max(0, filteredGares.length - displayedGares.length); - - // ============================================================================ - // RENDU - // ============================================================================ - return ( -
-
-
-
-

Exploitation

-

Gestion des gares

-

Administrez les gares, quais, transports et informations voyageurs.

-
-
- - - Nouvelle gare - - { window.location.assign('/gares/import'); }}> - - Import GTFS - -
-
+
+
+
+ +
+
- {/* Erreur globale */} - {error && ( - - Erreur - {error} - - )} - - {/* Barre de filtrage */} -
- setSearchFilter(e.target.value)} - /> - - {(searchFilter || typeFilter) && ( - - )} - {displayedGares.length} / {filteredGares.length} gare(s) + + Nouvelle gare + + { window.location.assign('/gares/import'); }} + > + + Import GTFS + +
+ + + {/* Erreur globale */} + {error && ( + + Erreur + {error} + + )} + + {/* 2. KPI STAT CARDS */} +
+
setTypeFilter('')} + > +
+ +
+
+ Total Gares + {stats.total} + Toutes les gares du réseau +
- {hiddenByDefault > 0 && ( -

- {hiddenByDefault} gares supplementaires masquees. Utilisez la recherche pour les retrouver. -

- )} +
setTypeFilter(typeFilter === 'ville' ? '' : 'ville')} + > +
+ +
+
+ Gares de Ville + {stats.villeCount} + Affichage quai : 30 min +
+
- {/* Contenu */} - {loading ? ( -
- -

Chargement des gares...

+
setTypeFilter(typeFilter === 'interurbaine' ? '' : 'interurbaine')} + > +
+ +
+
+ Interurbaines + {stats.interurbaineCount} + Affichage quai : 12 heures +
+
+ +
+
+ +
+
+ Quais Actifs + {stats.totalQuais} + Voies réparties +
+
+
+ + {/* 3. CONTROLS & FILTERS */} +
+
+ + setSearchFilter(e.target.value)} + /> + {searchFilter && ( + + )} +
+ +
+
+ + + +
+ +
+ {(searchFilter || typeFilter) && ( + + )} + +
+ +
- ) : filteredGares.length === 0 ? ( -
- -

Aucune gare enregistrée

- - Créer une gare - -
- ) : ( -
- - - - - - - - - - - - - {displayedGares.map(gare => ( - - - + + + + + + ))} + +
NomTypeQuaisTransportsTemps affichageActions
{gare.nom} - + + + + + {/* 4. CONTENU PRINCIPAL */} + {loading ? ( +
+ +

Chargement des gares...

+
+ ) : filteredGares.length === 0 ? ( +
+ +

Aucune gare trouvée

+

+ {searchFilter || typeFilter ? 'Aucun résultat ne correspond à vos filtres.' : 'Commencez par ajouter votre première gare au réseau.'} +

+ {canCreateGares && ( + + + Créer une gare + + )} +
+ ) : viewMode === 'grid' ? ( + /* 4A. GRID VIEW */ +
+ {filteredGares.map(gare => { + const info = gare.informations || {}; + const activeServices = [ + { key: 'wifi_gratuit', icon: 'wifi', title: 'WiFi Gratuit' }, + { key: 'service_assistance', icon: 'accessible', title: 'Assistance PMR' }, + { key: 'espace_attente', icon: 'weekend', title: 'Espace d\'attente' }, + { key: 'toilettes', icon: 'wc', title: 'Toilettes' }, + { key: 'parking', icon: 'local_parking', title: 'Parking' }, + { key: 'velo_libre_service', icon: 'pedal_bike', title: 'Vélos en libre service' }, + { key: 'defibrillateur', icon: 'favorite', title: 'Défibrillateur' } + ].filter(s => info[s.key] === true); + + return ( +
+
+
+
+
+ +
+
+

{gare.nom}

+ + {gare.type === 'interurbaine' ? 'Interurbaine' : 'Gare de ville'} + +
+
+
+ +
+
+ + {gare.gare_quais?.length || 0} quai(s) configuré(s) +
+
+ + Affichage quai : {getDisplayTime(gare.type)} +
+ + {gare.gare_transports && gare.gare_transports.length > 0 && ( +
+ {gare.gare_transports.map((t, i) => renderTransportChip(t, i))} +
+ )} +
+ + {activeServices.length > 0 && ( +
+ {activeServices.map(s => ( +
+ +
+ ))} +
+ )} +
+ +
+ { setGareToMerge(gare); setShowMergeModal(true); }} + disabled={!canUpdateGares} + title="Fusionner avec une autre gare" + > + + + openEditModal(gare)} + disabled={!canUpdateGares} + title="Modifier la gare" + > + + + setDeleteConfirmId(gare.id)} + disabled={!canDeleteGares} + title="Supprimer la gare" + > + + +
+
+ ); + })} +
+ ) : ( + /* 4B. TABLE VIEW */ +
+ + + + + + + + + + + + + {filteredGares.map(gare => ( + + + - - - - - - ))} - -
GareTypeQuaisTransportsDélai affichageActions
+
+
+ +
+ {gare.nom} +
+
+ {gare.type === 'interurbaine' ? 'Interurbaine' : 'Ville'} - {gare.gare_quais?.length || 0} quai(s) -
- {gare.gare_transports?.map(t => renderTransportBadge(t))} -
-
{getDisplayTime(gare.type)} -
- { setGareToMerge(gare); setShowMergeModal(true); }} disabled={!canUpdateGares} title="Fusionner"> - - - openEditModal(gare)} disabled={!canUpdateGares}> - - - setDeleteConfirmId(gare.id)} disabled={!canDeleteGares}> - - -
-
+
+ + {gare.gare_quais?.length || 0} quai(s) + + +
+ {gare.gare_transports?.map((t, i) => renderTransportChip(t, i))} +
+
+ {getDisplayTime(gare.type)} + +
+ { setGareToMerge(gare); setShowMergeModal(true); }} + disabled={!canUpdateGares} + title="Fusionner" + > + + + openEditModal(gare)} + disabled={!canUpdateGares} + title="Modifier" + > + + + setDeleteConfirmId(gare.id)} + disabled={!canDeleteGares} + title="Supprimer" + > + + +
+
+
+ )} + + {/* 5. MODAL CRÉATION / ÉDITION */} + {showModal && ( +
+
e.stopPropagation()} ref={modalRef}> +
+

{editingGare ? 'Modifier la gare' : 'Nouvelle gare'}

+
- )} - {/* Modal création/édition */} - {showModal && ( -
-
e.stopPropagation()}> -
-

{editingGare ? 'Modifier la gare' : 'Nouvelle gare'}

- -
+ {/* Tabs navigation */} +
+ {[ + { key: 'general', label: 'Général', icon: 'train' }, + { key: 'quais', label: 'Quais & Transports', icon: 'alt_route' }, + { key: 'informations', label: 'Informations & Services', icon: 'info' } + ].map(tab => ( + + ))} +
-
- {formError && ( - - Erreur - {formError} - - )} +
+ {formError && ( + + Erreur + {formError} + + )} - {/* ── Tabs navigation ── */} -
- {[ - { key: 'general', label: 'Général', icon: 'train' }, - { key: 'quais', label: 'Quais & Transports', icon: 'route' }, - { key: 'informations', label: 'Informations', icon: 'info' } - ].map(tab => ( - - ))} + {/* TAB 1: GÉNÉRAL */} + {activeTab === 'general' && ( +
+
+ + updateField('nom', e.target.value || '')} + />
-
+
+ + { + const val = e.detail?.value ?? e.target?.value; + if (val) updateField('type', val); + }} + > + updateField('type', 'ville')}> + (Affichage quai : 30 min avant départ) + + updateField('type', 'interurbaine')}> + (Affichage quai : 12h avant départ) + + +
+
+ )} - {/* ════════════════════════════════════ - TAB GÉNÉRAL - ════════════════════════════════════ */} -
-
-
- + {/* TAB 2: QUAIS & TRANSPORTS */} + {activeTab === 'quais' && ( +
+ {/* Transports */} +
+ + + updateField('transports', e.detail?.value || [])} + > + {TRANSPORT_OPTIONS.map(opt => ( + + {TRANSPORT_CONFIG[opt]?.label || opt} + + ))} + + +
+ + {/* Quais */} +
+ +
+ {formData.quais.map((quai, index) => ( +
+
updateField('nom', e.target.value || '')} + data-field="quai-nom" + data-quai-id={quai.clientId} + value={quai.nom} + placeholder="Nom du quai (ex: Voie A, Quai 1)" + onInput={(e) => updateQuai(quai.clientId, 'nom', e.target.value || '')} />
- -
- - { - const val = e.detail?.value ?? e.target?.value; - if (val) updateField('type', val); - }} +
+ updateQuai(quai.clientId, 'distance_metres', parseInt(e.target.value, 10) || 0)} + /> +
+
+ + {quaisCounts[quai.nom] ? `${quaisCounts[quai.nom]} horaire(s)` : '0 horaire'} + +
+ {formData.quais.length > 1 && ( + removeQuai(quai.clientId)} + title="Supprimer ce quai" > - updateField('type', 'ville')}> - (Affichage quai: 30 min avant départ) - - updateField('type', 'interurbaine')}> - (Affichage quai: 12h avant départ) - -
-
+ + + )}
-
- - {/* ════════════════════════════════════ - TAB QUAIS & TRANSPORTS - ════════════════════════════════════ */} -
-
- {/* Transports */} -
- - - updateField('transports', e.detail?.value || [])} - > - {TRANSPORT_OPTIONS.map(opt => ( - - {opt.charAt(0).toUpperCase() + opt.slice(1)} - - ))} - - -
- - {/* Quais */} -
- -
- {formData.quais.map((quai, index) => ( -
- updateQuai(quai.clientId, 'nom', e.target.value || '')} - /> - updateQuai(quai.clientId, 'distance_metres', parseInt(e.target.value, 10) || 0)} - /> -
- {quaisCounts[quai.nom] ? `${quaisCounts[quai.nom]} horaire(s)` : '0 horaire'} -
- {formData.quais.length > 1 && ( - removeQuai(quai.clientId)}>× - )} -
- ))} - - Ajouter un quai - -
-
- - {/* Halls de la gare */} -
- -
- {(formData.quais_hall || []).map((hall) => { - const associatedQuais = hall.quais || []; - return ( -
-
- updateHallName(hall.id, e.target.value)} - /> - -
-
- Quais associés à ce hall : -
- {formData.quais.map((quai, qIdx) => { - if (!quai.nom || quai.nom.trim() === '') return null; - const isSelected = associatedQuais.includes(quai.nom); - return ( - - ); - })} - {formData.quais.filter(q => q.nom && q.nom.trim() !== '').length === 0 && ( - Veuillez d'abord renseigner des quais ci-dessus pour pouvoir les associer. - )} -
-
-
- ); - })} - - Ajouter un hall - -
-
-
-
- - {/* ════════════════════════════════════ - TAB INFORMATIONS - ════════════════════════════════════ */} -
-
- - {/* ── Informations Générales ── */} -
-

Informations Générales

-
-
- - updateInfo('adresse', e.target.value)} - placeholder="Ex: 20 Boulevard Diderot, 75012 Paris" /> -
-
- - updateInfo('horaires_guichet', e.target.value)} - placeholder="Ex: 6h00 – 21h30 (Lun-Sam)" /> -
-
- - updateInfo('horaires_gare', e.target.value)} - placeholder="Ex: 5h00 – 00h30 (tous les jours)" /> -
-
- updateInfo('distributeur_ter', v)} /> -
-
-
- - {/* ── Accessibilité ── */} -
-

Accessibilité

- updateInfo('service_assistance', v)} /> -
- - {/* ── Services ── */} -
-

Services

-
- updateInfo('espace_attente', v)} /> - updateInfo('distributeur_tgv', v)} /> - updateInfo('objets_trouves', v)} /> - updateInfo('toilettes', v)} /> - updateInfo('wifi_gratuit', v)} /> - updateInfo('photomaton', v)} /> - updateInfo('defibrillateur', v)} /> - updateInfo('galeries_marchandes', v)} /> -
-
- - {/* ── Voiture ── */} -
-

Voiture

-
- updateInfo('depose_minute', v)} /> - updateInfo('espace_taxis', v)} /> - updateInfo('espace_vtc', v)} /> - updateInfo('location_voiture', v)} /> - updateInfo('parking', v)} /> -
-
- - {/* ── Vélo ── */} -
-

Vélo

-
- updateInfo('velo_libre_service', v)} /> - updateInfo('parking_velos', v)} /> -
-
- - {/* ── Transports en commun enrichis ── */} -
-

Transports en commun

-
- {[ - { key: 'lignes_bus', label: 'Bus', icon: '🚌' }, - { key: 'lignes_metro', label: 'Métro', icon: '🚇' }, - { key: 'lignes_tramway', label: 'Tramway', icon: '🚊' }, - { key: 'lignes_car_regional', label: 'Car Régional', icon: '🚍' } - ].map(({ key, label, icon }) => { - const lignes = formData.informations[key] || []; - return ( -
-
- - -
- {lignes.length === 0 ? ( -
- Aucune ligne {label.toLowerCase()} — cliquez sur "Ajouter une ligne" -
- ) : ( -
- {lignes.map((ligne, idx) => ( -
-
- - updateLigneTransport(key, idx, 'nom', e.target.value)} - placeholder="Ex: Ligne 7" /> -
-
- - updateLigneTransport(key, idx, 'terminus_a', e.target.value)} - placeholder="Ex: La Courneuve" /> -
-
- - updateLigneTransport(key, idx, 'terminus_b', e.target.value)} - placeholder="Ex: Villejuif" /> -
- -
- ))} -
- )} -
- ); - })} -
-
- -
-
-
-
- -
- Annuler - - {saving ? 'Enregistrement...' : (editingGare ? 'Mettre à jour' : 'Créer la gare')} - -
-
-
- )} - - {/* Modal confirmation suppression */} - {deleteConfirmId && ( -
setDeleteConfirmId(null)}> -
e.stopPropagation()}> -
-

Confirmer la suppression

-
-
-

Êtes-vous sûr de vouloir supprimer cette gare ?

-

Cette action est irréversible.

-
-
- setDeleteConfirmId(null)}> - Annuler - - handleDelete(deleteConfirmId)} disabled={!canDeleteGares}> - Supprimer - -
-
-
- )} - - {/* Modal de fusion */} - {showMergeModal && gareToMerge && ( -
setShowMergeModal(false)}> -
e.stopPropagation()}> -
-

Fusionner la gare

- -
-
-

Vous êtes sur le point de fusionner la gare {gareToMerge.nom} avec une autre.

-

Tous les horaires associés seront transférés vers la gare cible, puis cette gare sera supprimée. Cette action est irréversible.

-
- - + + + Ajouter un quai + +
+
+ + {/* Halls de la gare */} +
+ +
+ {(formData.quais_hall || []).map((hall) => { + const associatedQuais = hall.quais || []; + return ( +
+
+ updateHallName(hall.id, e.target.value)} + /> + removeHall(hall.id)}> + + Supprimer le hall + +
+
+ + Quais associés à ce hall : + +
+ {formData.quais.map((quai, qIdx) => { + if (!quai.nom || quai.nom.trim() === '') return null; + const isSelected = associatedQuais.includes(quai.nom); + return ( + + ); + })} + {formData.quais.filter(q => q.nom && q.nom.trim() !== '').length === 0 && ( + Renseignez d'abord des quais ci-dessus pour les associer. + )} +
+
+
+ ); + })} + + + Ajouter un hall + +
-
- setShowMergeModal(false)} disabled={saving}> - Annuler - - - {saving ? 'Fusion...' : 'Confirmer la fusion'} - + )} + + {/* TAB 3: INFORMATIONS & SERVICES */} + {activeTab === 'informations' && ( +
+ {/* Informations Générales */} +
+

+ + Informations Générales +

+
+
+ + updateInfo('adresse', e.target.value)} + placeholder="Ex: 20 Boulevard Diderot, 75012 Paris" + /> +
+
+ + updateInfo('horaires_guichet', e.target.value)} + placeholder="Ex: 6h00 – 21h30 (Lun-Sam)" + /> +
+
+ + updateInfo('horaires_gare', e.target.value)} + placeholder="Ex: 5h00 – 00h30 (tous les jours)" + /> +
+
+ updateInfo('distributeur_ter', v)} + /> +
+
+
+ + {/* Accessibilité */} +
+

+ + Accessibilité PMR +

+ updateInfo('service_assistance', v)} + /> +
+ + {/* Services */} +
+

+ + Services aux voyageurs +

+
+ updateInfo('espace_attente', v)} /> + updateInfo('distributeur_tgv', v)} /> + updateInfo('objets_trouves', v)} /> + updateInfo('toilettes', v)} /> + updateInfo('wifi_gratuit', v)} /> + updateInfo('photomaton', v)} /> + updateInfo('defibrillateur', v)} /> + updateInfo('galeries_marchandes', v)} /> +
+
+ + {/* Voiture & Vélo */} +
+

+ + Intermodalité Voiture & Vélo +

+
+ updateInfo('depose_minute', v)} /> + updateInfo('espace_taxis', v)} /> + updateInfo('espace_vtc', v)} /> + updateInfo('location_voiture', v)} /> + updateInfo('parking', v)} /> + updateInfo('velo_libre_service', v)} /> + updateInfo('parking_velos', v)} /> +
+
+ + {/* Transports en commun enrichis */} +
+

+ + Lignes de correspondance +

+
+ {[ + { key: 'lignes_bus', label: 'Bus', icon: 'directions_bus' }, + { key: 'lignes_metro', label: 'Métro', icon: 'subway' }, + { key: 'lignes_tramway', label: 'Tramway', icon: 'tram' }, + { key: 'lignes_car_regional', label: 'Car Régional', icon: 'directions_bus' } + ].map(({ key, label, icon }) => { + const lignes = formData.informations[key] || []; + return ( +
+
+
+ + {label} + {lignes.length > 0 && ( + {lignes.length} + )} +
+ addLigneTransport(key)}> + + Ajouter une ligne + +
+ {lignes.length === 0 ? ( +
+ Aucune ligne {label.toLowerCase()} configurée. +
+ ) : ( +
+ {lignes.map((ligne, idx) => ( +
+ updateLigneTransport(key, idx, 'nom', e.target.value)} + placeholder="Ex: Ligne 7" + /> + updateLigneTransport(key, idx, 'terminus_a', e.target.value)} + placeholder="Terminus A" + /> + updateLigneTransport(key, idx, 'terminus_b', e.target.value)} + placeholder="Terminus B" + /> + removeLigneTransport(key, idx)}> + + +
+ ))} +
+ )} +
+ ); + })} +
+
+ )} +
+ +
+ + Annuler + + + {saving ? 'Enregistrement...' : (editingGare ? 'Mettre à jour' : 'Créer la gare')} + +
+
+
+ )} + + {/* Modal confirmation suppression */} + {deleteConfirmId && ( +
setDeleteConfirmId(null)}> +
e.stopPropagation()}> +
+

Confirmer la suppression

+ +
+
+

Êtes-vous sûr de vouloir supprimer cette gare ?

+

Cette action est irréversible.

+
+
+ setDeleteConfirmId(null)}> + Annuler + + handleDelete(deleteConfirmId)} disabled={!canDeleteGares}> + + Supprimer + +
+
+
+ )} + + {/* Modal de fusion */} + {showMergeModal && gareToMerge && ( +
setShowMergeModal(false)}> +
e.stopPropagation()}> +
+

Fusionner la gare

+ +
+
+

+ Vous êtes sur le point de fusionner {gareToMerge.nom} avec une autre gare. +

+

+ Tous les horaires associés seront transférés vers la gare cible choisie ci-dessous. Cette gare sera ensuite supprimée. +

+
+ +
- )} +
+ setShowMergeModal(false)} disabled={saving}> + Annuler + + + + {saving ? 'Fusion...' : 'Confirmer la fusion'} + +
+
+
+ )} +
+
); } \ No newline at end of file diff --git a/src/pages/gares/ImportGaresGTFS.js b/src/pages/gares/ImportGaresGTFS.js index 9293f29..9a016c6 100644 --- a/src/pages/gares/ImportGaresGTFS.js +++ b/src/pages/gares/ImportGaresGTFS.js @@ -219,12 +219,32 @@ export default function ImportGaresGTFS() { }; return ( -
-
-
- -
-

Import Gares depuis GTFS

+
+
+
+
+
+
+
+ + + GTFS Transits + +
+

Import de Gares GTFS

+

+ Analysez et importez en masse les données d'arrêts et de correspondances depuis un flux GTFS. +

+
+
+ { window.location.assign('/gares'); }}> + + Retour aux gares + +
+
+ +
{step === 1 && (
@@ -292,7 +312,7 @@ export default function ImportGaresGTFS() { {groups[routeName].length} gare(s) { e.stopPropagation(); selectAllInGroup(routeName, true); }}>Tout { e.stopPropagation(); selectAllInGroup(routeName, false); }}>Aucun - {expandedRoutes[routeName] ? '▲' : '▼'} +
{expandedRoutes[routeName] && ( @@ -398,7 +418,7 @@ export default function ImportGaresGTFS() { )}
- +
); diff --git a/src/pages/gares/InfoGaresTab.css b/src/pages/gares/InfoGaresTab.css index ee80d92..e6856fc 100644 --- a/src/pages/gares/InfoGaresTab.css +++ b/src/pages/gares/InfoGaresTab.css @@ -236,4 +236,93 @@ .info-gare-form-grid { 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; } \ No newline at end of file diff --git a/src/pages/gares/InfoGaresTab.js b/src/pages/gares/InfoGaresTab.js index a561c25..d002ec9 100644 --- a/src/pages/gares/InfoGaresTab.js +++ b/src/pages/gares/InfoGaresTab.js @@ -285,17 +285,17 @@ const InfoGaresTab = forwardRef((props, ref) => {
{targetGare && ( - 🚆 {targetGare} + {targetGare} )} {targetLigne && ( - 🎫 Ligne {targetLigne} + Ligne {targetLigne} )} {!targetGare && !targetLigne && ( - 🌐 Toutes gares / lignes + Toutes gares / lignes )}
diff --git a/src/pages/gares/RichTextEditor.css b/src/pages/gares/RichTextEditor.css index 904c49e..ad1c17d 100644 --- a/src/pages/gares/RichTextEditor.css +++ b/src/pages/gares/RichTextEditor.css @@ -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; +} diff --git a/src/pages/horaires/AttributionQuaisDepartArrivee.css b/src/pages/horaires/AttributionQuaisDepartArrivee.css index abb822a..c9b4fda 100644 --- a/src/pages/horaires/AttributionQuaisDepartArrivee.css +++ b/src/pages/horaires/AttributionQuaisDepartArrivee.css @@ -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; +} + diff --git a/src/pages/horaires/AttributionQuaisSchematique.css b/src/pages/horaires/AttributionQuaisSchematique.css index 6702cf4..ed91850 100644 --- a/src/pages/horaires/AttributionQuaisSchematique.css +++ b/src/pages/horaires/AttributionQuaisSchematique.css @@ -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; +} + diff --git a/src/pages/horaires/Horaires.css b/src/pages/horaires/Horaires.css index 5f9d4d0..38c2335 100644 --- a/src/pages/horaires/Horaires.css +++ b/src/pages/horaires/Horaires.css @@ -1,241 +1,566 @@ :root { - --horaires-blue-950: #082e4f; - --horaires-blue-900: #0d3d67; - --horaires-blue-800: #11507e; - --horaires-blue-700: #1d5f90; + --horaires-blue-950: #061c2f; + --horaires-blue-900: #082e4f; + --horaires-blue-800: #0d3d67; + --horaires-blue-700: #11507e; + --horaires-blue-600: #1d5f90; --horaires-blue-500: #0088ce; + --horaires-blue-400: #38bdf8; + --horaires-blue-100: #e0f2fe; + --horaires-blue-50: #f0f9ff; --horaires-surface: #ffffff; - --horaires-surface-soft: #f7fbff; - --horaires-border: #d8e5f3; - --horaires-text: #12324e; - --horaires-muted: #62809a; - --horaires-shadow-soft: 0 8px 26px rgba(8, 46, 79, 0.1); - --horaires-shadow-strong: 0 16px 40px rgba(8, 46, 79, 0.2); + --horaires-surface-soft: #f8fafc; + --horaires-surface-card: #ffffff; + --horaires-border: #e2e8f0; + --horaires-border-soft: #edf2f7; + --horaires-text: #0f172a; + --horaires-muted: #64748b; + --horaires-shadow-soft: 0 4px 20px -2px rgba(15, 23, 42, 0.06), 0 2px 6px -1px rgba(15, 23, 42, 0.04); + --horaires-shadow-card: 0 10px 25px -5px rgba(15, 23, 42, 0.08), 0 8px 10px -6px rgba(15, 23, 42, 0.04); + --horaires-shadow-strong: 0 20px 40px -15px rgba(8, 46, 79, 0.35); + --horaires-radius-sm: 8px; + --horaires-radius-md: 12px; + --horaires-radius-lg: 16px; + --horaires-radius-xl: 20px; } -.horaires-page { +/* Base Layout */ +.horaires-page-root { background: - radial-gradient(circle at 0 0, rgba(17, 80, 126, 0.14) 0, rgba(17, 80, 126, 0) 35%), - radial-gradient(circle at 100% 0, rgba(8, 46, 79, 0.16) 0, rgba(8, 46, 79, 0) 40%), - #f2f7fd; - min-height: 100%; + radial-gradient(circle at 10% 10%, rgba(13, 61, 103, 0.04) 0%, transparent 40%), + radial-gradient(circle at 90% 20%, rgba(0, 136, 206, 0.05) 0%, transparent 50%), + #f8fafc; + min-height: 100vh; + width: 100%; } -.horaires-page-content { - width: min(1460px, 100%); +.horaires-modern-container { + max-width: 1540px; margin: 0 auto; - padding-bottom: 12px; + padding: 24px 32px 60px; + box-sizing: border-box; } -.horaires-page-header { - background: linear-gradient(140deg, var(--horaires-blue-950) 0%, var(--horaires-blue-800) 70%, var(--horaires-blue-700) 100%); - border-radius: 18px; - color: #f8fbff; - padding: 26px; - margin-bottom: 16px; +/* 1. HERO BANNER */ +.horaires-hero-banner { + background: linear-gradient(135deg, #082e4f 0%, #0d3d67 40%, #11507e 75%, #0088ce 100%); + border-radius: var(--horaires-radius-xl); + color: #ffffff; + padding: 32px 36px; + margin-bottom: 24px; display: flex; justify-content: space-between; - gap: 14px; - align-items: flex-start; + align-items: center; + gap: 24px; box-shadow: var(--horaires-shadow-strong); - border: 1px solid rgba(255, 255, 255, 0.14); + position: relative; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.12); } -.horaires-page-kicker { - margin: 0 0 8px; - text-transform: uppercase; - letter-spacing: 0.1em; +.horaires-hero-banner::after { + content: ''; + position: absolute; + top: -40%; + right: -10%; + width: 420px; + height: 420px; + background: radial-gradient(circle, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0) 70%); + border-radius: 50%; + pointer-events: none; +} + +.horaires-hero-content { + max-width: 820px; + position: relative; + z-index: 2; +} + +.horaires-hero-tags { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 12px; +} + +.horaires-kicker-pill { + display: inline-flex; + align-items: center; + gap: 6px; + background: rgba(255, 255, 255, 0.14); + backdrop-filter: blur(8px); + padding: 4px 12px; + border-radius: 9999px; font-size: 11px; font-weight: 700; - opacity: 0.86; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #e0f2fe; + border: 1px solid rgba(255, 255, 255, 0.18); } -.horaires-page-header h2 { +.horaires-hero-title { + margin: 0 0 10px; + font-size: 32px; + font-weight: 800; + letter-spacing: -0.02em; + color: #ffffff !important; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.horaires-hero-subtitle { margin: 0; - font-size: 30px; - letter-spacing: -0.01em; - color: #f8fbff !important; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.18); + font-size: 14.5px; + line-height: 1.55; + color: rgba(255, 255, 255, 0.92); } -.horaires-page-kicker, -.horaires-page-description { - color: #f2f9ff !important; +.horaires-hero-actions { + display: flex; + gap: 12px; + flex-wrap: wrap; + align-items: center; + position: relative; + z-index: 2; + flex-shrink: 0; } -.horaires-page-description { - margin: 10px 0 0; - color: rgba(247, 251, 255, 0.94); - max-width: 780px; +/* 2. KPI STAT CARDS */ +.horaires-kpi-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 18px; + margin-bottom: 24px; } -.horaires-alert { - margin-bottom: 1rem; -} - -.horaires-main-tabs { - border-radius: 16px; +.horaires-kpi-card { + background: var(--horaires-surface); border: 1px solid var(--horaires-border); - background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%); + border-radius: var(--horaires-radius-lg); + padding: 20px 22px; + display: flex; + align-items: center; + gap: 18px; box-shadow: var(--horaires-shadow-soft); - padding: 10px 14px 14px; + transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); + cursor: pointer; + user-select: none; +} + +.horaires-kpi-card:hover { + transform: translateY(-2px); + box-shadow: var(--horaires-shadow-card); + border-color: #cbd5e1; +} + +.horaires-kpi-card.active { + border-color: var(--horaires-blue-500); + background: linear-gradient(180deg, #ffffff 0%, #f0f9ff 100%); + box-shadow: 0 0 0 2px var(--horaires-blue-500), var(--horaires-shadow-card); +} + +.horaires-kpi-icon { + width: 52px; + height: 52px; + border-radius: var(--horaires-radius-md); + display: flex; + align-items: center; + justify-content: center; + font-size: 26px; + flex-shrink: 0; +} + +.kpi-blue { + background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%); + color: #0284c7; +} + +.kpi-emerald { + background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%); + color: #16a34a; +} + +.kpi-amber { + background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); + color: #d97706; +} + +.kpi-purple { + background: linear-gradient(135deg, #f3e8ff 0%, #e9d5ff 100%); + color: #9333ea; +} + +.horaires-kpi-info { + display: flex; + flex-direction: column; + min-width: 0; +} + +.horaires-kpi-label { + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--horaires-muted); + margin-bottom: 3px; +} + +.horaires-kpi-value { + font-size: 26px; + font-weight: 800; + color: var(--horaires-text); + line-height: 1.1; + letter-spacing: -0.02em; +} + +.horaires-kpi-subtext { + font-size: 11.5px; + color: #94a3b8; + margin-top: 4px; + white-space: nowrap; overflow: hidden; + text-overflow: ellipsis; } -.horaires-main-tabs { - --wcs-tabs-color: var(--horaires-blue-800); +/* 3. SEGMENTED MAIN TABS */ +.horaires-main-tabs-nav { + display: flex; + gap: 8px; + background: #ffffff; + padding: 8px; + border-radius: var(--horaires-radius-lg); + border: 1px solid var(--horaires-border); + box-shadow: var(--horaires-shadow-soft); + margin-bottom: 20px; + overflow-x: auto; + scrollbar-width: none; } +.horaires-main-tabs-nav::-webkit-scrollbar { + display: none; +} + +.horaires-main-tab-btn { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 10px 18px; + border: none; + background: transparent; + border-radius: var(--horaires-radius-md); + color: var(--horaires-muted); + font-size: 13.5px; + font-weight: 600; + cursor: pointer; + transition: all 0.18s ease; + white-space: nowrap; +} + +.horaires-main-tab-btn:hover { + background: var(--horaires-surface-soft); + color: var(--horaires-text); +} + +.horaires-main-tab-btn.active { + background: linear-gradient(135deg, #0d3d67 0%, #11507e 100%); + color: #ffffff; + box-shadow: 0 4px 12px rgba(13, 61, 103, 0.25); +} + +.horaires-main-tab-btn.active wcs-mat-icon { + color: #38bdf8; +} + +.horaires-main-tab-badge { + background: rgba(15, 23, 42, 0.08); + color: var(--horaires-muted); + padding: 2px 8px; + border-radius: 9999px; + font-size: 11px; + font-weight: 700; + transition: all 0.18s ease; +} + +.horaires-main-tab-btn.active .horaires-main-tab-badge { + background: rgba(255, 255, 255, 0.2); + color: #ffffff; +} + +/* 4. WORKSPACE & SIDEBAR (Horaires Tab) */ .horaires-workspace { display: flex; - gap: 0; - min-height: 560px; - border-radius: 14px; + min-height: 640px; + border-radius: var(--horaires-radius-lg); overflow: hidden; - border: 1px solid #e1eaf4; - background: #f6faff; + border: 1px solid var(--horaires-border); + background: #ffffff; + box-shadow: var(--horaires-shadow-card); } -.horaires-workspace > div:first-child { - box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.15); - background: linear-gradient(180deg, #0a3558 0%, #0f446f 60%, #12507f 100%) !important; - border-right: none !important; +.horaires-lines-sidebar { + flex-shrink: 0; + background: linear-gradient(180deg, #092e4f 0%, #0d3d67 50%, #11507e 100%); + color: #e2e8f0; + transition: width 0.25s cubic-bezier(0.16, 1, 0.3, 1), min-width 0.25s cubic-bezier(0.16, 1, 0.3, 1); + display: flex; + flex-direction: column; + position: relative; + border-right: 1px solid rgba(255, 255, 255, 0.08); } -/* Contraste panneau latéral lignes (normal / secondaire / actif) */ -.horaires-workspace > div:first-child { - color: #e8f4ff; +.horaires-lines-toggle-btn { + position: absolute; + top: 10px; + right: 8px; + z-index: 10; + background: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: var(--horaires-radius-sm); + color: #ffffff; + padding: 5px 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; } -/* Hover / active styles for lignes panel items */ -.horaires-workspace > div:first-child .ligne-item .ligne-name, -.horaires-workspace > div:first-child .ligne-item .ligne-count, -.horaires-workspace > div:first-child .ligne-item wcs-mat-icon { - transition: color 0.15s ease, background 0.15s ease, font-weight 0.15s ease; +.horaires-lines-toggle-btn:hover { + background: rgba(255, 255, 255, 0.28); } -/* When hovering an item, make its text and icon blue and the badge blue like the selected state */ -.horaires-workspace > div:first-child .ligne-item:hover .ligne-name { - color: #0088ce !important; - font-weight: 700 !important; -} -.horaires-workspace > div:first-child .ligne-item:hover wcs-mat-icon { - color: #0088ce !important; -} -.horaires-workspace > div:first-child .ligne-item:hover .ligne-count { - background: #0088ce !important; - color: #fff !important; +.horaires-lines-sidebar-header { + padding: 14px 16px 10px; + font-weight: 700; + font-size: 12px; + color: #93c5fd; + text-transform: uppercase; + letter-spacing: 0.08em; + display: flex; + align-items: center; + gap: 8px; } -/* Keep selected (active) appearance */ -.horaires-workspace > div:first-child .ligne-item.is-active .ligne-name { - color: #0088ce !important; - font-weight: 700 !important; -} -.horaires-workspace > div:first-child .ligne-item.is-active wcs-mat-icon { - color: #0088ce !important; -} -.horaires-workspace > div:first-child .ligne-item.is-active .ligne-count { - background: #0088ce !important; - color: #fff !important; +.horaires-lines-list { + overflow-y: auto; + flex: 1; + padding: 4px 8px; + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.2) transparent; } -.horaires-workspace > div:first-child [style*="color: #374151"], -.horaires-workspace > div:first-child [style*="color: #1f2937"], -.horaires-workspace > div:first-child [style*="color: #0088ce"], -.horaires-workspace > div:first-child [style*="color: #0369a1"], -.horaires-workspace > div:first-child [style*="color: #0c4a6e"] { - color: #f4faff !important; +.horaires-lines-list::-webkit-scrollbar { + width: 5px; +} +.horaires-lines-list::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; } -.horaires-workspace > div:first-child [style*="color: #6b7280"], -.horaires-workspace > div:first-child [style*="color: #9ca3af"], -.horaires-workspace > div:first-child [style*="color: #d1d5db"] { - color: #b7d0e3 !important; +.ligne-item-modern { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 9px 12px; + border: none; + border-radius: var(--horaires-radius-sm); + background: transparent; + cursor: pointer; + text-align: left; + transition: all 0.15s ease; + gap: 8px; + margin-bottom: 2px; + color: #cbd5e1; } -.horaires-workspace > div:first-child [style*="background: #e5e7eb"], -.horaires-workspace > div:first-child [style*="background: #f3f4f6"], -.horaires-workspace > div:first-child [style*="background: #e8f4fd"] { - background: rgba(255, 255, 255, 0.14) !important; +.ligne-item-modern:hover { + background: rgba(255, 255, 255, 0.12); + color: #ffffff; } -.horaires-workspace > div:first-child [style*="background: #0088ce"] { - background: #7dc8f3 !important; - color: #05263f !important; +.ligne-item-modern.is-active { + background: linear-gradient(90deg, #0284c7 0%, #0088ce 100%); + color: #ffffff; + box-shadow: 0 2px 8px rgba(0, 136, 206, 0.35); + font-weight: 700; } -.horaires-workspace > div:first-child button[style*="background: #e8f4fd"] { - background: rgba(125, 200, 243, 0.3) !important; +.ligne-item-modern .ligne-label { + display: flex; + align-items: center; + gap: 8px; + overflow: hidden; + font-size: 13px; } -.horaires-workspace > div:first-child button[style*="background: transparent"]:hover { - background: rgba(255, 255, 255, 0.18) !important; +.ligne-item-modern .ligne-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.horaires-workspace > div:first-child button { - border-radius: 10px !important; +.ligne-item-modern .ligne-count { + background: rgba(255, 255, 255, 0.16); + color: #ffffff; + border-radius: 9999px; + padding: 2px 7px; + font-size: 11px; + font-weight: 700; + flex-shrink: 0; } +.ligne-item-modern.is-active .ligne-count { + background: #ffffff; + color: #0284c7; +} + +.horaires-lines-footer { + border-top: 1px solid rgba(255, 255, 255, 0.1); + padding: 10px 12px; + background: rgba(0, 0, 0, 0.12); +} + +.horaires-create-ligne-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + width: 100%; + background: rgba(255, 255, 255, 0.08); + border: 1px dashed rgba(255, 255, 255, 0.3); + border-radius: var(--horaires-radius-sm); + padding: 8px 12px; + cursor: pointer; + color: #e2e8f0; + font-size: 12px; + font-weight: 600; + transition: all 0.15s ease; +} + +.horaires-create-ligne-btn:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.18); + border-color: #38bdf8; + color: #38bdf8; +} + +/* 5. MAIN CONTENT AREA */ .horaires-main-content { flex: 1; min-width: 0; - padding: 18px 18px 18px 20px; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(246, 250, 255, 0.96) 100%); + padding: 20px 24px; + background: #ffffff; + display: flex; + flex-direction: column; } +/* Filter Alert Banner */ +.horaires-filter-banner { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; + padding: 10px 16px; + border-radius: var(--horaires-radius-md); + font-size: 13.5px; +} + +.horaires-filter-banner.info { + background: #f0f9ff; + border: 1px solid #bae6fd; + color: #0369a1; +} + +.horaires-filter-banner.warning { + background: #fefce8; + border: 1px solid #fef08a; + color: #854d0e; +} + +.horaires-filter-banner-close { + margin-left: auto; + background: none; + border: none; + cursor: pointer; + color: inherit; + font-size: 18px; + line-height: 1; + opacity: 0.7; + padding: 2px 6px; + border-radius: 4px; +} + +.horaires-filter-banner-close:hover { + opacity: 1; + background: rgba(0, 0, 0, 0.05); +} + +/* Toolbar */ .horaires-toolbar { display: flex; justify-content: space-between; - margin-bottom: 16px; + align-items: center; flex-wrap: wrap; - gap: 10px; - padding: 12px; - border: 1px solid #dce8f5; - border-radius: 12px; - background: #f8fbff; + gap: 12px; + padding: 12px 16px; + border: 1px solid var(--horaires-border); + border-radius: var(--horaires-radius-md); + background: var(--horaires-surface-soft); + margin-bottom: 16px; } .horaires-toolbar-left { display: flex; - gap: 8px; align-items: center; + gap: 8px; flex-wrap: wrap; -} - -.horaires-select-btn { - margin-left: 8px; + flex: 1; + min-width: 320px; } .horaires-search-wrap { - margin-left: 6px; + position: relative; + display: flex; + align-items: center; + flex: 1; + max-width: 380px; +} + +.horaires-search-wrap wcs-mat-icon { + position: absolute; + left: 12px; + color: #94a3b8; + pointer-events: none; } .horaires-search-input { - padding: 9px 12px; - border: 1px solid #c9daeb; - border-radius: 10px; - min-width: 280px; - background: #fff; + width: 100%; + padding: 9px 12px 9px 36px; + border: 1px solid var(--horaires-border); + border-radius: var(--horaires-radius-sm); + background: #ffffff; color: var(--horaires-text); + font-size: 13.5px; outline: none; + transition: all 0.15s ease; } .horaires-search-input:focus { - border-color: #61a8d4; - box-shadow: 0 0 0 3px rgba(0, 136, 206, 0.12); + border-color: var(--horaires-blue-500); + box-shadow: 0 0 0 3px rgba(0, 136, 206, 0.15); } +/* Selection Mode Panel */ .horaires-selection-panel { display: flex; align-items: center; justify-content: space-between; - padding: 13px 16px; - background: linear-gradient(135deg, #e8f4fd 0%, #f1f9ff 100%); - border: 1px solid #8fc4e6; - border-radius: 10px; + padding: 12px 18px; + background: linear-gradient(135deg, #e0f2fe 0%, #f0f9ff 100%); + border: 1px solid #7dd3fc; + border-radius: var(--horaires-radius-md); margin-bottom: 16px; - gap: 10px; + gap: 12px; flex-wrap: wrap; } @@ -246,322 +571,273 @@ } .horaires-selection-count { - color: #005f93; - font-weight: 600; + color: #0369a1; + font-weight: 700; + font-size: 13.5px; } .horaires-selection-actions { display: flex; gap: 8px; + flex-wrap: wrap; } -.horaires-page .table-container { - border: 1px solid #d9e6f3; - border-radius: 14px; +/* 6. TABLES */ +.table-container { + border: 1px solid var(--horaires-border); + border-radius: var(--horaires-radius-md); overflow: hidden; - background: #fff; - box-shadow: 0 10px 30px rgba(8, 46, 79, 0.08); + background: #ffffff; + box-shadow: var(--horaires-shadow-soft); + margin-bottom: 16px; } -.horaires-page .wcs-table { +.horaires-page-root .wcs-table { + width: 100%; border-collapse: separate; border-spacing: 0; + text-align: left; } -.horaires-page .wcs-table thead tr { - background: linear-gradient(90deg, #0f456e 0%, #165989 100%); +.horaires-page-root .wcs-table thead tr { + background: linear-gradient(90deg, #0d3d67 0%, #11507e 100%); } -.horaires-page .wcs-table th { - color: #f5fbff; +.horaires-page-root .wcs-table th { + color: #ffffff; border-bottom: none; - font-weight: 600; + font-weight: 700; font-size: 12px; - letter-spacing: 0.04em; + letter-spacing: 0.05em; text-transform: uppercase; - padding: 13px 12px; + padding: 13px 14px; } -.horaires-page .wcs-table td { - border-bottom: 1px solid #ebf2fa; - padding: 12px; +.horaires-page-root .wcs-table td { + border-bottom: 1px solid var(--horaires-border-soft); + padding: 12px 14px; color: var(--horaires-text); + font-size: 13.5px; vertical-align: middle; } -.horaires-page .wcs-table tbody tr:hover { - background: #f6fbff; +.horaires-page-root .wcs-table tbody tr:hover { + background: #f8fafc; } -.horaires-responsive-table td[data-label] { - position: relative; -} - -.horaires-page details { - border-radius: 10px; - background: #f9fcff; -} - -.horaires-page details > summary { - color: #10486f; -} - -.horaires-page input, -.horaires-page select, -.horaires-page textarea { - border: 1px solid #c8d9e9 !important; - border-radius: 10px !important; - background: #fff; - color: var(--horaires-text); -} - -.horaires-page input:focus, -.horaires-page select:focus, -.horaires-page textarea:focus { - border-color: #64acd8 !important; - box-shadow: 0 0 0 3px rgba(0, 136, 206, 0.12); - outline: none; -} - -.horaires-page .modal-overlay { - background: rgba(6, 28, 46, 0.56); - backdrop-filter: blur(2px); -} - -.horaires-page .modal-content { - border-radius: 16px; - border: 1px solid #d5e3f2; - background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%); - box-shadow: 0 20px 42px rgba(8, 46, 79, 0.28); - overflow: hidden; -} - -.horaires-page .modal-header { - background: linear-gradient(125deg, #0a3456 0%, #11507e 75%, #1d5f90 100%); - color: #f6fbff; - border-bottom: 1px solid rgba(255, 255, 255, 0.18); -} - -.horaires-page .modal-header h3, -.horaires-page .modal-header strong { - color: inherit; -} - -.horaires-page .modal-close { - color: #f3f9ff; -} - -.horaires-page .modal-body { - background: transparent; -} - -.horaires-page .modal-footer { - background: #f2f8ff; - border-top: 1px solid #dbe7f4; -} - -.horaires-page .form-group label { - color: #1d4768; - font-weight: 600; -} - -.horaires-loading { - color: var(--horaires-muted); - padding: 12px; -} - -.horaires-empty-state { - text-align: center; - padding: 44px; - border-radius: 14px; - border: 1px dashed #a9c7df; - background: linear-gradient(180deg, #f9fcff 0%, #f2f8ff 100%); -} - -.horaires-empty-icon { - font-size: 50px; - color: #72a8cc; -} - -.horaires-empty-text { - color: #557894; -} - -.horaires-main-content > div[style*="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))"] { - gap: 18px !important; -} - -.horaires-main-content > div[style*="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))"] > div { - border-radius: 14px !important; - border: 1px solid #d6e3f1 !important; - box-shadow: 0 10px 24px rgba(8, 46, 79, 0.09); - background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%) !important; -} - -.horaires-page .quai-row-modal { - background: #f8fbff; - border-radius: 10px; -} - -.horaires-section-header { - display: flex; - justify-content: space-between; - gap: 12px; - align-items: center; - margin-bottom: 16px; - padding: 12px 14px; - border-radius: 12px; - border: 1px solid #d9e7f5; - background: linear-gradient(120deg, #f8fbff 0%, #eef6ff 100%); -} - -.horaires-section-title { - margin: 0; - color: #0d3f66; - font-size: 21px; -} - -.horaires-section-description { - margin: 0 0 16px; - color: #5a7b96; -} - -.horaires-table-full { - width: 100%; +.horaires-page-root .wcs-table tbody tr.horaires-clickable-row { + cursor: pointer; } .horaires-row-actions { display: flex; gap: 6px; + align-items: center; } -.horaires-empty-state-compact { - padding: 36px; -} - -.horaires-search-input-wide { - min-width: 300px; -} - -.horaires-clickable-row { - cursor: pointer; -} - -.horaires-time-muted { - color: #62809a; -} - -.horaires-status-success { - color: #138a56; - font-weight: 600; -} - -.horaires-status-muted { - color: #90a4b7; -} - -.horaires-gare-autocomplete { - position: relative; -} - -.horaires-gare-suggestions { - position: absolute; - top: calc(100% + 4px); - left: 0; - right: 0; - z-index: 50; - border: 1px solid #c9daeb; - border-radius: 10px; - background: #ffffff; - box-shadow: 0 10px 22px rgba(8, 46, 79, 0.16); - max-height: 220px; - overflow-y: auto; -} - -.horaires-gare-suggestion-item { - display: block; - width: 100%; - text-align: left; - border: 0; - background: transparent; - padding: 9px 11px; - color: #12324e; - cursor: pointer; -} - -.horaires-gare-suggestion-item:hover { - background: #eef6ff; - color: #0b4b78; -} - -.horaires-modal-tabs { - display: flex; - gap: 4px; - border-bottom: 1px solid #d8e5f3; - background: #eef6ff; - padding: 8px 10px 0; - flex-shrink: 0; - overflow-x: auto; - white-space: nowrap; - -webkit-overflow-scrolling: touch; -} - -/* Hide scrollbar for a cleaner look */ -.horaires-modal-tabs::-webkit-scrollbar { - display: none; -} -.horaires-modal-tabs { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ -} - -.horaires-modal-tabs-alt { - margin-bottom: 20px; -} - -.horaires-modal-tab-btn { - padding: 10px 14px; - border: none; - background: transparent; - border-radius: 10px 10px 0 0; - color: #446884; - cursor: pointer; - font-weight: 500; - border-bottom: 3px solid transparent; - transition: all 0.2s ease; - flex-shrink: 0; - white-space: nowrap; -} - -.horaires-modal-tab-btn.is-active { - background: #ffffff; - color: #0b4b78; +/* Table elements & Chips */ +.train-num-badge { font-weight: 700; - border-bottom-color: #0088ce; -} - -.horaires-modal-body-scroll { - flex: 1; - overflow-y: auto; - padding: 20px 24px; -} - -.horaires-form-error { - background: #fff1f1; - border: 1px solid #f5b5b5; - border-radius: 8px; - padding: 10px 14px; - margin-bottom: 16px; - color: #b42318; + color: var(--horaires-blue-900); font-size: 14px; } -.horaires-tab-panel { - padding: 4px 2px; +.train-type-pill { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 9999px; + font-size: 11.5px; + font-weight: 700; + background: #f1f5f9; + color: #334155; + border: 1px solid #cbd5e1; } -.horaires-modal-body { - background: linear-gradient(180deg, #fbfdff 0%, #f4f9ff 100%); +.time-highlight { + color: var(--horaires-blue-600); + font-weight: 600; +} + +.circ-days-badge { + font-size: 12px; + color: #475569; +} + +.subst-tag { + display: inline-flex; + align-items: center; + gap: 4px; + background: #fee2e2; + color: #dc2626; + border: 1px solid #fca5a5; + padding: 2px 8px; + border-radius: 9999px; + font-size: 11px; + font-weight: 700; +} + +/* Details & Accordion Rows */ +.horaires-page-root details { + background: #f8fafc; + border-radius: var(--horaires-radius-sm); +} + +.horaires-page-root details summary { + list-style: none; + padding: 10px 14px; + cursor: pointer; + font-weight: 700; + color: var(--horaires-blue-800); + display: flex; + align-items: center; + gap: 8px; + transition: background 0.15s ease; +} + +.horaires-page-root details summary:hover { + background: #f1f5f9; +} + +.horaires-page-root details summary::-webkit-details-marker { + display: none; +} + +.horaires-page-root details[open] summary wcs-mat-icon { + transform: rotate(180deg); +} + +/* 7. CARDS VIEW */ +.horaires-cards-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 16px; + margin-bottom: 20px; +} + +.horaires-card-item { + background: #ffffff; + border: 1px solid var(--horaires-border); + border-radius: var(--horaires-radius-md); + padding: 16px 18px; + box-shadow: var(--horaires-shadow-soft); + transition: all 0.2s ease; + display: flex; + flex-direction: column; +} + +.horaires-card-item:hover { + transform: translateY(-2px); + box-shadow: var(--horaires-shadow-card); + border-color: #cbd5e1; +} + +.horaires-card-item.selected { + border-color: var(--horaires-blue-500); + background: #f0f9ff; + box-shadow: 0 0 0 2px var(--horaires-blue-500); +} + +.horaires-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; +} + +.horaires-card-route { + display: flex; + justify-content: space-between; + align-items: center; + background: var(--horaires-surface-soft); + padding: 10px 12px; + border-radius: var(--horaires-radius-sm); + margin-bottom: 12px; + font-size: 13px; +} + +.horaires-card-route-col { + display: flex; + flex-direction: column; +} + +.horaires-card-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: auto; + padding-top: 12px; + border-top: 1px solid var(--horaires-border-soft); +} + +/* 8. SECTION HEADERS IN TABS */ +.horaires-section-header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 18px; + padding: 14px 18px; + border-radius: var(--horaires-radius-md); + border: 1px solid var(--horaires-border); + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); + box-shadow: var(--horaires-shadow-soft); +} + +.horaires-section-title { + margin: 0; + color: var(--horaires-blue-900); + font-size: 19px; + font-weight: 700; + display: flex; + align-items: center; + gap: 8px; +} + +.horaires-section-description { + margin: 0 0 16px; + color: var(--horaires-muted); + font-size: 13.5px; + line-height: 1.5; +} + +/* 9. MODALS */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(6, 28, 47, 0.6); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + padding: 20px; +} + +.modal-content { + background: #ffffff; + border-radius: var(--horaires-radius-lg); + border: 1px solid var(--horaires-border); + box-shadow: 0 25px 50px -12px rgba(15, 23, 42, 0.35); + width: 100%; + max-width: 640px; + max-height: 90vh; + display: flex; + flex-direction: column; + overflow: hidden; + animation: modalFadeIn 0.2s cubic-bezier(0.16, 1, 0.3, 1); +} + +@keyframes modalFadeIn { + from { + opacity: 0; + transform: scale(0.96) translateY(8px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } } .horaires-modal-large { @@ -569,313 +845,659 @@ } .horaires-modal-medium { - max-width: 800px; + max-width: 780px; } .horaires-modal-small { - max-width: 620px; + max-width: 580px; } -.horaires-form-group { +.modal-header { + background: linear-gradient(135deg, #082e4f 0%, #0d3d67 60%, #11507e 100%); + color: #ffffff; + padding: 18px 24px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); +} + +.modal-header h3 { + margin: 0; + font-size: 18px; + font-weight: 700; + color: #ffffff; +} + +.modal-close { + background: rgba(255, 255, 255, 0.12); + border: none; + color: #ffffff; + width: 32px; + height: 32px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + line-height: 1; + transition: all 0.15s ease; +} + +.modal-close:hover { + background: rgba(255, 255, 255, 0.25); +} + +.modal-body { + padding: 22px 26px; + overflow-y: auto; + flex: 1; +} + +.modal-footer { + background: var(--horaires-surface-soft); + border-top: 1px solid var(--horaires-border); + padding: 16px 24px; + display: flex; + justify-content: flex-end; + gap: 10px; + align-items: center; +} + +/* Modal Segmented Tabs */ +.horaires-modal-tabs { + display: flex; + gap: 6px; + background: var(--horaires-surface-soft); + padding: 5px; + border-radius: var(--horaires-radius-md); + border: 1px solid var(--horaires-border); + margin-bottom: 20px; +} + +.horaires-modal-tab-btn { + flex: 1; + padding: 9px 14px; + border: none; + background: transparent; + border-radius: var(--horaires-radius-sm); + color: var(--horaires-muted); + cursor: pointer; + font-weight: 600; + font-size: 13px; + transition: all 0.18s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +.horaires-modal-tab-btn:hover { + color: var(--horaires-text); +} + +.horaires-modal-tab-btn.is-active { + background: #ffffff; + color: var(--horaires-blue-900); + font-weight: 700; + box-shadow: var(--horaires-shadow-soft); +} + +/* Form Styles */ +.form-group { margin-bottom: 16px; } +.form-group label { + display: block; + margin-bottom: 6px; + font-weight: 600; + font-size: 13px; + color: #334155; +} + .horaires-field { width: 100%; - padding: 10px; + padding: 9px 12px; + border: 1px solid var(--horaires-border); + border-radius: var(--horaires-radius-sm); + background: #ffffff; + color: var(--horaires-text); + font-size: 13.5px; box-sizing: border-box; + outline: none; + transition: all 0.15s ease; +} + +.horaires-field:focus { + border-color: var(--horaires-blue-500); + box-shadow: 0 0 0 3px rgba(0, 136, 206, 0.12); } .horaires-two-cols { display: grid; grid-template-columns: 1fr 1fr; - gap: 16px; - margin-bottom: 16px; + gap: 14px; + margin-bottom: 14px; } +/* Tracé intermediate stations */ .horaires-trace-list { - margin-top: 12px; -} - -.horaires-trace-empty { - color: #7991a5; - text-align: center; - padding: 20px; - background: #f6faff; - border: 1px dashed #bfd4e8; - border-radius: 10px; + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 10px; } .horaires-trace-row { display: grid; - grid-template-columns: 34px 2fr 1fr auto; + grid-template-columns: 28px 1fr 140px auto; gap: 8px; align-items: center; - margin-bottom: 8px; + background: var(--horaires-surface-soft); + padding: 8px 12px; + border-radius: var(--horaires-radius-sm); + border: 1px solid var(--horaires-border); } .horaires-trace-index { - color: #4f6f89; - font-weight: 600; + font-weight: 700; + color: var(--horaires-blue-800); + font-size: 12.5px; } +.horaires-trace-empty { + color: var(--horaires-muted); + text-align: center; + padding: 20px; + background: var(--horaires-surface-soft); + border: 1px dashed var(--horaires-border); + border-radius: var(--horaires-radius-sm); + font-size: 13px; +} + +/* PDF Upload Card & Area */ .horaires-pdf-current-card { display: flex; align-items: center; - gap: 12px; - padding: 12px 16px; - background: linear-gradient(120deg, #f0fdf4 0%, #ecfdf5 100%); + gap: 14px; + padding: 14px 18px; + background: #f0fdf4; border: 1px solid #86efac; - border-radius: 10px; - margin-bottom: 20px; + border-radius: var(--horaires-radius-md); + margin-bottom: 18px; } -.horaires-upload-error { - margin-top: 10px; - color: #dc2626; - font-size: 13px; - display: flex; - align-items: center; - gap: 6px; +.horaires-upload-dropzone { + border: 2px dashed var(--horaires-border); + border-radius: var(--horaires-radius-md); + padding: 32px 20px; + text-align: center; + background: var(--horaires-surface-soft); + transition: all 0.2s ease; + cursor: pointer; } -.horaires-upload-loading { - margin-top: 12px; - display: flex; - align-items: center; - gap: 8px; - color: #0074d9; +.horaires-upload-dropzone:hover { + border-color: var(--horaires-blue-500); + background: #f0f9ff; } .horaires-info-note { margin-top: 16px; padding: 10px 14px; background: #fffbeb; - border: 1px solid #fbbf24; - border-radius: 8px; - font-size: 13px; + border: 1px solid #fde68a; + border-radius: var(--horaires-radius-sm); + font-size: 12.5px; color: #92400e; + display: flex; + align-items: center; + gap: 8px; } +/* Quai Cards in Quai Modal */ .horaires-trajet-banner { margin-bottom: 16px; - padding: 10px 14px; - background: linear-gradient(120deg, #eaf5ff 0%, #f2f9ff 100%); - border: 1px solid #93c5fd; - border-radius: 8px; - color: #0b4b78; + padding: 12px 16px; + background: linear-gradient(135deg, #e0f2fe 0%, #f0f9ff 100%); + border: 1px solid #bae6fd; + border-radius: var(--horaires-radius-md); + color: #0369a1; + font-size: 14px; } .horaires-quai-card { - margin-bottom: 12px; - padding: 12px; - background: linear-gradient(180deg, #f8fbff 0%, #f3f8ff 100%); - border: 1px solid #d8e5f3; - border-radius: 10px; + margin-bottom: 10px; + padding: 12px 16px; + background: #ffffff; + border: 1px solid var(--horaires-border); + border-radius: var(--horaires-radius-sm); + transition: all 0.15s ease; +} + +.horaires-quai-card:hover { + border-color: #cbd5e1; + background: var(--horaires-surface-soft); } .horaires-quai-row-modal { display: flex; justify-content: space-between; align-items: center; - gap: 8px; + gap: 12px; } .horaires-quai-select { - padding: 8px; - min-width: 220px; + padding: 8px 12px; + border: 1px solid var(--horaires-border); + border-radius: var(--horaires-radius-sm); + background: #ffffff; + min-width: 200px; + font-size: 13px; + outline: none; } -@media (max-width: 1200px) { - .horaires-main-content { - padding: 12px; - } - - .horaires-search-input { - min-width: 220px; - } +.horaires-status-success { + display: inline-flex; + align-items: center; + gap: 4px; + color: #16a34a; + font-weight: 600; + font-size: 12.5px; } +.horaires-status-muted { + color: #94a3b8; + font-size: 12.5px; +} + +/* Empty & Loading states */ +.horaires-loading { + color: var(--horaires-muted); + padding: 24px; + text-align: center; + font-size: 14px; +} + +.horaires-empty-state { + text-align: center; + padding: 48px 24px; + border-radius: var(--horaires-radius-lg); + border: 1px dashed #cbd5e1; + background: var(--horaires-surface-soft); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; +} + +.horaires-empty-icon { + font-size: 48px; + color: #94a3b8; +} + +.horaires-empty-text { + color: #64748b; + font-size: 15px; + font-weight: 500; + margin: 0; +} + +/* Responsive */ @media (max-width: 1024px) { - .horaires-page .table-container { - border: none; - box-shadow: none; - background: transparent; + .horaires-modern-container { + padding: 20px 18px 48px; } - - .horaires-responsive-table { - display: block; - width: 100%; - border: 0; - background: transparent; + .horaires-kpi-grid { + grid-template-columns: repeat(2, 1fr); } - - .horaires-responsive-table thead { - display: none; + .horaires-hero-banner { + padding: 26px 28px; } - - .horaires-responsive-table tbody { - display: grid; - gap: 12px; - } - - .horaires-responsive-table tbody tr { - display: grid; - gap: 8px; - padding: 12px; - border-radius: 14px; - border: 1px solid #cfe1f2; - background: linear-gradient(145deg, #ffffff 0%, #f3f9ff 100%); - box-shadow: 0 10px 24px rgba(8, 46, 79, 0.11); - } - - .horaires-responsive-table tbody td { - display: grid; - grid-template-columns: minmax(130px, 38%) 1fr; - gap: 8px; - align-items: start; - border: 0 !important; - padding: 0; - min-height: 22px; - } - - .horaires-responsive-table tbody td::before { - content: attr(data-label); - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.06em; - font-weight: 700; - color: #19527f; - padding-top: 2px; - } - - .horaires-responsive-table tbody td[data-label="Actions"], - .horaires-responsive-table tbody td[data-label="Action"] { - grid-template-columns: 1fr; - gap: 10px; - margin-top: 2px; - padding-top: 8px; - border-top: 1px dashed #c7dcef !important; - } - - .horaires-responsive-table tbody td[data-label="Actions"]::before, - .horaires-responsive-table tbody td[data-label="Action"]::before { - padding-top: 0; - } - - .horaires-responsive-table .horaires-row-actions { - display: flex; - flex-wrap: wrap; - gap: 8px; - } - - .horaires-responsive-table tbody td[data-label="Sélection"] { - grid-template-columns: 1fr; - } - - .horaires-responsive-table tbody td[data-label="Sélection"]::before { - display: none; - } - - .horaires-responsive-table tbody td[colspan] { - display: block; - padding: 0 !important; - } - - .horaires-responsive-table tbody td[colspan]::before { - display: none; - } - - .horaires-responsive-table details { - border: 1px solid #cfe1f2; - background: linear-gradient(145deg, #ffffff 0%, #f5faff 100%); - } - - .horaires-responsive-table-horaires tbody td[data-label="N° Train"] strong, - .horaires-responsive-table-lignes tbody td[data-label="Nom"] strong, - .horaires-responsive-table-services tbody td[data-label="Nom"] strong, - .horaires-responsive-table-quais tbody td[data-label="N° Train"] strong { - color: #0f4e79; - font-size: 15px; - } -} - -@media (max-width: 980px) { - .horaires-main-content { - padding-left: 10px; - } - .horaires-workspace { flex-direction: column; } - - .horaires-workspace > div:first-child { + .horaires-lines-sidebar { width: 100% !important; min-width: 100% !important; - } - - .horaires-toolbar { - flex-direction: column; - align-items: stretch; - } - - .horaires-search-wrap, - .horaires-select-btn { - margin-left: 0; - } - - .horaires-search-input { - width: 100%; - min-width: 0; - } - - .horaires-search-input-wide { - min-width: 0; - } - - .horaires-section-header { - flex-direction: column; - align-items: stretch; - } - - .horaires-row-actions { - flex-wrap: wrap; - } - - .horaires-responsive-table tbody td { - grid-template-columns: 1fr; - gap: 4px; - } - - .horaires-responsive-table tbody td::before { - font-size: 10px; - } - - .horaires-two-cols { - grid-template-columns: 1fr; - } - - .horaires-trace-row { - grid-template-columns: 1fr; - } - - .horaires-pdf-current-card { - flex-direction: column; - align-items: flex-start; - } - - .horaires-quai-select { - width: 100%; - min-width: 0; - } - - .horaires-page .modal-content { - width: min(96vw, 900px); - } - - .horaires-page .quai-row-modal { - flex-direction: column !important; - align-items: flex-start !important; + max-height: 240px; } } +@media (max-width: 768px) { + .horaires-modern-container { + padding: 16px 12px 40px; + } + .horaires-hero-banner { + flex-direction: column; + align-items: flex-start; + padding: 20px 18px; + gap: 16px; + } + .horaires-hero-actions { + width: 100%; + display: flex; + gap: 8px; + } + .horaires-hero-actions > * { + flex: 1; + justify-content: center; + } + .horaires-kpi-grid { + grid-template-columns: 1fr; + gap: 12px; + } + .horaires-toolbar { + flex-direction: column; + align-items: stretch; + gap: 12px; + } + .horaires-toolbar-left { + min-width: 0; + flex-direction: column; + align-items: stretch; + gap: 10px; + } + .horaires-search-wrap { + max-width: 100%; + } + .horaires-two-cols { + grid-template-columns: 1fr; + } + .horaires-trace-row { + grid-template-columns: 24px 1fr; + gap: 6px; + } + .horaires-cards-grid { + grid-template-columns: 1fr; + } + .modal-content { + max-width: 95vw; + max-height: 95vh; + } +} + +@media (max-width: 480px) { + .horaires-hero-title { + font-size: 24px; + } + .horaires-hero-subtitle { + font-size: 13px; + } +} + +/* ═══════════════════════════════════════════════════════════════════════════ + 10. DARK MODE SUPPORT + ═══════════════════════════════════════════════════════════════════════════ */ + +body.dark-mode .horaires-page-root, +[data-theme="dark"] .horaires-page-root { + background-color: #0b111e; + color: #f1f5f9; +} + +body.dark-mode .horaires-hero-banner, +[data-theme="dark"] .horaires-hero-banner { + background: linear-gradient(135deg, #05162a 0%, #082d4d 50%, #0d4b78 100%); + border-color: rgba(255, 255, 255, 0.1); + box-shadow: 0 16px 36px -10px rgba(0, 0, 0, 0.6); +} + +body.dark-mode .horaires-kpi-card, +[data-theme="dark"] .horaires-kpi-card { + background: #151f30; + border-color: #243247; + color: #f1f5f9; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); +} + +body.dark-mode .horaires-kpi-card:hover, +[data-theme="dark"] .horaires-kpi-card:hover { + border-color: #3b82f6; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); +} + +body.dark-mode .horaires-kpi-card.active, +[data-theme="dark"] .horaires-kpi-card.active { + background: #0d2744; + border-color: #0088ce; + box-shadow: 0 8px 24px rgba(0, 136, 206, 0.25); +} + +body.dark-mode .horaires-kpi-label, +[data-theme="dark"] .horaires-kpi-label { + color: #94a3b8; +} + +body.dark-mode .horaires-kpi-value, +[data-theme="dark"] .horaires-kpi-value { + color: #f8fafc; +} + +body.dark-mode .horaires-kpi-subtext, +[data-theme="dark"] .horaires-kpi-subtext { + color: #64748b; +} + +body.dark-mode .horaires-main-tabs-nav, +[data-theme="dark"] .horaires-main-tabs-nav { + background: #151f30; + border-color: #243247; +} + +body.dark-mode .horaires-main-tab-btn, +[data-theme="dark"] .horaires-main-tab-btn { + color: #94a3b8; +} + +body.dark-mode .horaires-main-tab-btn:hover, +[data-theme="dark"] .horaires-main-tab-btn:hover { + background: #1e293b; + color: #f8fafc; +} + +body.dark-mode .horaires-main-tab-btn.active, +[data-theme="dark"] .horaires-main-tab-btn.active { + background: linear-gradient(135deg, #0284c7 0%, #0088ce 100%); + color: #ffffff; +} + +body.dark-mode .horaires-main-tab-badge, +[data-theme="dark"] .horaires-main-tab-badge { + background: rgba(255, 255, 255, 0.15); + color: #cbd5e1; +} + +body.dark-mode .horaires-workspace, +body.dark-mode .horaires-main-content, +[data-theme="dark"] .horaires-workspace, +[data-theme="dark"] .horaires-main-content { + background: #151f30; + border-color: #243247; + color: #f1f5f9; +} + +body.dark-mode .horaires-lines-sidebar, +[data-theme="dark"] .horaires-lines-sidebar { + background: linear-gradient(180deg, #07192c 0%, #0c2642 50%, #103459 100%); + border-color: rgba(255, 255, 255, 0.08); +} + +body.dark-mode .horaires-toolbar, +[data-theme="dark"] .horaires-toolbar { + background: #0f172a; + border-color: #243247; +} + +body.dark-mode .horaires-search-input, +[data-theme="dark"] .horaires-search-input { + background: #0b111e; + border-color: #243247; + color: #f8fafc; +} + +body.dark-mode .horaires-search-input:focus, +[data-theme="dark"] .horaires-search-input:focus { + border-color: #0088ce; + box-shadow: 0 0 0 3px rgba(0, 136, 206, 0.25); +} + +body.dark-mode .table-container, +body.dark-mode .horaires-page-root .wcs-table, +[data-theme="dark"] .table-container, +[data-theme="dark"] .horaires-page-root .wcs-table { + background: #151f30; + border-color: #243247; +} + +body.dark-mode .horaires-page-root .wcs-table thead tr, +[data-theme="dark"] .horaires-page-root .wcs-table thead tr { + background: #0f172a; +} + +body.dark-mode .horaires-page-root .wcs-table th, +[data-theme="dark"] .horaires-page-root .wcs-table th { + color: #94a3b8; +} + +body.dark-mode .horaires-page-root .wcs-table td, +[data-theme="dark"] .horaires-page-root .wcs-table td { + border-bottom-color: #1e293b; + color: #e2e8f0; +} + +body.dark-mode .horaires-page-root .wcs-table tbody tr:hover, +[data-theme="dark"] .horaires-page-root .wcs-table tbody tr:hover { + background-color: #1a273c; +} + +body.dark-mode .train-num-badge, +[data-theme="dark"] .train-num-badge { + color: #38bdf8; +} + +body.dark-mode .train-type-pill, +[data-theme="dark"] .train-type-pill { + background: #1e293b; + border-color: #334155; + color: #cbd5e1; +} + +body.dark-mode .circ-days-badge, +[data-theme="dark"] .circ-days-badge { + color: #94a3b8; +} + +body.dark-mode .horaires-card-item, +[data-theme="dark"] .horaires-card-item { + background: #151f30; + border-color: #243247; + color: #f1f5f9; +} + +body.dark-mode .horaires-card-item:hover, +[data-theme="dark"] .horaires-card-item:hover { + border-color: #3b82f6; + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.45); +} + +body.dark-mode .horaires-card-route, +[data-theme="dark"] .horaires-card-route { + background: #0f172a; +} + +body.dark-mode .horaires-section-header, +[data-theme="dark"] .horaires-section-header { + background: #151f30; + border-color: #243247; +} + +body.dark-mode .horaires-section-title, +[data-theme="dark"] .horaires-section-title { + color: #f8fafc; +} + +body.dark-mode .modal-content, +[data-theme="dark"] .modal-content { + background: #151f30; + border-color: #243247; + color: #f1f5f9; +} + +body.dark-mode .modal-header, +body.dark-mode .modal-footer, +[data-theme="dark"] .modal-header, +[data-theme="dark"] .modal-footer { + background: #0e1726; + border-color: #243247; +} + +body.dark-mode .modal-header h3, +[data-theme="dark"] .modal-header h3 { + color: #f8fafc; +} + +body.dark-mode .horaires-field, +[data-theme="dark"] .horaires-field { + background-color: #0b111e; + border-color: #243247; + color: #f8fafc; +} + +body.dark-mode .horaires-field:focus, +[data-theme="dark"] .horaires-field:focus { + border-color: #0088ce; + box-shadow: 0 0 0 3px rgba(0, 136, 206, 0.25); +} + +body.dark-mode .horaires-modal-tabs, +[data-theme="dark"] .horaires-modal-tabs { + background: #0e1726; + border-color: #243247; +} + +body.dark-mode .horaires-modal-tab-btn, +[data-theme="dark"] .horaires-modal-tab-btn { + color: #94a3b8; +} + +body.dark-mode .horaires-modal-tab-btn.is-active, +[data-theme="dark"] .horaires-modal-tab-btn.is-active { + background: #1e293b; + color: #38bdf8; +} + +body.dark-mode .horaires-quai-card, +[data-theme="dark"] .horaires-quai-card { + background: #101826; + border-color: #243247; + color: #f1f5f9; +} + +body.dark-mode .horaires-quai-select, +[data-theme="dark"] .horaires-quai-select { + background-color: #0b111e; + border-color: #243247; + color: #f8fafc; +} + +body.dark-mode .horaires-upload-dropzone, +[data-theme="dark"] .horaires-upload-dropzone { + background: #0f172a; + border-color: #243247; +} + +body.dark-mode .horaires-empty-state, +[data-theme="dark"] .horaires-empty-state { + background: #151f30; + border-color: #243247; +} + +body.dark-mode .horaires-empty-text, +[data-theme="dark"] .horaires-empty-text { + color: #94a3b8; +} + diff --git a/src/pages/horaires/Horaires.js b/src/pages/horaires/Horaires.js index 2672ce5..d96b58e 100644 --- a/src/pages/horaires/Horaires.js +++ b/src/pages/horaires/Horaires.js @@ -1,9 +1,8 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import NavPanel from '../../components/nav-panel/NavPanel'; import './Horaires.css'; import { getAllHoraires, - createHoraire, - updateHoraire, deleteHoraire, formatHeure, getJoursCirculation, @@ -14,7 +13,7 @@ import { import { getAllMateriels } from '../../api/materielsRoulants'; import { getAllGares } from '../../api/gares'; import { getAllTypesTrains } from '../../api/typesTrains'; -import { getAllLignes, createLigne, updateLigne, deleteLigne, uploadFicheHoraire, deleteFicheHoraire } from '../../api/lignes'; +import { getAllLignes, createLigne, updateLigne, deleteLigne, deleteFicheHoraire } from '../../api/lignes'; import { getAllServicesAnnuels, createServiceAnnuel, updateServiceAnnuel, deleteServiceAnnuel, formatDate } from '../../api/servicesAnnuels'; import { getAllRegions } from '../../api/regions'; import { canPerform, getCurrentUser, isRegionFiltered, getUserRegionId } from '../../utils/userUtils'; @@ -25,191 +24,9 @@ import * as XLSX from 'xlsx'; import HoraireModal from '../../components/HoraireModal/HoraireModal'; export default function Horaires() { - // Onglet principal - const [mainTab, setMainTab] = useState('horaires'); - - // AssignLigneModal - const [assignLigneModalOpen, setAssignLigneModalOpen] = useState(false); - const [assignTargetLigne, setAssignTargetLigne] = useState(''); - const [assignLoading, setAssignLoading] = useState(false); - - // Données - const [horaires, setHoraires] = useState([]); - const [materiels, setMateriels] = useState([]); - const [garesList, setGaresList] = useState([]); - const [typesTrainsList, setTypesTrainsList] = useState([]); - const [lignes, setLignes] = useState([]); - const [servicesAnnuels, setServicesAnnuels] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - - // UI - const [viewMode, setViewMode] = useState('liste'); - const [showModal, setShowModal] = useState(false); - // eslint-disable-next-line no-unused-vars - const [gareSuggestionsOpenFor, setGareSuggestionsOpenFor] = useState(null); - const [editingItem, setEditingItem] = useState(null); - // eslint-disable-next-line no-unused-vars - const [activeTab, setActiveTab] = useState('general'); - const [saving, setSaving] = useState(false); - const [formError, setFormError] = useState(''); - // Recherche - const [searchQuery, setSearchQuery] = useState(''); - - // Mode sélection et duplication - const [selectionMode, setSelectionMode] = useState(false); - const [selectedHoraires, setSelectedHoraires] = useState([]); - const [isDuplicating, setIsDuplicating] = useState(false); // Indique si on est en train de modifier un horaire dupliqué - - // Pagination - const [currentPage, setCurrentPage] = useState(1); - const PAGE_SIZE = 10; // 10 lignes (numéros de train) par page - - // Champs formulaire - const [numeroTrain, setNumeroTrain] = useState(''); - const [typeTrainId, setTypeTrainId] = useState(''); - const [typeTrainNom, setTypeTrainNom] = useState(''); - const [ligne, setLigne] = useState(''); - const [serviceAnnuelId, setServiceAnnuelId] = useState(''); - const [serviceAnnuelNom, setServiceAnnuelNom] = useState(''); - - // gare départ / arrivée stockent id + nom pour affichage - const [gareDepartId, setGareDepartId] = useState(''); - const [gareDepartNom, setGareDepartNom] = useState(''); - const [heureDepart, setHeureDepart] = useState(''); - - const [gareArriveeId, setGareArriveeId] = useState(''); - const [gareArriveeNom, setGareArriveeNom] = useState(''); - const [heureArrivee, setHeureArrivee] = useState(''); - - const [garesDesservies, setGaresDesservies] = useState([]); - - // jours - const [circuleLundi, setCirculeLundi] = useState(true); - const [circuleMardi, setCirculeMardi] = useState(true); - const [circuleMercredi, setCirculeMercredi] = useState(true); - const [circuleJeudi, setCirculeJeudi] = useState(true); - const [circuleVendredi, setCirculeVendredi] = useState(true); - const [circuleSamedi, setCirculeSamedi] = useState(true); - const [circuleDimanche, setCirculeDimanche] = useState(true); - const [circuleJoursFeries, setCirculeJoursFeries] = useState(false); - const [circuleDimanchesFeries, setCirculeDimanchesFeries] = useState(false); - const [joursNonCirculation, setJoursNonCirculation] = useState([]); - const [joursPersonnalises, setJoursPersonnalises] = useState([]); - // eslint-disable-next-line no-unused-vars - const [showJoursPerso, setShowJoursPerso] = useState(false); - // Périodes de circulation/non-circulation : [{debut: 'YYYY-MM-DD', fin: 'YYYY-MM-DD', circule: true/false}] - // eslint-disable-next-line no-unused-vars - const [periodesCirculation, setPeriodesCirculation] = useState([]); - - // matériel - const [materielRoulantId, setMaterielRoulantId] = useState(null); - // composition de train : [{id: uid_local, materiel_id, materiel_nom, materiel_nom_technique, materiel_image_url, position}] - const [compositionTrain, setCompositionTrain] = useState([]); - // drag & drop composition - // eslint-disable-next-line no-unused-vars - const [dragOverIndex, setDragOverIndex] = useState(null); - - // Substitution - const [estSubstitution, setEstSubstitution] = useState(false); - const [motifSubstitution, setMotifSubstitution] = useState(''); - - // États pour les lignes - const [showLigneModal, setShowLigneModal] = useState(false); - const [editingLigne, setEditingLigne] = useState(null); - const [ligneNom, setLigneNom] = useState(''); - const [ligneCode, setLigneCode] = useState(''); - const [ligneDepartId, setLigneDepartId] = useState(''); - const [ligneArriveeId, setLigneArriveeId] = useState(''); - const [garesTracees, setGaresTracees] = useState([]); - // Fiche Horaire - const [ligneFHUrl, setLigneFHUrl] = useState(''); - const [ligneFHFile, setLigneFHFile] = useState(null); - const [ligneFHUploading, setLigneFHUploading] = useState(false); - const [ligneFHError, setLigneFHError] = useState(''); - const [ligneModalTab, setLigneModalTab] = useState('infos'); - const [ligneRegion, setLigneRegion] = useState(''); - const originalGaresTraceesRef = useRef(null); - const [regionsList, setRegionsList] = useState([]); - - // Initialiser quaiAssignments depuis les horaires chargés - useEffect(() => { - if (!horaires || horaires.length === 0) return; - const initial = {}; - horaires.forEach(h => { - if (!h || !h.id) return; - const gares = Array.isArray(h.gares_desservies) ? h.gares_desservies : []; - // Gare de départ - if (h.quai_depart) initial[`${h.id}_depart`] = h.quai_depart; - // Gares intermédiaires (depuis JSONB gares_desservies) - gares.forEach((g, idx) => { - if (g && g.quai) { - const key = `gare_${g.ordre ?? idx + 1}`; - initial[`${h.id}_${key}`] = g.quai; - } - }); - // Gare d'arrivée - if (h.quai_arrivee) initial[`${h.id}_arrivee`] = h.quai_arrivee; - }); - setQuaiAssignments(prev => ({ ...initial, ...prev })); - }, [horaires]); - // Si on arrive avec un paramètre gtfs_import, préremplir la modal d'horaire - useEffect(() => { - try { - const params = new URLSearchParams(window.location.search); - const gtfsEncoded = params.get('gtfs_import'); - if (!gtfsEncoded) return; - const decoded = decodeURIComponent(gtfsEncoded); - const payload = JSON.parse(decoded); - if (!payload) return; - - // Préremplir le formulaire avec les données du payload - setEditingItem(null); - setIsDuplicating(false); - setNumeroTrain(payload.numero_train || ''); - setTypeTrainId(''); setTypeTrainNom(''); - setLigne(payload.ligne || ''); - setGareDepartId(payload.gare_depart_id || ''); setGareDepartNom(payload.gare_depart_nom || ''); setHeureDepart(payload.heure_depart || ''); - setGareArriveeId(payload.gare_arrivee_id || ''); setGareArriveeNom(payload.gare_arrivee_nom || ''); setHeureArrivee(payload.heure_arrivee || ''); - setGaresDesservies(Array.isArray(payload.gares_desservies) ? payload.gares_desservies.filter(g => !g.is_depart && !g.is_arrivee) : []); - // ouvrir la modale - setFormError(''); setActiveTab('general'); setShowModal(true); - // Nettoyer l'URL pour éviter ré-ouverture accidentelle - try { - const u = new URL(window.location.href); - u.searchParams.delete('gtfs_import'); - window.history.replaceState({}, '', u.toString()); - } catch (e) { - // Ne pas bloquer l'utilisateur si le nettoyage de l'URL échoue - console.warn('Impossible de nettoyer le paramètre gtfs_import de l\'URL', e); - } - } catch (e) { - console.error('Erreur parsing gtfs_import param:', e); - } - }, []); - const [showServiceModal, setShowServiceModal] = useState(false); - const [editingService, setEditingService] = useState(null); - const [serviceNom, setServiceNom] = useState(''); - const [serviceCode, setServiceCode] = useState(''); - const [serviceDateDebut, setServiceDateDebut] = useState(''); - const [serviceDateFin, setServiceDateFin] = useState(''); - const [serviceDescription, setServiceDescription] = useState(''); - - // États pour les quais - const [showQuaiModal, setShowQuaiModal] = useState(false); - const [quaiSelectedHoraire, setQuaiSelectedHoraire] = useState(null); - const [quaiAssignments, setQuaiAssignments] = useState({}); - const [quaiSaving, setQuaiSaving] = useState(false); - const [quaiSearchQuery, setQuaiSearchQuery] = useState(''); - - const [quaiViewMode, setQuaiViewMode] = useState('liste'); // 'liste' or 'schematique' - const [quaiGareSelectionneeId, setQuaiGareSelectionneeId] = useState(''); - - // Panneau latéral lignes - const [selectedLigneFilter, setSelectedLigneFilter] = useState(null); // null = toutes les lignes - const [lignesPanelOpen, setLignesPanelOpen] = useState(true); - const currentUser = getCurrentUser(); + + // Permissions RBAC const canCreateHoraires = canPerform(currentUser, 'horaires', 'create'); const canUpdateHoraires = canPerform(currentUser, 'horaires', 'update'); const canDeleteHoraires = canPerform(currentUser, 'horaires', 'delete'); @@ -220,12 +37,110 @@ export default function Horaires() { const canUpdateServices = canPerform(currentUser, 'horaires', 'update'); const canDeleteServices = canPerform(currentUser, 'horaires', 'delete'); - // Chargement initial + // Onglet principal ('horaires' | 'lignes' | 'services' | 'quais') + const [mainTab, setMainTab] = useState('horaires'); + + // Données + const [horaires, setHoraires] = useState([]); + const [materiels, setMateriels] = useState([]); + const [garesList, setGaresList] = useState([]); + const [typesTrainsList, setTypesTrainsList] = useState([]); + const [lignes, setLignes] = useState([]); + const [servicesAnnuels, setServicesAnnuels] = useState([]); + const [regionsList, setRegionsList] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + // UI Horaires + const [viewMode, setViewMode] = useState('liste'); // 'liste' | 'carte' + const [showModal, setShowModal] = useState(false); + const [editingItem, setEditingItem] = useState(null); + const [isDuplicating, setIsDuplicating] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + + // Mode sélection et duplication + const [selectionMode, setSelectionMode] = useState(false); + const [selectedHoraires, setSelectedHoraires] = useState([]); + + // Pagination + const [currentPage, setCurrentPage] = useState(1); + const PAGE_SIZE = 10; + + // Tri + const [sortColumn, setSortColumn] = useState('numero_train'); + const [sortDirection, setSortDirection] = useState('asc'); + + // Panneau latéral lignes (Filtre) + const [selectedLigneFilter, setSelectedLigneFilter] = useState(null); // null = toutes les lignes, '__none__' = sans ligne, id = id de ligne + const [lignesPanelOpen, setLignesPanelOpen] = useState(true); + + // AssignLigneModal + const [assignLigneModalOpen, setAssignLigneModalOpen] = useState(false); + const [assignTargetLigne, setAssignTargetLigne] = useState(''); + const [assignLoading, setAssignLoading] = useState(false); + + // Modal Lignes + const [showLigneModal, setShowLigneModal] = useState(false); + const [editingLigne, setEditingLigne] = useState(null); + const [ligneNom, setLigneNom] = useState(''); + const [ligneCode, setLigneCode] = useState(''); + const [ligneDepartId, setLigneDepartId] = useState(''); + const [ligneArriveeId, setLigneArriveeId] = useState(''); + const [garesTracees, setGaresTracees] = useState([]); + const [ligneFHUrl, setLigneFHUrl] = useState(''); + const [ligneFHFile, setLigneFHFile] = useState(null); + const [ligneFHUploading, setLigneFHUploading] = useState(false); + const [ligneFHError, setLigneFHError] = useState(''); + const [ligneModalTab, setLigneModalTab] = useState('infos'); + const [ligneRegion, setLigneRegion] = useState(''); + const originalGaresTraceesRef = useRef(null); + + // Modal Services Annuels + const [showServiceModal, setShowServiceModal] = useState(false); + const [editingService, setEditingService] = useState(null); + const [serviceNom, setServiceNom] = useState(''); + const [serviceCode, setServiceCode] = useState(''); + const [serviceDateDebut, setServiceDateDebut] = useState(''); + const [serviceDateFin, setServiceDateFin] = useState(''); + const [serviceDescription, setServiceDescription] = useState(''); + + // États pour l'attribution des quais + const [showQuaiModal, setShowQuaiModal] = useState(false); + const [quaiSelectedHoraire, setQuaiSelectedHoraire] = useState(null); + const [quaiAssignments, setQuaiAssignments] = useState({}); + const [quaiSaving, setQuaiSaving] = useState(false); + const [quaiSearchQuery, setQuaiSearchQuery] = useState(''); + const [quaiViewMode, setQuaiViewMode] = useState('liste'); // 'liste' | 'schematique' | 'depart_arrivee' + const [quaiGareSelectionneeId, setQuaiGareSelectionneeId] = useState(''); + + // Form error & saving + const [formError, setFormError] = useState(''); + const [saving, setSaving] = useState(false); + + // Initialiser quaiAssignments depuis les horaires chargés + useEffect(() => { + if (!horaires || horaires.length === 0) return; + const initial = {}; + horaires.forEach(h => { + if (!h || !h.id) return; + const gares = Array.isArray(h.gares_desservies) ? h.gares_desservies : []; + if (h.quai_depart) initial[`${h.id}_depart`] = h.quai_depart; + gares.forEach((g, idx) => { + if (g && g.quai) { + const key = `gare_${g.ordre ?? idx + 1}`; + initial[`${h.id}_${key}`] = g.quai; + } + }); + if (h.quai_arrivee) initial[`${h.id}_arrivee`] = h.quai_arrivee; + }); + setQuaiAssignments(prev => ({ ...initial, ...prev })); + }, [horaires]); + + // Chargement des données const loadData = useCallback(async () => { setLoading(true); setError(''); try { - // Filtrage RBAC par région const user = getCurrentUser(); const regionId = isRegionFiltered(user) ? getUserRegionId(user) : null; @@ -248,7 +163,6 @@ export default function Horaires() { if (!regionsRes.error) setRegionsList(Array.isArray(regionsRes.data) ? regionsRes.data : []); if (horairesRes.error || materielsRes.error || garesRes.error || typesTrainsRes.error || lignesRes.error || servicesRes.error) { - // afficher un message minimal setError((horairesRes.error || materielsRes.error || garesRes.error || typesTrainsRes.error || lignesRes.error || servicesRes.error).message || 'Erreur lors du chargement'); } } catch (err) { @@ -258,775 +172,104 @@ export default function Horaires() { setLoading(false); }, []); - useEffect(() => { const id = setTimeout(() => { loadData(); }, 0); return () => clearTimeout(id); }, [loadData]); + useEffect(() => { + const id = setTimeout(() => { loadData(); }, 0); + return () => clearTimeout(id); + }, [loadData]); - // handlers pour assignation - const handleAssignSelectedToLigne = async () => { - if (selectedHoraires.length === 0 || !assignTargetLigne) return; - setAssignLoading(true); - - const targetLigneObj = lignes.find(l => String(l.id) === assignTargetLigne); - const targetNom = targetLigneObj ? targetLigneObj.nom : ''; + // Si on arrive avec un paramètre gtfs_import, préremplir la modal d'horaire + useEffect(() => { + try { + const params = new URLSearchParams(window.location.search); + const gtfsEncoded = params.get('gtfs_import'); + if (!gtfsEncoded) return; + const decoded = decodeURIComponent(gtfsEncoded); + const payload = JSON.parse(decoded); + if (!payload) return; - const { error } = await updateHorairesLigneBatch(selectedHoraires, assignTargetLigne, targetNom); - if (error) { - alert('Erreur lors de l\'attribution.'); - } else { - await loadData(); - setSelectedHoraires([]); - setAssignLigneModalOpen(false); - setAssignTargetLigne(''); - setSelectionMode(false); - } - setAssignLoading(false); - }; + setEditingItem(payload); + setIsDuplicating(false); + setShowModal(true); - // helpers gares desservies (useCallback pour stabilité) - // eslint-disable-next-line no-unused-vars - const addGareDesservie = useCallback(() => { - setGaresDesservies(prev => ([...prev, { id: `new-${Date.now()}`, gare_id: '', gare_nom: '', heure_arrivee: '', heure_depart: '', ordre: prev.length + 1 }])); - }, []); - - // eslint-disable-next-line no-unused-vars - const updateGareDesservieField = useCallback((index, field, value) => { - setGaresDesservies(prev => { - const copy = [...prev]; - copy[index] = { ...copy[index], [field]: value }; - return copy; - }); - }, []); - - // eslint-disable-next-line no-unused-vars - const removeGareDesservie = useCallback((index) => { - setGaresDesservies(prev => { - const filtered = prev.filter((_, i) => i !== index); - return filtered.map((g, i) => ({ ...g, ordre: i + 1 })); - }); - }, []); - - // eslint-disable-next-line no-unused-vars - const moveGareUp = useCallback((index) => { - if (index === 0) return; - setGaresDesservies(prev => { - const copy = [...prev]; - const [moved] = copy.splice(index, 1); - copy.splice(index - 1, 0, moved); - return copy.map((g, i) => ({ ...g, ordre: i + 1 })); - }); - }, []); - - // eslint-disable-next-line no-unused-vars - const moveGareDown = useCallback((index) => { - setGaresDesservies(prev => { - if (index === prev.length - 1) return prev; - const copy = [...prev]; - const [moved] = copy.splice(index, 1); - copy.splice(index + 1, 0, moved); - return copy.map((g, i) => ({ ...g, ordre: i + 1 })); - }); - }, []); - - const findGareFromSearch = useCallback((query) => { - const value = String(query || '').trim(); - if (!value) return null; - const lower = value.toLowerCase(); - return garesList.find(g => String(g.id) === value || String(g.nom || '').toLowerCase() === lower) || null; - }, [garesList]); - - // eslint-disable-next-line no-unused-vars - const applyMainGareSelection = useCallback((value, setId, setNom) => { - const selected = findGareFromSearch(value); - if (selected) { - setId(String(selected.id)); - setNom(selected.nom || ''); - return; - } - setId(''); - setNom(value || ''); - }, [findGareFromSearch]); - - // eslint-disable-next-line no-unused-vars - const getMatchingGares = useCallback((query) => { - const value = String(query || '').trim().toLowerCase(); - if (!value || value.length < 2) return []; - return (garesList || []) - .filter(g => String(g?.nom || '').toLowerCase().includes(value)) - .slice(0, 8); - }, [garesList]); - - const resetForm = useCallback(() => { - setNumeroTrain(''); - setTypeTrainId(''); setTypeTrainNom(''); - setLigne(''); - setGareDepartId(''); setGareDepartNom(''); setHeureDepart(''); - setGareArriveeId(''); setGareArriveeNom(''); setHeureArrivee(''); - setGaresDesservies([]); - setCirculeLundi(true); setCirculeMardi(true); setCirculeMercredi(true); - setCirculeJeudi(true); setCirculeVendredi(true); setCirculeSamedi(true); setCirculeDimanche(true); - setCirculeJoursFeries(false); setCirculeDimanchesFeries(false); - setJoursNonCirculation([]); setJoursPersonnalises([]); setShowJoursPerso(false); - setMaterielRoulantId(null); - setCompositionTrain([]); - setDragOverIndex(null); - setEstSubstitution(false); setMotifSubstitution(''); - setServiceAnnuelId(''); setServiceAnnuelNom(''); - setFormError(''); setActiveTab('general'); - // NB: ne pas modifier l'état `showModal` ici : resetForm ne doit pas ouvrir/fermer la modale - setIsDuplicating(false); - }, []); - - // Trouve le service annuel "actif en ce moment". - // Critères: champ `actif` truthy et la date courante est entre date_debut et date_fin (inclus). - // Si plusieurs correspondent, on prend celui ayant la date_debut la plus récente. - // eslint-disable-next-line no-unused-vars - const getActiveService = useCallback(() => { - if (!Array.isArray(servicesAnnuels) || servicesAnnuels.length === 0) return null; - const today = new Date(); - const inRange = servicesAnnuels.filter(s => s && (s.actif === undefined || s.actif === true) && s.date_debut && s.date_fin).filter(s => { try { - const start = new Date(s.date_debut); - const end = new Date(s.date_fin); - // Normaliser heures pour neutraliser fuseaux - start.setHours(0,0,0,0); - end.setHours(23,59,59,999); - return start <= today && today <= end; - } catch (e) { return false; } - }); - - if (inRange.length > 0) { - inRange.sort((a, b) => new Date(b.date_debut) - new Date(a.date_debut)); - return inRange[0]; - } - - // fallback: prendre le premier marqué actif si présent - const fallback = servicesAnnuels.find(s => s && (s.actif === undefined || s.actif === true)); - return fallback || null; - }, [servicesAnnuels]); - - // ============================================================================ - // GESTION DU MODE SÉLECTION - // ============================================================================ - - const toggleSelectionMode = () => { - setSelectionMode(!selectionMode); - setSelectedHoraires([]); - }; - - const toggleSelectHoraire = (horaireId) => { - setSelectedHoraires(prev => { - if (prev.includes(horaireId)) { - return prev.filter(id => id !== horaireId); - } else { - return [...prev, horaireId]; + const u = new URL(window.location.href); + u.searchParams.delete('gtfs_import'); + window.history.replaceState({}, '', u.toString()); + } catch (e) { + console.warn('Impossible de nettoyer le paramètre gtfs_import de l\'URL', e); } - }); - }; - - // ============================================================================ - // GESTION DE LA DUPLICATION - // ============================================================================ - - const LOCALSTORAGE_KEY = 'ferrovia_duplicated_horaire'; - - const handleDuplicateSelected = () => { - if (selectedHoraires.length === 0) return; - - // On prend le premier horaire sélectionné pour la duplication - const horaireId = selectedHoraires[0]; - const horaireToDuplicate = horaires.find(h => h.id === horaireId); - - if (!horaireToDuplicate) return; - - // Créer une copie de l'horaire sans l'ID (pour éviter les conflits) - const duplicatedHoraire = { - ...horaireToDuplicate, - id: null, // Sera généré par la BDD lors de la sauvegarde - numero_train: `${horaireToDuplicate.numero_train} (copie)`, - gares_desservies: Array.isArray(horaireToDuplicate.gares_desservies) - ? horaireToDuplicate.gares_desservies.map(g => ({ ...g, id: null })) - : [] - }; - - // Sauvegarder dans le LocalStorage - localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(duplicatedHoraire)); - - // Quitter le mode sélection - setSelectionMode(false); - setSelectedHoraires([]); - - // Ouvrir la modale de modification avec les données dupliquées - openEditModalFromDuplicate(duplicatedHoraire); - }; - - // Supprimer les horaires sélectionnés (soft delete via API) - const handleDeleteSelected = async () => { - if (!canDeleteHoraires) { - alert('Vous n\'avez pas les droits pour supprimer des horaires.'); - return; + } catch (e) { + console.error('Erreur parsing gtfs_import param:', e); } - if (!selectedHoraires || selectedHoraires.length === 0) return; - if (!window.confirm(`Êtes-vous sûr de vouloir supprimer ${selectedHoraires.length} élément(s) ?`)) return; - - setSaving(true); - // Optimistic update : retirer immédiatement de l'UI - const toDelete = new Set(selectedHoraires.map(String)); - setHoraires(prev => prev.filter(h => !toDelete.has(String(h.id)))); - setSelectedHoraires([]); - setSelectionMode(false); - try { - const promises = Array.from(toDelete).map(id => deleteHoraire(id, false)); - const results = await Promise.all(promises); - const errors = results.filter(r => r && r.error).map(r => r.error?.message || JSON.stringify(r.error)); - if (errors.length > 0) { - alert('Certaines suppressions ont échoué :\n' + errors.join('\n')); - await loadData(); // Resync si erreur - } - } catch (err) { - console.error('Erreur lors de la suppression groupée :', err); - alert('Erreur lors de la suppression des éléments sélectionnés'); - await loadData(); // Resync si exception - } finally { - setSaving(false); - } - }; - - // Exporter les horaires sélectionnés - const handleExportSelected = () => { - if (!selectedHoraires || selectedHoraires.length === 0) return; - - const headers = [ - 'id', 'numero_train', 'type_train', 'type_train_id', 'gare_depart_id', - 'gare_depart_nom', 'heure_depart', 'gare_arrivee_id', 'gare_arrivee_nom', - 'heure_arrivee', 'circule_lundi', 'circule_mardi', 'circule_mercredi', - 'circule_jeudi', 'circule_vendredi', 'circule_samedi', 'circule_dimanche', - 'circule_jours_feries', 'circule_dimanches_feries', 'jours_personnalises', - 'jours_non_circulation', 'materiel_roulant_id', 'ligne_id', 'ligne_nom', - 'service_annuel_id', 'service_annuel_nom', 'est_substitution', - 'motif_substitution', 'actif', 'created_at', 'updated_at', 'deleted_at', - 'gares_desservies', 'composition_train', 'substitution_disponible', 'region_id' - ]; - - const dataToExport = horaires - .filter(h => selectedHoraires.includes(h.id)) - .map(h => { - const row = {}; - headers.forEach(header => { - let val = h[header]; - if (Array.isArray(val) || (typeof val === 'object' && val !== null)) { - val = JSON.stringify(val); - } - row[header] = val !== undefined && val !== null ? val : ''; - }); - return row; - }); - - const worksheet = XLSX.utils.json_to_sheet(dataToExport, { header: headers }); - const workbook = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(workbook, worksheet, 'Horaires'); - - // Déterminer le nom du fichier - const uniqueLigneIds = Array.from(new Set(dataToExport.map(h => h.ligne_id).filter(Boolean))); - const matchedLignes = uniqueLigneIds.map(id => lignes.find(l => String(l.id) === String(id))).filter(Boolean); - - let filenameBase = 'export_horaires'; - if (matchedLignes.length === 1) { - const nomLigne = matchedLignes[0].nom || 'Ligne'; - filenameBase = `export_horaires_${nomLigne.replace(/[^a-zA-Z0-9]/g, '_')}`; - } else if (matchedLignes.length > 1) { - const codes = matchedLignes.map(l => l.code || 'NOCODE').filter(Boolean); - filenameBase = `export_horaires_${codes.join('-')}`; - } - - const dateStr = new Date().toISOString().split('T')[0]; - XLSX.writeFile(workbook, `${filenameBase}_${dateStr}.csv`, { bookType: 'csv' }); - }; - - const openEditModalFromDuplicate = (horaire) => { - setEditingItem(horaire); - setIsDuplicating(true); - setShowModal(true); - }; - - const openCreateModal = () => { - if (!canCreateHoraires) { - setFormError('Vous n\'avez pas les droits pour créer un horaire.'); - return; - } - setEditingItem(null); - setIsDuplicating(false); - setShowModal(true); - }; - - const openEditModal = (horaire) => { - if (!canUpdateHoraires) { - setFormError('Vous n\'avez pas les droits pour modifier un horaire.'); - return; - } - if (!horaire) return; - setEditingItem(horaire); - setIsDuplicating(false); - setShowModal(true); - }; - - const closeModal = () => { - - setShowModal(false); - setEditingItem(null); - resetForm(); - }; - - - // Sauvegarde - // eslint-disable-next-line no-unused-vars - const handleSave = async () => { - if (!editingItem && !canCreateHoraires) return setFormError('Vous n\'avez pas les droits pour créer un horaire.'); - if (editingItem && !canUpdateHoraires) return setFormError('Vous n\'avez pas les droits pour modifier un horaire.'); - setFormError(''); - if (!numeroTrain) return setFormError('Le numéro de train est requis.'); - if (!gareDepartId || !gareArriveeId) return setFormError('Les gares de départ et d\'arrivée sont requises.'); - if (!heureDepart || !heureArrivee) return setFormError('Les heures de départ et d\'arrivée sont requises.'); - - setSaving(true); - const selLigne = lignes.find(ll => String(ll.id) === String(ligne)); - const payload = { - numero_train: numeroTrain, - type_train_id: typeTrainId || null, - type_train: typeTrainNom, - ligne_id: ligne || null, - ligne_nom: selLigne ? selLigne.nom : (typeof ligne === 'string' ? ligne : null), - gare_depart_id: gareDepartId, - gare_depart_nom: gareDepartNom, - heure_depart: heureDepart, - gare_arrivee_id: gareArriveeId, - gare_arrivee_nom: gareArriveeNom, - heure_arrivee: heureArrivee, - circule_lundi: circuleLundi, - circule_mardi: circuleMardi, - circule_mercredi: circuleMercredi, - circule_jeudi: circuleJeudi, - circule_vendredi: circuleVendredi, - circule_samedi: circuleSamedi, - circule_dimanche: circuleDimanche, - circule_jours_feries: circuleJoursFeries, - circule_dimanches_feries: circuleDimanchesFeries, - jours_non_circulation: joursNonCirculation, - // Fusionne jours uniques et périodes dans jours_personnalises - jours_personnalises: [ - // Jours uniques (format {date, circule}) - ...joursPersonnalises.filter(j => j && j.date), - // Périodes générales (format {debut, fin, circule}) - ...( - Array.isArray(periodesCirculation) - ? periodesCirculation.filter(p => p && p.debut && p.fin && p.debut <= p.fin).map(p => ({ debut: p.debut, fin: p.fin, circule: p.circule })) - : [] - ) - ], - materiel_roulant_id: materielRoulantId || null, - composition_train: compositionTrain.map((item, idx) => ({ - materiel_id: item.materiel_id, - materiel_nom: item.materiel_nom || '', - materiel_nom_technique: item.materiel_nom_technique || '', - materiel_image_url: item.materiel_image_url || null, - position: idx + 1 - })), - est_substitution: estSubstitution, - motif_substitution: estSubstitution ? motifSubstitution : null, - service_annuel_id: serviceAnnuelId || null, - service_annuel_nom: serviceAnnuelNom || null, - actif: true - }; - - try { - let res; - if (editingItem && editingItem.id) { - // Update existing - res = await updateHoraire(editingItem.id, payload, garesDesservies); - } else { - // Create new - // `garesClean` n'existe pas — utiliser `garesDesservies` qui contient la liste des gares saisies - res = await createHoraire(payload, garesDesservies, currentUser); - } - - if (res?.error) { - setFormError(res.error.message || 'Erreur lors de la sauvegarde'); - setSaving(false); - return; - } - - // Si c'était une duplication, nettoyer le LocalStorage - if (isDuplicating) { - - } - - await loadData(); - setSaving(false); - closeModal(); - } catch (err) { - console.error(err); - setFormError('Erreur inattendue lors de la sauvegarde'); - setSaving(false); - } - }; - - const handleDelete = async (id) => { - if (!canDeleteHoraires) { - alert('Vous n\'avez pas les droits pour supprimer un horaire.'); - return; - } - if (!id) return; - if (!window.confirm('Êtes-vous sûr de vouloir supprimer cet horaire ?')) return; - setSaving(true); - // Optimistic update : retirer immédiatement de l'UI - setHoraires(prev => prev.filter(h => String(h.id) !== String(id))); - const { error } = await deleteHoraire(id, false); - if (error) { - alert('Erreur lors de la suppression: ' + (error.message || 'Erreur')); - await loadData(); // Resync si erreur - } - setSaving(false); - }; - - const areGaresTraceesEqual = (a, b) => { - if (!a || !b) return a === b; - if (a.length !== b.length) return false; - return a.every((g, i) => { - const other = b[i]; - return String(g.gare_id) === String(other.gare_id) && g.desserte === other.desserte && Number(g.ordre) === Number(other.ordre); - }); - }; - - const openCreateLigneModal = () => { - if (!canCreateLignes) { - setFormError('Vous n\'avez pas les droits pour créer une ligne.'); - return; - } - setEditingLigne(null); - setLigneNom(''); - setLigneCode(''); - setLigneDepartId(''); - setLigneArriveeId(''); - setGaresTracees([]); - setLigneFHUrl(''); - setLigneFHFile(null); - setLigneFHError(''); - setLigneModalTab('infos'); - setLigneRegion(''); - setShowLigneModal(true); - }; - - const openEditLigneModal = (ligne) => { - if (!canUpdateLignes) { - setFormError('Vous n\'avez pas les droits pour modifier une ligne.'); - return; - } - if (!ligne) return; - setEditingLigne(ligne); - setLigneNom(ligne.nom || ''); - setLigneCode(ligne.code || ''); - setLigneDepartId(ligne.gare_depart_id || ''); - setLigneArriveeId(ligne.gare_arrivee_id || ''); - setGaresTracees(Array.isArray(ligne.lignes_gares) ? ligne.lignes_gares.map(lg => ({ - id: lg.id, - gare_id: lg.gare?.id || '', - gare_nom: lg.gare?.nom || '', - desserte: lg.desserte || 'tous', - ordre: lg.ordre - })) : []); - setLigneFHUrl(ligne.fh_url || ''); - setLigneFHFile(null); - setLigneFHError(''); - setLigneModalTab('infos'); - setLigneRegion(ligne.region || ''); - setShowLigneModal(true); - const initialGares = ligne.lignes_gares ? ligne.lignes_gares.map(lg => ({ - gare_id: lg.gare?.id || lg.gare_id, - desserte: lg.desserte, - ordre: lg.ordre - })) : []; - originalGaresTraceesRef.current = initialGares; - }; - - const closeLigneModal = () => { - setShowLigneModal(false); - setEditingLigne(null); - originalGaresTraceesRef.current = null; - }; - - const addGareTracee = useCallback(() => { - setGaresTracees(prev => ([...prev, { id: `new-${Date.now()}`, gare_id: '', gare_nom: '', desserte: 'tous', ordre: prev.length + 1 }])); }, []); - const updateGareTraceeField = useCallback((index, field, value) => { - setGaresTracees(prev => { - const copy = [...prev]; - copy[index] = { ...copy[index], [field]: value }; - return copy; + // Stats pour les KPI Cards + const stats = useMemo(() => { + const totalSillons = horaires.length; + const totalLignes = lignes.length; + const totalServices = servicesAnnuels.length; + + // Nombre d'horaires avec au moins un quai attribué + let quaisAttribues = 0; + horaires.forEach(h => { + const hasQuaisState = Object.keys(quaiAssignments).some(k => k.startsWith(`${h.id}_`) && quaiAssignments[k]); + const hasQuaisBDD = normalizeHoraireDesservies(h).all.some(g => g && g.quai); + if (hasQuaisState || hasQuaisBDD) quaisAttribues++; }); - }, []); - const removeGareTracee = useCallback((index) => { - setGaresTracees(prev => { - const filtered = prev.filter((_, i) => i !== index); - return filtered.map((g, i) => ({ ...g, ordre: i + 1 })); - }); - }, []); - - const handleSaveLigne = async () => { - if (!editingLigne && !canCreateLignes) return setFormError('Vous n\'avez pas les droits pour créer une ligne.'); - if (editingLigne && !canUpdateLignes) return setFormError('Vous n\'avez pas les droits pour modifier une ligne.'); - setFormError(''); - if (!ligneNom) return setFormError('Le nom de la ligne est requis.'); - if (!ligneDepartId || !ligneArriveeId) return setFormError('Les gares de départ et d\'arrivée sont requises.'); - - setSaving(true); - - // Gérer l'upload de la fiche horaire si un fichier est sélectionné - let fhUrl = ligneFHUrl || null; - - if (ligneFHFile) { - setLigneFHUploading(true); - // Pour une création, on utilise un ID temporaire ; pour une modif, l'ID existant - const tempId = editingLigne?.id || `tmp-${Date.now()}`; - const { url, error: uploadErr } = await uploadFicheHoraire(tempId, ligneFHFile); - setLigneFHUploading(false); - - if (uploadErr) { - setFormError('Erreur lors de l\'upload de la fiche horaire : ' + (uploadErr.message || uploadErr)); - setSaving(false); - return; - } - fhUrl = url; - } - - const payload = { - nom: ligneNom, - code: ligneCode, - gare_depart_id: ligneDepartId, - gare_arrivee_id: ligneArriveeId, - actif: true, - fh_url: fhUrl, - region: ligneRegion || null + return { + totalSillons, + totalLignes, + totalServices, + quaisAttribues }; + }, [horaires, lignes, servicesAnnuels, quaiAssignments]); - try { - let res; - if (editingLigne && editingLigne.id) { - const currentGaresTracees = garesTracees.map(g => ({ - gare_id: g.gare_id, - desserte: g.desserte, - ordre: g.ordre - })); - - const garesTraceesToSend = areGaresTraceesEqual(currentGaresTracees, originalGaresTraceesRef.current) - ? undefined - : garesTracees; - - res = await updateLigne(editingLigne.id, payload, garesTraceesToSend); - } else { - res = await createLigne(payload, garesTracees); - } - - if (res?.error) { - setFormError(res.error.message || 'Erreur lors de la sauvegarde'); - setSaving(false); - return; - } - - await loadData(); - setSaving(false); - closeLigneModal(); - } catch (err) { - console.error(err); - setFormError('Erreur inattendue lors de la sauvegarde'); - setSaving(false); + // Tri et filtrage des horaires + const compareNumeroTrain = useCallback((a, b) => { + const aStr = String(a?.numero_train || ''); + const bStr = String(b?.numero_train || ''); + const aNum = parseInt(aStr, 10); + const bNum = parseInt(bStr, 10); + if (!isNaN(aNum) && !isNaN(bNum)) { + return aNum - bNum; } - }; - - const handleDeleteLigne = async (id) => { - if (!canDeleteLignes) { - alert('Vous n\'avez pas les droits pour supprimer une ligne.'); - return; - } - if (!id || !window.confirm('Êtes-vous sûr de vouloir supprimer cette ligne ?')) return; - setSaving(true); - const { error } = await deleteLigne(id, true); - if (error) alert('Erreur lors de la suppression: ' + (error.message || 'Erreur')); - await loadData(); - setSaving(false); - }; - - // ============================================================================ - // GESTION DES SERVICES ANNUELS - // ============================================================================ - - const openCreateServiceModal = () => { - if (!canCreateServices) { - setFormError('Vous n\'avez pas les droits pour créer un service annuel.'); - return; - } - setEditingService(null); - setServiceNom(''); - setServiceCode(''); - setServiceDateDebut(''); - setServiceDateFin(''); - setServiceDescription(''); - setShowServiceModal(true); - }; - - const openEditServiceModal = (service) => { - if (!canUpdateServices) { - setFormError('Vous n\'avez pas les droits pour modifier un service annuel.'); - return; - } - if (!service) return; - setEditingService(service); - setServiceNom(service.nom || ''); - setServiceCode(service.code || ''); - setServiceDateDebut(service.date_debut || ''); - setServiceDateFin(service.date_fin || ''); - setServiceDescription(service.description || ''); - setShowServiceModal(true); - }; - - const closeServiceModal = () => { - setShowServiceModal(false); - setEditingService(null); - }; - - const handleSaveService = async () => { - if (!editingService && !canCreateServices) return setFormError('Vous n\'avez pas les droits pour créer un service annuel.'); - if (editingService && !canUpdateServices) return setFormError('Vous n\'avez pas les droits pour modifier un service annuel.'); - setFormError(''); - if (!serviceNom) return setFormError('Le nom du service annuel est requis.'); - if (!serviceDateDebut || !serviceDateFin) return setFormError('Les dates de début et de fin sont requises.'); - - setSaving(true); - const payload = { - nom: serviceNom, - code: serviceCode, - date_debut: serviceDateDebut, - date_fin: serviceDateFin, - description: serviceDescription, - actif: true - }; - - try { - let res; - if (editingService && editingService.id) { - res = await updateServiceAnnuel(editingService.id, payload); - } else { - res = await createServiceAnnuel(payload); - } - - if (res?.error) { - setFormError(res.error.message || 'Erreur lors de la sauvegarde'); - setSaving(false); - return; - } - - await loadData(); - setSaving(false); - closeServiceModal(); - } catch (err) { - console.error(err); - setFormError('Erreur inattendue lors de la sauvegarde'); - setSaving(false); - } - }; - - const handleDeleteService = async (id) => { - if (!canDeleteServices) { - alert('Vous n\'avez pas les droits pour supprimer un service annuel.'); - return; - } - if (!id || !window.confirm('Êtes-vous sûr de vouloir supprimer ce service annuel ?')) return; - setSaving(true); - const { error } = await deleteServiceAnnuel(id, true); - if (error) alert('Erreur lors de la suppression: ' + (error.message || 'Erreur')); - await loadData(); - setSaving(false); - }; - - // Render - - // ============================================================================ - // GESTION DU TRI - // ============================================================================ - - const [sortColumn, setSortColumn] = useState(null); - const [sortDirection, setSortDirection] = useState('asc'); - - const handleSort = (column) => { - if (sortColumn === column) { - // Si on clique sur la même colonne, inverser la direction - setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); - } else { - // Nouvelle colonne, tri ascendant par défaut - setSortColumn(column); - setSortDirection('asc'); - } - }; - - const compareNumeroTrain = useCallback((aValue, bValue) => { - const aStr = String(aValue || ''); - const bStr = String(bValue || ''); - return aStr.localeCompare(bStr, undefined, { numeric: true, sensitivity: 'base' }); + return aStr.localeCompare(bStr, undefined, { numeric: true }); }, []); - const getSortedHoraires = useCallback((horairesArray) => { - if (!sortColumn) return horairesArray; - - return [...horairesArray].sort((a, b) => { - let aVal, bVal; - + const getSortedHoraires = useCallback((list) => { + if (!sortColumn) return list; + return [...list].sort((a, b) => { + let comparison = 0; switch (sortColumn) { case 'numero_train': - return sortDirection === 'asc' - ? compareNumeroTrain(a?.numero_train, b?.numero_train) - : compareNumeroTrain(b?.numero_train, a?.numero_train); + comparison = compareNumeroTrain(a, b); + break; case 'type_train': - aVal = a?.type_train || ''; - bVal = b?.type_train || ''; + comparison = String(a.type_train || '').localeCompare(String(b.type_train || '')); break; case 'gare_depart': - aVal = a?.gare_depart_nom || ''; - bVal = b?.gare_depart_nom || ''; + comparison = String(a.gare_depart_nom || '').localeCompare(String(b.gare_depart_nom || '')); break; case 'gare_arrivee': - aVal = a?.gare_arrivee_nom || ''; - bVal = b?.gare_arrivee_nom || ''; - break; - case 'heure_depart': - aVal = a?.heure_depart || ''; - bVal = b?.heure_depart || ''; - break; - case 'heure_arrivee': - aVal = a?.heure_arrivee || ''; - bVal = b?.heure_arrivee || ''; + comparison = String(a.gare_arrivee_nom || '').localeCompare(String(b.gare_arrivee_nom || '')); break; case 'jours': - { - const joursA = getJoursCirculation(a); - const joursB = getJoursCirculation(b); - aVal = joursA.length; - bVal = joursB.length; + comparison = (getJoursCirculation(a).join(', ') || '').localeCompare(getJoursCirculation(b).join(', ') || ''); + break; + case 'materiel': { + const aMat = a.materiel_roulant ? (a.materiel_roulant.nom || a.materiel_roulant.nom_technique) : ''; + const bMat = b.materiel_roulant ? (b.materiel_roulant.nom || b.materiel_roulant.nom_technique) : ''; + comparison = aMat.localeCompare(bMat); + break; } - break; - case 'materiel': - aVal = a?.materiel_roulant?.nom || a?.materiel_roulant?.nom_technique || ''; - bVal = b?.materiel_roulant?.nom || b?.materiel_roulant?.nom_technique || ''; - break; case 'substitution': - aVal = a?.est_substitution ? 1 : 0; - bVal = b?.est_substitution ? 1 : 0; + comparison = (a.est_substitution ? 1 : 0) - (b.est_substitution ? 1 : 0); break; default: - return 0; + comparison = 0; } - - if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1; - if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1; - return 0; + return sortDirection === 'asc' ? comparison : -comparison; }); }, [sortColumn, sortDirection, compareNumeroTrain]); @@ -1071,47 +314,680 @@ export default function Horaires() { const sortedHoraires = useMemo(() => getSortedHoraires(filteredHoraires), [filteredHoraires, getSortedHoraires]); + const handleSort = (column) => { + if (sortColumn === column) { + setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc'); + } else { + setSortColumn(column); + setSortDirection('asc'); + } + }; + const renderSortIcon = (column) => { if (sortColumn !== column) { - return ; + return ; } return sortDirection === 'asc' ? : ; }; - // Render d'une ligne de tableau pour un horaire + // ============================================================================ + // SÉLECTION & ACTIONS DE MASSE + // ============================================================================ + + const toggleSelectionMode = () => { + setSelectionMode(!selectionMode); + setSelectedHoraires([]); + }; + + const toggleSelectHoraire = (horaireId) => { + setSelectedHoraires(prev => { + if (prev.includes(horaireId)) { + return prev.filter(id => id !== horaireId); + } else { + return [...prev, horaireId]; + } + }); + }; + + const handleDuplicateSelected = () => { + if (selectedHoraires.length === 0) return; + if (selectedHoraires.length === 1) { + const horaireToDuplicate = horaires.find(h => h.id === selectedHoraires[0]); + if (horaireToDuplicate) { + setEditingItem(horaireToDuplicate); + setIsDuplicating(true); + setShowModal(true); + setSelectionMode(false); + setSelectedHoraires([]); + } + return; + } + // Plusieurs éléments + const horairesToDuplicate = horaires.filter(h => selectedHoraires.includes(h.id)); + if (horairesToDuplicate.length === 0) return; + setEditingItem(horairesToDuplicate[0]); + setIsDuplicating(true); + setShowModal(true); + setSelectionMode(false); + setSelectedHoraires([]); + }; + + const handleDeleteSelected = async () => { + if (!canDeleteHoraires) { + alert("Vous n'avez pas les droits pour supprimer des horaires."); + return; + } + if (selectedHoraires.length === 0) return; + if (!window.confirm(`Êtes-vous sûr de vouloir supprimer ${selectedHoraires.length} horaire(s) ?`)) return; + + setSaving(true); + try { + for (const id of selectedHoraires) { + await deleteHoraire(id, false); + } + await loadData(); + setSelectedHoraires([]); + setSelectionMode(false); + } catch (err) { + console.error(err); + alert('Erreur lors de la suppression groupée.'); + } + setSaving(false); + }; + + const handleExportSelected = () => { + if (selectedHoraires.length === 0) return; + + const headers = [ + 'id', 'numero_train', 'type_train_id', 'type_train', 'gare_depart_id', + 'gare_depart_nom', 'heure_depart', 'gare_arrivee_id', 'gare_arrivee_nom', + 'heure_arrivee', 'circule_lundi', 'circule_mardi', 'circule_mercredi', + 'circule_jeudi', 'circule_vendredi', 'circule_samedi', 'circule_dimanche', + 'circule_jours_feries', 'circule_dimanches_feries', 'jours_personnalises', + 'jours_non_circulation', 'materiel_roulant_id', 'ligne_id', 'ligne_nom', + 'service_annuel_id', 'service_annuel_nom', 'est_substitution', + 'motif_substitution', 'actif', 'created_at', 'updated_at', 'deleted_at', + 'gares_desservies', 'composition_train', 'substitution_disponible', 'region_id' + ]; + + const dataToExport = horaires + .filter(h => selectedHoraires.includes(h.id)) + .map(h => { + const row = {}; + headers.forEach(header => { + let val = h[header]; + if (Array.isArray(val) || (typeof val === 'object' && val !== null)) { + val = JSON.stringify(val); + } + row[header] = val !== undefined && val !== null ? val : ''; + }); + return row; + }); + + const worksheet = XLSX.utils.json_to_sheet(dataToExport, { header: headers }); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, 'Horaires'); + + const uniqueLigneIds = Array.from(new Set(dataToExport.map(h => h.ligne_id).filter(Boolean))); + const matchedLignes = uniqueLigneIds.map(id => lignes.find(l => String(l.id) === String(id))).filter(Boolean); + + let filenameBase = 'export_horaires'; + if (matchedLignes.length === 1) { + const nomLigne = matchedLignes[0].nom || 'Ligne'; + filenameBase = `export_horaires_${nomLigne.replace(/[^a-zA-Z0-9]/g, '_')}`; + } else if (matchedLignes.length > 1) { + const codes = matchedLignes.map(l => l.code || 'NOCODE').filter(Boolean); + filenameBase = `export_horaires_${codes.join('-')}`; + } + + const dateStr = new Date().toISOString().split('T')[0]; + XLSX.writeFile(workbook, `${filenameBase}_${dateStr}.csv`, { bookType: 'csv' }); + }; + + const handleAssignSelectedToLigne = async () => { + if (!assignTargetLigne || selectedHoraires.length === 0) return; + setAssignLoading(true); + + const targetLigneObj = lignes.find(l => String(l.id) === String(assignTargetLigne)); + const ligneNomVal = targetLigneObj ? targetLigneObj.nom : null; + + try { + const { error: batchError } = await updateHorairesLigneBatch(selectedHoraires, assignTargetLigne, ligneNomVal); + if (batchError) { + alert("Erreur lors de l'attribution : " + (batchError.message || 'Erreur')); + setAssignLoading(false); + return; + } + await loadData(); + setAssignLigneModalOpen(false); + setAssignTargetLigne(''); + setSelectedHoraires([]); + setSelectionMode(false); + } catch (err) { + console.error('Erreur assignation ligne batch:', err); + alert("Erreur inattendue lors de l'attribution."); + } + setAssignLoading(false); + }; + + // ============================================================================ + // GESTION DES MODALES HORAIRES + // ============================================================================ + + const openCreateModal = () => { + if (!canCreateHoraires) { + alert("Vous n'avez pas les droits pour créer un horaire."); + return; + } + setEditingItem(null); + setIsDuplicating(false); + setShowModal(true); + }; + + const openEditModal = (horaire) => { + if (!canUpdateHoraires) { + alert("Vous n'avez pas les droits pour modifier un horaire."); + return; + } + if (!horaire) return; + setEditingItem(horaire); + setIsDuplicating(false); + setShowModal(true); + }; + + const closeModal = () => { + setShowModal(false); + setEditingItem(null); + setIsDuplicating(false); + }; + + const handleDelete = async (id) => { + if (!canDeleteHoraires) { + alert("Vous n'avez pas les droits pour supprimer un horaire."); + return; + } + if (!id) return; + if (!window.confirm('Êtes-vous sûr de vouloir supprimer cet horaire ?')) return; + setSaving(true); + setHoraires(prev => prev.filter(h => String(h.id) !== String(id))); + const { error: delErr } = await deleteHoraire(id, false); + if (delErr) { + alert('Erreur lors de la suppression: ' + (delErr.message || 'Erreur')); + await loadData(); + } + setSaving(false); + }; + + // ============================================================================ + // GESTION DES LIGNES + // ============================================================================ + + const openCreateLigneModal = () => { + if (!canCreateLignes) { + alert("Vous n'avez pas les droits pour créer une ligne."); + return; + } + setEditingLigne(null); + setLigneNom(''); + setLigneCode(''); + setLigneDepartId(''); + setLigneArriveeId(''); + setGaresTracees([]); + setLigneFHUrl(''); + setLigneFHFile(null); + setLigneFHError(''); + setLigneModalTab('infos'); + setLigneRegion(''); + setFormError(''); + setShowLigneModal(true); + }; + + const openEditLigneModal = (ligneItem) => { + if (!canUpdateLignes) { + alert("Vous n'avez pas les droits pour modifier une ligne."); + return; + } + if (!ligneItem) return; + setEditingLigne(ligneItem); + setLigneNom(ligneItem.nom || ''); + setLigneCode(ligneItem.code || ''); + setLigneDepartId(ligneItem.gare_depart_id || ''); + setLigneArriveeId(ligneItem.gare_arrivee_id || ''); + setGaresTracees(Array.isArray(ligneItem.lignes_gares) ? ligneItem.lignes_gares.map(lg => ({ + id: lg.id, + gare_id: lg.gare?.id || lg.gare_id || '', + gare_nom: lg.gare?.nom || '', + desserte: lg.desserte || 'tous', + ordre: lg.ordre + })) : []); + setLigneFHUrl(ligneItem.fh_url || ''); + setLigneFHFile(null); + setLigneFHError(''); + setLigneModalTab('infos'); + setLigneRegion(ligneItem.region || ''); + setFormError(''); + setShowLigneModal(true); + const initialGares = ligneItem.lignes_gares ? ligneItem.lignes_gares.map(lg => ({ + gare_id: lg.gare?.id || lg.gare_id, + desserte: lg.desserte, + ordre: lg.ordre + })) : []; + originalGaresTraceesRef.current = initialGares; + }; + + const closeLigneModal = () => { + setShowLigneModal(false); + setEditingLigne(null); + originalGaresTraceesRef.current = null; + }; + + const addGareTracee = useCallback(() => { + setGaresTracees(prev => ([...prev, { id: `new-${Date.now()}`, gare_id: '', gare_nom: '', desserte: 'tous', ordre: prev.length + 1 }])); + }, []); + + const updateGareTraceeField = useCallback((index, field, value) => { + setGaresTracees(prev => { + const copy = [...prev]; + copy[index] = { ...copy[index], [field]: value }; + return copy; + }); + }, []); + + const removeGareTracee = useCallback((index) => { + setGaresTracees(prev => { + const filtered = prev.filter((_, i) => i !== index); + return filtered.map((g, i) => ({ ...g, ordre: i + 1 })); + }); + }, []); + + const handleSaveLigne = async () => { + if (!editingLigne && !canCreateLignes) return setFormError("Vous n'avez pas les droits pour créer une ligne."); + if (editingLigne && !canUpdateLignes) return setFormError("Vous n'avez pas les droits pour modifier une ligne."); + setFormError(''); + if (!ligneNom) return setFormError('Le nom de la ligne est requis.'); + if (!ligneDepartId || !ligneArriveeId) return setFormError('Les gares de départ et d\'arrivée sont requises.'); + + setSaving(true); + const payload = { + nom: ligneNom, + code: ligneCode || null, + region: ligneRegion || null, + gare_depart_id: ligneDepartId, + gare_arrivee_id: ligneArriveeId, + actif: true + }; + + try { + let res; + if (editingLigne && editingLigne.id) { + res = await updateLigne(editingLigne.id, payload, garesTracees); + } else { + res = await createLigne(payload, garesTracees); + } + + if (res?.error) { + setFormError(res.error.message || 'Erreur lors de la sauvegarde de la ligne'); + setSaving(false); + return; + } + + await loadData(); + setSaving(false); + closeLigneModal(); + } catch (err) { + console.error(err); + setFormError('Erreur inattendue lors de la sauvegarde'); + setSaving(false); + } + }; + + const handleDeleteLigne = async (id) => { + if (!canDeleteLignes) { + alert("Vous n'avez pas les droits pour supprimer une ligne."); + return; + } + if (!id) return; + if (!window.confirm('Êtes-vous sûr de vouloir supprimer cette ligne ?')) return; + + setSaving(true); + const { error: delErr } = await deleteLigne(id); + if (delErr) { + alert('Erreur lors de la suppression: ' + (delErr.message || 'Erreur')); + } + await loadData(); + setSaving(false); + }; + + // ============================================================================ + // GESTION DES SERVICES ANNUELS + // ============================================================================ + + const openCreateServiceModal = () => { + if (!canCreateServices) { + alert("Vous n'avez pas les droits pour créer un service annuel."); + return; + } + setEditingService(null); + setServiceNom(''); + setServiceCode(''); + setServiceDateDebut(''); + setServiceDateFin(''); + setServiceDescription(''); + setFormError(''); + setShowServiceModal(true); + }; + + const openEditServiceModal = (serv) => { + if (!canUpdateServices) { + alert("Vous n'avez pas les droits pour modifier un service annuel."); + return; + } + if (!serv) return; + setEditingService(serv); + setServiceNom(serv.nom || ''); + setServiceCode(serv.code || ''); + setServiceDateDebut(serv.date_debut ? serv.date_debut.split('T')[0] : ''); + setServiceDateFin(serv.date_fin ? serv.date_fin.split('T')[0] : ''); + setServiceDescription(serv.description || ''); + setFormError(''); + setShowServiceModal(true); + }; + + const closeServiceModal = () => { + setShowServiceModal(false); + setEditingService(null); + }; + + const handleSaveService = async () => { + if (!editingService && !canCreateServices) return setFormError("Vous n'avez pas les droits pour créer un service annuel."); + if (editingService && !canUpdateServices) return setFormError("Vous n'avez pas les droits pour modifier un service annuel."); + setFormError(''); + if (!serviceNom) return setFormError('Le nom du service annuel est requis.'); + if (!serviceDateDebut || !serviceDateFin) return setFormError('Les dates de début et fin sont requises.'); + + setSaving(true); + const payload = { + nom: serviceNom, + code: serviceCode || null, + date_debut: serviceDateDebut, + date_fin: serviceDateFin, + description: serviceDescription || null, + actif: true + }; + + try { + let res; + if (editingService && editingService.id) { + res = await updateServiceAnnuel(editingService.id, payload); + } else { + res = await createServiceAnnuel(payload); + } + + if (res?.error) { + setFormError(res.error.message || 'Erreur lors de la sauvegarde du service annuel'); + setSaving(false); + return; + } + + await loadData(); + setSaving(false); + closeServiceModal(); + } catch (err) { + console.error(err); + setFormError('Erreur inattendue lors de la sauvegarde'); + setSaving(false); + } + }; + + const handleDeleteService = async (id) => { + if (!canDeleteServices) { + alert("Vous n'avez pas les droits pour supprimer un service annuel."); + return; + } + if (!id) return; + if (!window.confirm('Êtes-vous sûr de vouloir supprimer ce service annuel ?')) return; + + setSaving(true); + const { error: delErr } = await deleteServiceAnnuel(id); + if (delErr) { + alert('Erreur lors de la suppression: ' + (delErr.message || 'Erreur')); + } + await loadData(); + setSaving(false); + }; + + // ============================================================================ + // GESTION DE L'ATTRIBUTION DES QUAIS + // ============================================================================ + + const getFilteredDesservies = (horaire) => { + const normalized = normalizeHoraireDesservies(horaire); + const depart = normalized.depart; + const arrivee = normalized.arrivee; + const sameGare = (a, b) => { + if (!a || !b) return false; + const aId = a.gare_id || a.id || null; + const bId = b.gare_id || b.id || null; + if (aId && bId && String(aId) === String(bId)) return true; + const aNom = String(a.gare_nom || a.nom || '').trim().toLowerCase(); + const bNom = String(b.gare_nom || b.nom || '').trim().toLowerCase(); + return aNom && bNom && aNom === bNom; + }; + const isSameStop = (row, ref) => { + if (!row || !ref) return false; + return sameGare(row, ref) + && String(row.heure_arrivee || '') === String(ref.heure_arrivee || '') + && String(row.heure_depart || '') === String(ref.heure_depart || ''); + }; + const seen = new Set(); + return normalized.intermediaires.filter(row => { + if (isSameStop(row, depart) || isSameStop(row, arrivee)) return false; + const key = `${row.gare_id || row.gare_nom || ''}|${row.heure_arrivee || ''}|${row.heure_depart || ''}|${row.ordre ?? ''}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + }; + + const getDesservieKey = (gare, index) => `gare_${gare?.ordre ?? index + 1}`; + + const openQuaiModal = (horaire) => { + setQuaiSelectedHoraire(horaire); + const assignments = {}; + const normalizedDesservies = normalizeHoraireDesservies(horaire); + + // Gare de départ + const storedDepart = quaiAssignments[`${horaire.id}_depart`] || normalizedDesservies.depart?.quai || horaire.quai_depart || ''; + if (storedDepart) { + const gareDepart = garesList.find(g => String(g.id) === String(horaire.gare_depart_id) || g.nom === horaire.gare_depart_nom); + assignments['depart'] = resolveQuaiIdFromNom(gareDepart, storedDepart); + } else { + assignments['depart'] = ''; + } + + // Gares desservies + const filteredDesservies = normalizedDesservies.intermediaires; + filteredDesservies.forEach((g, idx) => { + const key = getDesservieKey(g, idx); + const stored = quaiAssignments[`${horaire.id}_${key}`] || g.quai || ''; + if (stored) { + const gareObj = garesList.find(gg => String(gg.id) === String(g.gare_id) || gg.nom === g.gare_nom); + assignments[key] = resolveQuaiIdFromNom(gareObj, stored); + } else { + assignments[key] = ''; + } + }); + + // Gare d'arrivée + const storedArr = quaiAssignments[`${horaire.id}_arrivee`] || normalizedDesservies.arrivee?.quai || horaire.quai_arrivee || ''; + if (storedArr) { + const gareArr = garesList.find(g => String(g.id) === String(horaire.gare_arrivee_id) || g.nom === horaire.gare_arrivee_nom); + assignments['arrivee'] = resolveQuaiIdFromNom(gareArr, storedArr); + } else { + assignments['arrivee'] = ''; + } + setQuaiAssignments(prev => ({ ...prev, [`_modal_${horaire.id}`]: assignments })); + setShowQuaiModal(true); + }; + + const closeQuaiModal = () => { + setShowQuaiModal(false); + setQuaiSelectedHoraire(null); + }; + + const handleQuaiChange = (key, value) => { + if (!quaiSelectedHoraire) return; + setQuaiAssignments(prev => ({ + ...prev, + [`_modal_${quaiSelectedHoraire.id}`]: { + ...(prev[`_modal_${quaiSelectedHoraire.id}`] || {}), + [key]: value + } + })); + }; + + const handleSaveQuais = async (horaireIdFromModal, payloadFromModal) => { + const targetHoraireId = horaireIdFromModal || (quaiSelectedHoraire ? quaiSelectedHoraire.id : null); + if (!targetHoraireId) return; + + setQuaiSaving(true); + try { + let quaisParOrdre = {}; + + if (payloadFromModal) { + quaisParOrdre = payloadFromModal; + } else if (quaiSelectedHoraire) { + const modalAssignments = quaiAssignments[`_modal_${quaiSelectedHoraire.id}`] || {}; + const gares = normalizeHoraireDesservies(quaiSelectedHoraire).intermediaires; + gares.forEach((g, idx) => { + const key = getDesservieKey(g, idx); + const quaiId = modalAssignments[key] || ''; + const gareObj = garesList.find(gg => String(gg.id) === String(g.gare_id) || gg.nom === g.gare_nom); + const quaiObj = gareObj?.gare_quais?.find(q => String(q.id) === String(quaiId)); + quaisParOrdre[g.ordre ?? idx + 1] = quaiObj ? quaiObj.nom : (quaiId || null); + }); + + if (Object.prototype.hasOwnProperty.call(modalAssignments, 'depart')) { + const departId = modalAssignments['depart'] || ''; + const gareDepartObj = garesList.find(gg => String(gg.id) === String(quaiSelectedHoraire.gare_depart_id) || gg.nom === quaiSelectedHoraire.gare_depart_nom); + const departQuaiObj = gareDepartObj?.gare_quais?.find(q => String(q.id) === String(departId)); + quaisParOrdre['depart'] = departQuaiObj ? departQuaiObj.nom : (departId || null); + } + if (Object.prototype.hasOwnProperty.call(modalAssignments, 'arrivee')) { + const arriveeId = modalAssignments['arrivee'] || ''; + const gareArrObj = garesList.find(gg => String(gg.id) === String(quaiSelectedHoraire.gare_arrivee_id) || gg.nom === quaiSelectedHoraire.gare_arrivee_nom); + const arriveeQuaiObj = gareArrObj?.gare_quais?.find(q => String(q.id) === String(arriveeId)); + quaisParOrdre['arrivee'] = arriveeQuaiObj ? arriveeQuaiObj.nom : (arriveeId || null); + } + } + + const { error: saveErr } = await updateQuaisHoraire(targetHoraireId, quaisParOrdre); + if (saveErr) { + alert('Erreur lors de la sauvegarde des quais: ' + (saveErr.message || 'Erreur')); + setQuaiSaving(false); + return; + } + + await loadData(); + if (quaiSelectedHoraire) { + closeQuaiModal(); + } + } catch (err) { + console.error('Erreur sauvegarde quais:', err); + alert('Erreur lors de la sauvegarde des quais'); + } + setQuaiSaving(false); + }; + + const handleSetDefaultQuais = () => { + if (!quaiSelectedHoraire) return; + if (!window.confirm('Définir les quais actuels comme quais par défaut pour cet horaire ?')) return; + const modalAssignments = quaiAssignments[`_modal_${quaiSelectedHoraire.id}`] || {}; + const defaults = { ...quaiAssignments }; + Object.entries(modalAssignments).forEach(([key, value]) => { + defaults[`default_${quaiSelectedHoraire.id}_${key}`] = value; + }); + setQuaiAssignments(defaults); + alert('Quais par défaut enregistrés.'); + }; + + const getQuaisForGare = (gareId, gareNom) => { + const gareById = gareId ? garesList.find(g => String(g.id) === String(gareId)) : null; + const gare = gareById || garesList.find(g => g.nom === gareNom); + if (!gare) return []; + return gare.gare_quais || []; + }; + + // ============================================================================ + // RENDERING HORAIRES TAB + // ============================================================================ + const renderHoraireRow = (h, i, isNested = false) => ( - selectionMode && h?.id && toggleSelectHoraire(h.id)} - > - {selectionMode && ( - e.stopPropagation()}> - h?.id && toggleSelectHoraire(h.id)} - style={{ width: 18, height: 18, cursor: 'pointer' }} - /> - - )} - {h?.numero_train || '-'} - {h?.type_train || '-'} -
{h?.gare_depart_nom || '-'}
{formatHeure(h?.heure_depart)} -
{h?.gare_arrivee_nom || '-'}
{formatHeure(h?.heure_arrivee)} - {getJoursCirculation(h).join(', ') || 'Aucun'} - {h?.materiel_roulant ? {h.materiel_roulant.nom || h.materiel_roulant.nom_technique} : '-'} - {h?.est_substitution ? Oui : '-'} - e.stopPropagation()}> - openEditModal(h)} disabled={!canUpdateHoraires}>Modifier - handleDelete(h?.id)} disabled={!canDeleteHoraires}>Supprimer + selectionMode && h?.id && toggleSelectHoraire(h.id)} + > + {selectionMode && ( + e.stopPropagation()} style={{ width: 44 }}> + h?.id && toggleSelectHoraire(h.id)} + style={{ width: 18, height: 18, cursor: 'pointer', accentColor: '#0088ce' }} + /> - + )} + + {h?.numero_train || '-'} + + + {h?.type_train || '-'} + + +
{h?.gare_depart_nom || '-'}
+
{formatHeure(h?.heure_depart)}
+ + +
{h?.gare_arrivee_nom || '-'}
+
{formatHeure(h?.heure_arrivee)}
+ + + {getJoursCirculation(h).join(', ') || 'Aucun'} + + + {h?.materiel_roulant ? ( + + {h.materiel_roulant.nom || h.materiel_roulant.nom_technique} + + ) : '-'} + + + {h?.est_substitution ? ( + + + Substitution + + ) : ( + - + )} + + e.stopPropagation()}> +
+ openEditModal(h)} disabled={!canUpdateHoraires}> + Modifier + + handleDelete(h?.id)} disabled={!canDeleteHoraires}> + Supprimer + +
+ + ); const renderListView = () => { @@ -1125,7 +1001,6 @@ export default function Horaires() { }); const numeroTrains = Object.keys(groupedHoraires); - // Pagination: calculer les pages à afficher (on paginate par numéro de train) const totalPages = Math.max(1, Math.ceil(numeroTrains.length / PAGE_SIZE)); const safePage = Math.min(Math.max(1, currentPage), totalPages); const startIndex = (safePage - 1) * PAGE_SIZE; @@ -1135,51 +1010,42 @@ export default function Horaires() { const colspan = selectionMode ? 9 : 8; return ( - <> -
- - + <> +
+
+ - {selectionMode && } + {selectionMode && } - - + + {visibleNumeroTrains.map(numTrain => { const group = groupedHoraires[numTrain]; - if (!group || group.length === 0) return null; if (group.length === 1) { - // Horaire unique - affichage normal return ( {renderHoraireRow(group[0], numTrain)} @@ -1187,33 +1053,36 @@ export default function Horaires() { ); } - // Plusieurs horaires avec le même numéro - accordion dans une seule ligne + // Accordion pour trains multiples return ( - - + -
handleSort('numero_train')} style={{ cursor: 'pointer', userSelect: 'none' }}> - N° Train - {renderSortIcon('numero_train')} + N° Train {renderSortIcon('numero_train')} handleSort('type_train')} style={{ cursor: 'pointer', userSelect: 'none' }}> - Type - {renderSortIcon('type_train')} + Type {renderSortIcon('type_train')} handleSort('gare_depart')} style={{ cursor: 'pointer', userSelect: 'none' }}> - Départ - {renderSortIcon('gare_depart')} + Départ {renderSortIcon('gare_depart')} handleSort('gare_arrivee')} style={{ cursor: 'pointer', userSelect: 'none' }}> - Arrivée - {renderSortIcon('gare_arrivee')} + Arrivée {renderSortIcon('gare_arrivee')} handleSort('jours')} style={{ cursor: 'pointer', userSelect: 'none' }}> - Jours - {renderSortIcon('jours')} + Jours {renderSortIcon('jours')} handleSort('materiel')} style={{ cursor: 'pointer', userSelect: 'none' }}> - Matériel - {renderSortIcon('materiel')} + Matériel {renderSortIcon('materiel')} handleSort('substitution')} style={{ cursor: 'pointer', userSelect: 'none' }}> - Substitution - {renderSortIcon('substitution')} + Substitution {renderSortIcon('substitution')} Actions
-
- - - {`${numTrain} (${group.length} horaires)`} +
+
+ + + {numTrain} + + ({group.length} sillons programmés) + -
- +
+
- - {selectionMode && } - - - - - - - - - + + {selectionMode && } + + + + + + + + + - {group.map((h, i) => renderHoraireRow(h, `${numTrain}-${i}`, true))} + {group.map((h, i) => renderHoraireRow(h, `${numTrain}-${i}`, true))}
N° TrainTypeDépartArrivéeJoursMatérielSubstitutionActions
N° TrainTypeDépartArrivéeJoursMatérielSubstitutionActions
@@ -1223,1375 +1092,1447 @@ export default function Horaires() { ); })} -
+ + +
- {/* Pagination WCS SNCF - 10 numéros de train par page */} -
- {/* Utilisation de wcs-pagination si disponible */} - { - const v = e?.detail?.value ?? e?.detail?.page ?? null; - if (v) setCurrentPage(Number(v)); - }} - /> - - {/* Champ d'entrée de page directe */} -
- { - const v = Number(e.target.value || 1); - if (!isNaN(v)) setCurrentPage(Math.min(Math.max(1, v), totalPages)); - }} - style={{ width: 80, padding: '6px 8px', border: '1px solid #e5e7eb', borderRadius: 6 }} - /> - / {totalPages} -
- - {/* Fallback controls (Prev / Next) visibles si wcs-pagination non reconnu */} -
- setCurrentPage(p => Math.max(1, p - 1))}> - - -
- Page {safePage} / {totalPages} -
- = totalPages} onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}> - - -
-
-
{/* fin table-container */} - + {/* Pagination */} +
+ setCurrentPage(p => Math.max(1, p - 1))}> + + +
+ Page {safePage} sur {totalPages} ({numeroTrains.length} trains) +
+ = totalPages} onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}> + + +
+ ); }; - // Remplacer renderCardsView pour paginer aussi la vue cartes const renderCardsView = () => { - // Pagination pour les cartes (on pagine par horaire individuel) sur la liste filtrée const totalPagesCards = Math.max(1, Math.ceil(sortedHoraires.length / PAGE_SIZE)); const safePageCards = Math.min(Math.max(1, currentPage), totalPagesCards); const startCards = (safePageCards - 1) * PAGE_SIZE; const endCards = startCards + PAGE_SIZE; const visibleHoraires = sortedHoraires.slice(startCards, endCards); - const renderHoraireCard = (h, i) => ( -
selectionMode && h?.id && toggleSelectHoraire(h.id)} - > -
-
- {selectionMode && ( - h?.id && toggleSelectHoraire(h.id)} - onClick={e => e.stopPropagation()} - style={{ width: 20, height: 20, cursor: 'pointer', marginTop: 4 }} - /> - )} -
-
{h?.numero_train || '-'}
-
{h?.type_train || '-'}
-
-
- {h?.est_substitution && Substitution} -
- -
-
-
{h?.gare_depart_nom || '-'}
{formatHeure(h?.heure_depart)}
-
{h?.gare_arrivee_nom || '-'}
{formatHeure(h?.heure_arrivee)}
-
-
{h?.gares_desservies?.length ? `${h.gares_desservies.length} arrêt(s)` : 'Direct'}
-
- -
e.stopPropagation()}> - openEditModal(h)} disabled={!canUpdateHoraires}>Modifier - handleDelete(h?.id)} disabled={!canDeleteHoraires}>Supprimer -
-
- ); - return ( - <> -
- {visibleHoraires.map(renderHoraireCard)} -
+ <> +
+ {visibleHoraires.map((h, idx) => { + const isSelected = selectedHoraires.includes(h.id); + return ( +
selectionMode && h?.id && toggleSelectHoraire(h.id)} + style={{ cursor: selectionMode ? 'pointer' : 'default' }} + > +
+
+ {selectionMode && ( + h?.id && toggleSelectHoraire(h.id)} + onClick={e => e.stopPropagation()} + style={{ width: 18, height: 18, cursor: 'pointer', accentColor: '#0088ce' }} + /> + )} +
+ {h.numero_train || '-'} +
+ {h.type_train || '-'} +
+
+
+ {h.est_substitution && ( + Substitution + )} +
- {/* Pagination cartes */} -
- { - const v = e?.detail?.value ?? e?.detail?.page ?? null; - if (v) setCurrentPage(Number(v)); - }} - /> +
+
+ {h.gare_depart_nom || '-'} + {formatHeure(h.heure_depart)} +
+ +
+ {h.gare_arrivee_nom || '-'} + {formatHeure(h.heure_arrivee)} +
+
-
- { - const v = Number(e.target.value || 1); - if (!isNaN(v)) setCurrentPage(Math.min(Math.max(1, v), totalPagesCards)); - }} - style={{ width: 80, padding: '6px 8px', border: '1px solid #e5e7eb', borderRadius: 6 }} - /> - / {totalPagesCards} -
+
+
Jours : {getJoursCirculation(h).join(', ') || 'Aucun'}
+ {h.materiel_roulant && ( +
+ Matériel : {h.materiel_roulant.nom || h.materiel_roulant.nom_technique} +
+ )} +
-
- setCurrentPage(p => Math.max(1, p - 1))}> - - -
- Page {safePageCards} / {totalPagesCards} +
e.stopPropagation()}> + openEditModal(h)} disabled={!canUpdateHoraires}> + Modifier + + handleDelete(h.id)} disabled={!canDeleteHoraires}> + Supprimer + +
- = totalPagesCards} onClick={() => setCurrentPage(p => Math.min(totalPagesCards, p + 1))}> - - -
+ ); + })} +
+ + {/* Pagination Cards */} +
+ setCurrentPage(p => Math.max(1, p - 1))}> + + +
+ Page {safePageCards} sur {totalPagesCards} ({sortedHoraires.length} horaires)
- + = totalPagesCards} onClick={() => setCurrentPage(p => Math.min(totalPagesCards, p + 1))}> + + +
+ ); }; - return ( -
- -
-
-
-
-

Exploitation

-

Gestion des horaires

-

Administrez les sillons, lignes, services annuels et attribution des quais.

-
-
- - {error && Erreur{error}} - - {/* Tabs principaux */} -
- setMainTab(e.detail)}> - - {renderHorairesTab()} - - - {renderLignesTab()} - - - {renderServicesTab()} - - - {renderQuaisTab()} - - -
- - -
-
-
- ); - - function renderHorairesTab() { - // Calculer le nombre d'horaires par ligne + const renderHorairesTab = () => { const countByLigne = {}; (horaires || []).forEach(h => { const id = h?.ligne_id || h?.ligne; if (id) countByLigne[id] = (countByLigne[id] || 0) + 1; }); + const activeLigneObj = selectedLigneFilter && selectedLigneFilter !== '__none__' + ? (lignes || []).find(x => String(x?.id) === String(selectedLigneFilter)) + : null; + return ( -
+
+ {/* Panneau latéral Lignes */} +
+ - {/* ── Panneau latéral Lignes ── */} -
- {/* Bouton toggle */} - + {lignesPanelOpen && ( +
+
+ + Lignes du Réseau +
- {lignesPanelOpen && ( -
- {/* Titre */} -
- Lignes -
- - {/* Liste des lignes */} -
- {/* Item "Toutes les lignes" */} - - - {/* Séparateur */} -
- - {/* Sans ligne */} - {(() => { - const sansligne = (horaires || []).filter(h => !h?.ligne_id && !h?.ligne); - if (sansligne.length === 0) return null; - return ( - - ); - })()} - - {/* Lignes existantes */} - {(lignes || []).map(l => { - const count = countByLigne[l?.id] || 0; - const isActive = String(selectedLigneFilter) === String(l?.id); - const region = (regionsList || []).find(r => r.id === l?.region); - return ( - - ); - })} - - {lignes.length === 0 && !loading && ( -
- Aucune ligne créée -
- )} -
- - {/* Footer : créer une ligne */} -
- -
-
- )} -
- - {/* ── Contenu principal ── */} -
- {/* Bandeau ligne sélectionnée */} - {selectedLigneFilter && selectedLigneFilter !== '__none__' && (() => { - const l = (lignes || []).find(x => String(x?.id) === String(selectedLigneFilter)); - return l ? ( -
- - {l.nom} - {l.code && {l.code}} - {l.gare_depart?.nom && {l.gare_depart.nom} → {l.gare_arrivee?.nom}} - -
- ) : null; - })()} - - {selectedLigneFilter === '__none__' && ( -
- - Horaires sans ligne attribuée - -
- )} - - {/* Barre d'outils */} -
-
- setViewMode('liste')}> Liste - setViewMode('carte')}> Cartes - + {/* Tous les horaires */} +
-
- { window.location.href = '/horaires/imports'; }} disabled={!canCreateHoraires}> - Import Excel - - Créer un horaire -
-
+ + + Tous les horaires + + {horaires.length} + - {/* Panel de sélection */} - {selectionMode && ( -
-
- { - if (selectedHoraires.length === filteredHoraires.length && filteredHoraires.length > 0) { - setSelectedHoraires([]); - } else { - setSelectedHoraires(filteredHoraires.map(h => h.id)); - } - }}> - 0 ? 'check_box' : 'check_box_outline_blank'} size="s"/> - {selectedHoraires.length === filteredHoraires.length && filteredHoraires.length > 0 ? 'Tout désélectionner' : 'Tout sélectionner'} - - - {selectedHoraires.length} élément(s) sélectionné(s) - -
-
- - Dupliquer - - - Exporter - - setAssignLigneModalOpen(true)} disabled={selectedHoraires.length === 0 || !canUpdateHoraires} style={{ opacity: (selectedHoraires.length === 0 || !canUpdateHoraires) ? 0.5 : 1 }}> - Attribuer - - - Supprimer - -
-
- )} +
- {/* Liste/Cartes */} - {loading ? ( -

Chargement...

- ) : filteredHoraires.length === 0 ? ( -
- -

- {selectedLigneFilter - ? 'Aucun horaire pour cette ligne' - : 'Aucun horaire trouvé'} -

- Créer un horaire -
- ) : (viewMode === 'liste' ? renderListView() : renderCardsView())} - - {/* Modale Attribution */} - {assignLigneModalOpen && ( -
-
-
-

Attribuer {selectedHoraires.length} horaire(s) à une ligne

- -
-
-

Choisissez la ligne à laquelle vous souhaitez attribuer ces horaires :

-
- - -
-
-
- setAssignLigneModalOpen(false)} disabled={assignLoading}> - Annuler - - - {assignLoading ? 'Attribution...' : 'Confirmer'} - -
-
-
- )} - - {/* Modal horaires */} - {showModal && ( - { - loadData(); - }} - lignes={lignes} - materiels={materiels} - typesTrainsList={typesTrainsList} - allGares={garesList} - servicesAnnuels={servicesAnnuels} - horaires={horaires} - currentUser={currentUser} - canCreateHoraires={canCreateHoraires} - canUpdateHoraires={canUpdateHoraires} - /> - )} -
-
- ); - } - - function renderLignesTab() { - return ( -
-
-

Gestion des Lignes

- Créer une ligne -
- - {loading ?

Chargement...

: (lignes.length === 0 ? ( -
- -

Aucune ligne trouvée

- Créer une ligne -
- ) : ( -
- - - - - - - - - - - - - - {lignes.map((l, i) => ( - - - - - - - - - - ))} - -
NomCodeRégionDépartArrivéeGares sur le tracéActions
{l?.nom || '-'}{l?.code || '-'}{regionsList.find(r => r.id === l?.region)?.nom || '-'}{l?.gare_depart?.nom || '-'}{l?.gare_arrivee?.nom || '-'}{l?.lignes_gares?.length || 0} gare(s) - openEditLigneModal(l)} disabled={!canUpdateLignes}>Modifier - handleDeleteLigne(l?.id)} disabled={!canDeleteLignes}>Supprimer -
-
- ))} - - {/* Modal lignes */} - {showLigneModal && renderLigneModal()} -
- ); - } - - function renderServicesTab() { - return ( -
-
-

Gestion des Services Annuels

- Créer un service annuel -
- - {loading ?

Chargement...

: (servicesAnnuels.length === 0 ? ( -
- -

Aucun service annuel trouvé

- Créer un service annuel -
- ) : ( -
- - - - - - - - - - - - - {servicesAnnuels.map((s, i) => ( - - - - - - - - - ))} - -
NomCodeDate débutDate finDescriptionActions
{s?.nom || '-'}{s?.code || '-'}{formatDate(s?.date_debut)}{formatDate(s?.date_fin)}{s?.description || '-'} - openEditServiceModal(s)} disabled={!canUpdateServices}>Modifier - handleDeleteService(s?.id)} disabled={!canDeleteServices}>Supprimer -
-
- ))} - - {/* Modal services */} - {showServiceModal && renderServiceModal()} -
- ); - } - - - - function renderLigneModal() { - return ( -
-
e.stopPropagation()}> -
-

{editingLigne ? 'Modifier la ligne' : 'Nouvelle ligne'}

- -
-
- {formError && Erreur{formError}} - - {/* Onglets de la modale ligne */} -
- {[ - { key: 'infos', label: 'Informations' }, - { key: 'fiches-horaires', label: 'Fiches Horaires' } - ].map(tab => ( + {/* Sans ligne */} + {(() => { + const sansLigne = (horaires || []).filter(h => !h?.ligne_id && !h?.ligne); + if (sansLigne.length === 0) return null; + return ( - ))} -
+ ); + })()} - {/* Onglet Informations */} - {ligneModalTab === 'infos' && ( - <> -
- - setLigneNom(e.target.value)} placeholder="Ex: Paris - Lyon" /> -
- -
- - setLigneCode(e.target.value)} placeholder="Ex: PL01" /> -
- -
- - -
- -
-
- - -
-
- - -
-
- -
- -
- {garesTracees.length === 0 ? ( -

Aucune gare intermédiaire

- ) : ( - garesTracees.map((gt, idx) => ( -
- {idx + 1}. - - - removeGareTracee(idx)}> -
- )) + {/* Liste des lignes */} + {(lignes || []).map(l => { + const count = countByLigne[l?.id] || 0; + const isActive = String(selectedLigneFilter) === String(l?.id); + const region = (regionsList || []).find(r => r.id === l?.region); + return ( +
- - )} + {count} + + ); + })} - {/* Onglet Fiches Horaires */} - {ligneModalTab === 'fiches-horaires' && ( -
-

- Importez la fiche horaire PDF de cette ligne. Elle sera accessible sur les sites régionaux pour consultation. -

- - {/* Fiche horaire actuelle */} - {ligneFHUrl && !ligneFHFile && ( -
- -
-
Fiche horaire actuelle
- - {ligneFHUrl} - -
-
- - - Ouvrir - - - { - if (!window.confirm('Supprimer la fiche horaire ?')) return; - if (editingLigne?.id) { - await deleteFicheHoraire(editingLigne.id); - await updateLigne(editingLigne.id, { - nom: ligneNom, code: ligneCode, - gare_depart_id: ligneDepartId, - gare_arrivee_id: ligneArriveeId, - actif: true, fh_url: null - }, garesTracees); - await loadData(); - } - setLigneFHUrl(''); - }} - style={{ color: '#dc2626' }} - > - Supprimer - -
-
- )} - - {/* Zone d'upload */} -
document.getElementById('fh-pdf-input').click()} - onDragOver={e => { e.preventDefault(); e.currentTarget.style.borderColor = '#0088ce'; e.currentTarget.style.background = '#f0f9ff'; }} - onDragLeave={e => { e.currentTarget.style.borderColor = ligneFHFile ? '#0088ce' : '#d1d5db'; e.currentTarget.style.background = ligneFHFile ? '#f0f9ff' : '#fafafa'; }} - onDrop={e => { - e.preventDefault(); - const file = e.dataTransfer.files?.[0]; - if (file && file.type === 'application/pdf') { - setLigneFHFile(file); - setLigneFHError(''); - } else { - setLigneFHError('Seuls les fichiers PDF sont acceptés.'); - } - e.currentTarget.style.borderColor = '#0088ce'; - e.currentTarget.style.background = '#f0f9ff'; - }} - > - { - const file = e.target.files?.[0]; - if (file) { - if (file.type !== 'application/pdf') { - setLigneFHError('Seuls les fichiers PDF sont acceptés.'); - return; - } - setLigneFHFile(file); - setLigneFHError(''); - } - }} - /> - {ligneFHFile ? ( -
- -
{ligneFHFile.name}
-
- {(ligneFHFile.size / 1024).toFixed(1)} Ko — Cliquez pour changer -
- -
- ) : ( -
- -
- Glissez votre PDF ici ou cliquez pour parcourir -
-
Fichier PDF uniquement
-
- )} -
- - {ligneFHError && ( -
- {ligneFHError} -
- )} - - {ligneFHUploading && ( -
- Upload en cours... -
- )} - -
- - Le PDF sera enregistré lors de la sauvegarde de la ligne. Il sera consultable sur les pages régionales dédiées aux fiches horaires. -
+ {lignes.length === 0 && !loading && ( +
+ Aucune ligne configurée
- )} + )} +
+ +
+ +
-
- Annuler - - {saving || ligneFHUploading ? 'Enregistrement...' : (editingLigne ? 'Mettre à jour' : 'Créer')} + )} +
+ + {/* Contenu principal */} +
+ {/* Bandeau de filtre actif */} + {activeLigneObj && ( +
+ +
+ Ligne active : {activeLigneObj.nom} + {activeLigneObj.code && {activeLigneObj.code}} + {activeLigneObj.gare_depart?.nom && ({activeLigneObj.gare_depart.nom} → {activeLigneObj.gare_arrivee?.nom})} +
+ +
+ )} + + {selectedLigneFilter === '__none__' && ( +
+ +
+ Horaires sans ligne attribuée +
+ +
+ )} + + {/* Barre d'outils */} +
+
+
+ + +
+ + + + {selectionMode ? 'Annuler la sélection' : 'Sélectionner'} + + +
+ + { setSearchQuery(e.target.value); setCurrentPage(1); }} + className="horaires-search-input" + /> +
+
+ +
+ { window.location.href = '/horaires/imports'; }} disabled={!canCreateHoraires}> + Import Excel + + + Nouveau sillon
+ + {/* Bandeau d'actions de sélection */} + {selectionMode && ( +
+
+ { + if (selectedHoraires.length === filteredHoraires.length && filteredHoraires.length > 0) { + setSelectedHoraires([]); + } else { + setSelectedHoraires(filteredHoraires.map(h => h.id)); + } + }} + > + 0 ? 'check_box' : 'check_box_outline_blank'} size="s" /> + {selectedHoraires.length === filteredHoraires.length && filteredHoraires.length > 0 ? 'Tout désélectionner' : 'Tout sélectionner'} + + + {selectedHoraires.length} horaire(s) sélectionné(s) + +
+
+ + Dupliquer + + + Exporter CSV + + setAssignLigneModalOpen(true)} disabled={selectedHoraires.length === 0 || !canUpdateHoraires}> + Attribuer à une ligne + + + Supprimer + +
+
+ )} + + {/* Affichage des données */} + {loading ? ( +
Chargement des horaires en cours...
+ ) : filteredHoraires.length === 0 ? ( +
+ +

+ {selectedLigneFilter + ? 'Aucun horaire trouvé pour cette ligne.' + : 'Aucun horaire ne correspond à votre recherche.'} +

+ + Créer un horaire + +
+ ) : ( + viewMode === 'liste' ? renderListView() : renderCardsView() + )}
+
); - } + }; - function renderQuaisTab() { - const getFilteredDesservies = (horaire) => { - const normalized = normalizeHoraireDesservies(horaire); - const depart = normalized.depart; - const arrivee = normalized.arrivee; - const sameGare = (a, b) => { - if (!a || !b) return false; - const aId = a.gare_id || a.id || null; - const bId = b.gare_id || b.id || null; - if (aId && bId && String(aId) === String(bId)) return true; - const aNom = String(a.gare_nom || a.nom || '').trim().toLowerCase(); - const bNom = String(b.gare_nom || b.nom || '').trim().toLowerCase(); - return aNom && bNom && aNom === bNom; - }; - const isSameStop = (row, ref) => { - if (!row || !ref) return false; - return sameGare(row, ref) - && String(row.heure_arrivee || '') === String(ref.heure_arrivee || '') - && String(row.heure_depart || '') === String(ref.heure_depart || ''); - }; - const seen = new Set(); - return normalized.intermediaires.filter(row => { - if (isSameStop(row, depart) || isSameStop(row, arrivee)) return false; - const key = `${row.gare_id || row.gare_nom || ''}|${row.heure_arrivee || ''}|${row.heure_depart || ''}|${row.ordre ?? ''}`; - if (seen.has(key)) return false; - seen.add(key); - return true; - }); - }; + // ============================================================================ + // RENDERING LIGNES TAB + // ============================================================================ - const getDesservieKey = (gare, index) => `gare_${gare?.ordre ?? index + 1}`; + const renderLignesTab = () => ( +
+
+

+ + Lignes Commerciales & Tracés +

+ + Créer une ligne + +
- // Filtrer les horaires selon la recherche +

+ Configurez les lignes ferroviaires, définissez les gares d'origine et de destination ainsi que les arrêts intermédiaires sur le tracé. +

+ + {loading ? ( +
Chargement des lignes...
+ ) : lignes.length === 0 ? ( +
+ +

Aucune ligne configurée

+ + Créer une ligne + +
+ ) : ( +
+ + + + + + + + + + + + + + {lignes.map((l, i) => ( + + + + + + + + + + ))} + +
Nom de la ligneCodeRégionOrigine / DestinationGares sur le tracéFiche Horaire PDFActions
{l?.nom || '-'} + {l?.code ? ( + + {l.code} + + ) : '-'} + {regionsList.find(r => r.id === l?.region)?.nom || '-'} +
+ {l?.gare_depart?.nom || '-'} + + {l?.gare_arrivee?.nom || '-'} +
+
+ + {l?.lignes_gares?.length || 0} arrêt(s) + + + {l?.fh_url ? ( + + PDF disponible + + ) : ( + Aucun PDF + )} + +
+ openEditLigneModal(l)} disabled={!canUpdateLignes}> + Modifier + + handleDeleteLigne(l?.id)} disabled={!canDeleteLignes}> + Supprimer + +
+
+
+ )} +
+ ); + + // ============================================================================ + // RENDERING SERVICES TAB + // ============================================================================ + + const renderServicesTab = () => ( +
+
+

+ + Services Annuels & Périodes de Circulation +

+ + Nouveau service annuel + +
+ +

+ Administrez les grilles de validité temporelle (ex: SA 2025, Service Été 2025) rattachées aux sillons de circulation. +

+ + {loading ? ( +
Chargement des services annuels...
+ ) : servicesAnnuels.length === 0 ? ( +
+ +

Aucun service annuel configuré

+ + Créer un service annuel + +
+ ) : ( +
+ + + + + + + + + + + + + {servicesAnnuels.map((s, i) => ( + + + + + + + + + ))} + +
Nom du serviceCodeDate de débutDate de finDescriptionActions
{s?.nom || '-'} + {s?.code ? ( + + {s.code} + + ) : '-'} + {formatDate(s?.date_debut)}{formatDate(s?.date_fin)}{s?.description || '-'} +
+ openEditServiceModal(s)} disabled={!canUpdateServices}> + Modifier + + handleDeleteService(s?.id)} disabled={!canDeleteServices}> + Supprimer + +
+
+
+ )} +
+ ); + + // ============================================================================ + // RENDERING QUAIS TAB + // ============================================================================ + + const renderQuaisTab = () => { const filteredQuaiHoraires = horaires.filter(h => { if (!quaiSearchQuery?.trim()) return true; const q = quaiSearchQuery.toLowerCase(); return ( - (h.numero_train || '').toLowerCase().includes(q) || - (h.gare_depart_nom || '').toLowerCase().includes(q) || - (h.gare_arrivee_nom || '').toLowerCase().includes(q) || - (h.ligne_nom || '').toLowerCase().includes(q) + (h.numero_train || '').toLowerCase().includes(q) || + (h.gare_depart_nom || '').toLowerCase().includes(q) || + (h.gare_arrivee_nom || '').toLowerCase().includes(q) || + (h.ligne_nom || '').toLowerCase().includes(q) ); }); - const openQuaiModal = (horaire) => { - setQuaiSelectedHoraire(horaire); - // Initialiser les assignments depuis les données existantes - const assignments = {}; - const normalizedDesservies = normalizeHoraireDesservies(horaire); - - // Gare de départ - const storedDepart = quaiAssignments[`${horaire.id}_depart`] || normalizedDesservies.depart?.quai || horaire.quai_depart || ''; - if (storedDepart) { - // storedDepart peut être un nom ou un id ; essayer de résoudre en id - const gareDepart = garesList.find(g => String(g.id) === String(horaire.gare_depart_id) || g.nom === horaire.gare_depart_nom); - assignments['depart'] = resolveQuaiIdFromNom(gareDepart, storedDepart); - } else { - assignments['depart'] = ''; - } - - // Gares desservies - const filteredDesservies = normalizedDesservies.intermediaires; - filteredDesservies.forEach((g, idx) => { - const key = getDesservieKey(g, idx); - const stored = quaiAssignments[`${horaire.id}_${key}`] || g.quai || ''; - if (stored) { - const gareObj = garesList.find(gg => String(gg.id) === String(g.gare_id) || gg.nom === g.gare_nom); - assignments[key] = resolveQuaiIdFromNom(gareObj, stored); - } else { - assignments[key] = ''; - } - }); - - // Gare d'arrivée - const storedArr = quaiAssignments[`${horaire.id}_arrivee`] || normalizedDesservies.arrivee?.quai || horaire.quai_arrivee || ''; - if (storedArr) { - const gareArr = garesList.find(g => String(g.id) === String(horaire.gare_arrivee_id) || g.nom === horaire.gare_arrivee_nom); - assignments['arrivee'] = resolveQuaiIdFromNom(gareArr, storedArr); - } else { - assignments['arrivee'] = ''; - } - setQuaiAssignments(prev => ({ ...prev, [`_modal_${horaire.id}`]: assignments })); - setShowQuaiModal(true); - }; - - const closeQuaiModal = () => { - setShowQuaiModal(false); - setQuaiSelectedHoraire(null); - }; - - const handleQuaiChange = (key, value) => { - if (!quaiSelectedHoraire) return; - setQuaiAssignments(prev => ({ - ...prev, - [`_modal_${quaiSelectedHoraire.id}`]: { - ...(prev[`_modal_${quaiSelectedHoraire.id}`] || {}), - [key]: value - } - })); - }; - - const handleSaveQuais = async (horaireIdFromModal, payloadFromModal) => { - // Allows both list view modal and schematic view modal to save - const targetHoraireId = horaireIdFromModal || (quaiSelectedHoraire ? quaiSelectedHoraire.id : null); - if (!targetHoraireId) return; - - setQuaiSaving(true); - try { - let quaisParOrdre = {}; - - if (payloadFromModal) { - quaisParOrdre = payloadFromModal; - } else if (quaiSelectedHoraire) { - const modalAssignments = quaiAssignments[`_modal_${quaiSelectedHoraire.id}`] || {}; - const gares = normalizeHoraireDesservies(quaiSelectedHoraire).intermediaires; - gares.forEach((g, idx) => { - const key = getDesservieKey(g, idx); - const quaiId = modalAssignments[key] || ''; - // Chercher le nom du quai dans la liste de la gare - const gareObj = garesList.find(gg => String(gg.id) === String(g.gare_id) || gg.nom === g.gare_nom); - const quaiObj = gareObj?.gare_quais?.find(q => String(q.id) === String(quaiId)); - quaisParOrdre[g.ordre ?? idx + 1] = quaiObj ? quaiObj.nom : (quaiId || null); - }); - - // Inclure les quais de départ/arrivée - if (Object.prototype.hasOwnProperty.call(modalAssignments, 'depart')) { - const departId = modalAssignments['depart'] || ''; - const gareDepartObj = garesList.find(gg => String(gg.id) === String(quaiSelectedHoraire.gare_depart_id) || gg.nom === quaiSelectedHoraire.gare_depart_nom); - const departQuaiObj = gareDepartObj?.gare_quais?.find(q => String(q.id) === String(departId)); - quaisParOrdre['depart'] = departQuaiObj ? departQuaiObj.nom : (departId || null); - } - if (Object.prototype.hasOwnProperty.call(modalAssignments, 'arrivee')) { - const arriveeId = modalAssignments['arrivee'] || ''; - const gareArrObj = garesList.find(gg => String(gg.id) === String(quaiSelectedHoraire.gare_arrivee_id) || gg.nom === quaiSelectedHoraire.gare_arrivee_nom); - const arriveeQuaiObj = gareArrObj?.gare_quais?.find(q => String(q.id) === String(arriveeId)); - quaisParOrdre['arrivee'] = arriveeQuaiObj ? arriveeQuaiObj.nom : (arriveeId || null); - } - } - - // Sauvegarder en BDD - const { error: saveErr } = await updateQuaisHoraire(targetHoraireId, quaisParOrdre); - if (saveErr) { - alert('Erreur lors de la sauvegarde des quais: ' + (saveErr.message || 'Erreur')); - setQuaiSaving(false); - return; - } - - // Recharger les horaires pour refléter les quais mis à jour - await loadData(); - if (quaiSelectedHoraire) { - closeQuaiModal(); - } - } catch (err) { - console.error('Erreur sauvegarde quais:', err); - alert('Erreur lors de la sauvegarde des quais'); - } - setQuaiSaving(false); - }; - - const handleSetDefaultQuais = () => { - if (!quaiSelectedHoraire) return; - if (!window.confirm('Définir les quais actuels comme quais par défaut pour cet horaire ?')) return; - // Copier les quais actuels comme défaut - const modalAssignments = quaiAssignments[`_modal_${quaiSelectedHoraire.id}`] || {}; - const defaults = { ...quaiAssignments }; - Object.entries(modalAssignments).forEach(([key, value]) => { - defaults[`default_${quaiSelectedHoraire.id}_${key}`] = value; - }); - setQuaiAssignments(defaults); - alert('Quais par défaut enregistrés.'); - }; - - // Trouver les quais d'une gare par id (prioritaire) ou nom - const getQuaisForGare = (gareId, gareNom) => { - const gareById = gareId ? garesList.find(g => String(g.id) === String(gareId)) : null; - const gare = gareById || garesList.find(g => g.nom === gareNom); - if (!gare) return []; - return gare.gare_quais || []; - }; - return ( -
-
-

Attribution des quais

- -
- {(quaiViewMode === 'schematique' || quaiViewMode === 'depart_arrivee') && ( - - )} - -
- - - -
+
+
+

+ + Attribution des Voies & Quais +

- setQuaiSearchQuery(e.target.value)} - className="horaires-search-input horaires-search-input-wide" - /> +
+ {(quaiViewMode === 'schematique' || quaiViewMode === 'depart_arrivee') && ( + + )} + +
+ + + +
+ +
+ + setQuaiSearchQuery(e.target.value)} + className="horaires-search-input" + />
+
-

- Cliquez sur un horaire pour attribuer les quais aux différentes gares du parcours. -

+

+ Supervisez et affectez les voies de circulation et quais pour chaque arrêt des trains programmés. +

- {loading ?

Chargement...

: quaiViewMode === 'liste' ? ( - filteredQuaiHoraires.length === 0 ? ( -
- -

Aucun horaire trouvé

-
- ) : ( -
- - - - - - - - - - - - - - {filteredQuaiHoraires.slice(0, 50).map((h, i) => { - const hasQuaisState = Object.keys(quaiAssignments).some(k => k.startsWith(`${h.id}_`) && quaiAssignments[k]); - const hasQuaisBDD = normalizeHoraireDesservies(h).all.some(g => g && g.quai); - const hasQuais = hasQuaisState || hasQuaisBDD; - return ( - openQuaiModal(h)} - > - - - - - - - - - ); - })} - -
N° TrainTypeDépartArrivéeLigneQuais attribuésAction
{h.numero_train || '-'}{h.type_train || '-'}{h.gare_depart_nom || '-'} {formatHeure(h.heure_depart)}{h.gare_arrivee_nom || '-'} {formatHeure(h.heure_arrivee)}{h.ligne_nom || '-'} - {hasQuais ? ( - - - Attribués - - ) : ( - Non attribués - )} - e.stopPropagation()}> - openQuaiModal(h)}> - Attribuer - -
-
- ) - ) : quaiViewMode === 'schematique' ? ( - /* Mode schématique */ -
- String(g.id) === quaiGareSelectionneeId) || null} - onUpdateQuai={handleSaveQuais} - onUpdateQuaisBatch={async (updates) => { - const { error } = await updateQuaisBatch(updates); - if (error) { - alert("Certaines attributions ont échoué."); - } - await loadData(); - }} - /> + {loading ? ( +
Chargement des attributions de quais...
+ ) : quaiViewMode === 'liste' ? ( + filteredQuaiHoraires.length === 0 ? ( +
+ +

Aucun horaire trouvé pour les critères sélectionnés.

) : ( - /* Mode départs/arrivées */ -
- String(g.id) === quaiGareSelectionneeId) || null} - onUpdateQuai={handleSaveQuais} - /> +
+ + + + + + + + + + + + + + {filteredQuaiHoraires.slice(0, 50).map((h, i) => { + const hasQuaisState = Object.keys(quaiAssignments).some(k => k.startsWith(`${h.id}_`) && quaiAssignments[k]); + const hasQuaisBDD = normalizeHoraireDesservies(h).all.some(g => g && g.quai); + const hasQuais = hasQuaisState || hasQuaisBDD; + + return ( + openQuaiModal(h)} + > + + + + + + + + + ); + })} + +
N° TrainTypeOrigineDestinationLigneStatut QuaiAction
{h.numero_train || '-'}{h.type_train || '-'} +
{h.gare_depart_nom || '-'}
+ {formatHeure(h.heure_depart)} +
+
{h.gare_arrivee_nom || '-'}
+ {formatHeure(h.heure_arrivee)} +
{h.ligne_nom || '-'} + {hasQuais ? ( + + + Quais attribués + + ) : ( + Non attribués + )} + e.stopPropagation()}> + openQuaiModal(h)}> + Configurer + +
- )} - - {/* Modal attribution des quais (Mode liste) */} - {showQuaiModal && quaiSelectedHoraire && ( -
-
e.stopPropagation()}> -
-

Attribution des quais - Train {quaiSelectedHoraire.numero_train}

- -
-
-
- {quaiSelectedHoraire.gare_depart_nom} ({formatHeure(quaiSelectedHoraire.heure_depart)}) - → {quaiSelectedHoraire.gare_arrivee_nom} ({formatHeure(quaiSelectedHoraire.heure_arrivee)}) -
- - {/* Gare de départ */} -
-
-
- Départ: {quaiSelectedHoraire.gare_depart_nom} - {formatHeure(quaiSelectedHoraire.heure_depart)} -
- -
-
- - {/* Gares desservies */} - {getFilteredDesservies(quaiSelectedHoraire).map((g, idx) => ( -
-
-
- Arrêt: {g.gare_nom} - - {g.heure_arrivee ? formatHeure(g.heure_arrivee) : ''} → {g.heure_depart ? formatHeure(g.heure_depart) : ''} - -
- -
-
- ))} - - {/* Gare d'arrivée */} -
-
-
- Arrivée: {quaiSelectedHoraire.gare_arrivee_nom} - {formatHeure(quaiSelectedHoraire.heure_arrivee)} -
- -
-
-
-
- - Définir par défaut - -
- Annuler - handleSaveQuais()} disabled={quaiSaving}> - {quaiSaving ? 'Enregistrement...' : 'Enregistrer'} - -
-
-
-
- )} -
+ ) + ) : quaiViewMode === 'schematique' ? ( +
+ String(g.id) === quaiGareSelectionneeId) || null} + onUpdateQuai={handleSaveQuais} + onUpdateQuaisBatch={async (updates) => { + const { error: batchErr } = await updateQuaisBatch(updates); + if (batchErr) { + alert("Certaines attributions ont échoué."); + } + await loadData(); + }} + /> +
+ ) : ( +
+ String(g.id) === quaiGareSelectionneeId) || null} + onUpdateQuai={handleSaveQuais} + /> +
+ )} +
); - } + }; - function renderServiceModal() { - return ( -
-
e.stopPropagation()}> -
-

{editingService ? 'Modifier le service annuel' : 'Nouveau service annuel'}

- -
-
- {formError && Erreur{formError}} + // ============================================================================ + // RENDERING MODALS (LIGNE, SERVICE, ASSIGN, QUAI) + // ============================================================================ -
- - setServiceNom(e.target.value)} placeholder="Ex: Hiver 2025-2026" /> -
+ const renderLigneModal = () => ( +
+
e.stopPropagation()}> +
+

{editingLigne ? 'Modifier la ligne' : 'Nouvelle ligne ferroviaire'}

+ +
+
+ {formError && ( + + Erreur + {formError} + + )} -
- - setServiceCode(e.target.value)} placeholder="Ex: H2526" /> +
+ {[ + { key: 'infos', label: 'Informations & Tracé' }, + { key: 'fiches-horaires', label: 'Fiche Horaire PDF' } + ].map(tab => ( + + ))} +
+ + {ligneModalTab === 'infos' && ( + <> +
+ + setLigneNom(e.target.value)} + placeholder="Ex: Paris Montparnasse - Rennes" + />
- - setServiceDateDebut(e.target.value)} /> + + setLigneCode(e.target.value)} + placeholder="Ex: L01" + />
- - setServiceDateFin(e.target.value)} /> + +
-
- -