RELEASE V.1.2.1 - Voici les fonctionnalités :

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

No files matched your search

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