Files
ferrovia-panel/src/pages/dashboard/Dashboard.js
T
AdminGit f77d59ca1c V5.0.0 //
-> Refonte du système de GESTION DES ROULEMENTS de matériels roulants
-> Optimisation rendue BDD
2026-10-03 17:33:53 +02:00

2044 lines
99 KiB
JavaScript

import React, { useEffect, useState, useCallback, useRef } from 'react';
import { getAllHoraires } from '../../api/horaires';
import { getAllPerturbations } from '../../api/perturbations';
import { getAllTravaux } from '../../api/travaux';
import { getAllMateriels } from '../../api/materielsRoulants';
import { getCurrentUser, getScopedRegionId } from '../../utils/userUtils';
import './Dashboard.css';
export default function Dashboard() {
// ---------------------------------------------------------------------------
// ÉTATS DES DONNÉES & UTILISATEUR
// ---------------------------------------------------------------------------
const [loading, setLoading] = useState(true);
const [lastRefreshed, setLastRefreshed] = useState(new Date());
const [currentUser, setCurrentUser] = useState(null);
const [scopedRegionId, setScopedRegionId] = useState(null);
// Données brutes
const [horaires, setHoraires] = useState([]);
const [perturbations, setPerturbations] = useState([]);
const [travaux, setTravaux] = useState([]);
const [materiels, setMateriels] = useState([]);
// Navigation par onglet ('overview', 'schedules', 'incidents', 'works', 'fleet')
const [activeTab, setActiveTab] = useState('overview');
// Options & Auto-refresh
const [autoRefresh, setAutoRefresh] = useState(false);
const [refreshCountdown, setRefreshCountdown] = useState(30);
// Filtres et recherche
const [searchTerm, setSearchTerm] = useState('');
const [selectedTypeFilter, setSelectedTypeFilter] = useState('all');
const [selectedLineFilter, setSelectedLineFilter] = useState('');
const [selectedPerturbationType, setSelectedPerturbationType] = useState('all');
const [punctualityFilterMode, setPunctualityFilterMode] = useState('all'); // 'all', 'delayed', 'ontime'
const [onlyWithStops, setOnlyWithStops] = useState(false);
const [onlyWithTrack, setOnlyWithTrack] = useState(false);
// Limite dynamique des départs imminents (contrôlée par wcs-counter)
const [imminentLimit, setImminentLimit] = useState(6);
// Modal de détail interactif
const [selectedItem, setSelectedItem] = useState(null); // { type: 'horaire'|'perturbation', data: ... }
// Références WCS Web Components
const tabsRef = useRef(null);
const modalRef = useRef(null);
const switchRef = useRef(null);
const radioGroupRef = useRef(null);
const checkStopsRef = useRef(null);
const checkTrackRef = useRef(null);
const counterRef = useRef(null);
// ---------------------------------------------------------------------------
// CHARGEMENT COMPLET DES DONNÉES DU RÉSEAU
// ---------------------------------------------------------------------------
const loadDashboardData = useCallback(async () => {
setLoading(true);
try {
const user = getCurrentUser();
setCurrentUser(user);
const regionId = getScopedRegionId(user);
setScopedRegionId(regionId || null);
// Chargement en parallèle de toutes les entités du système
const [horairesRes, perturbationsRes, travauxRes, materielsRes] = await Promise.all([
getAllHoraires(regionId),
getAllPerturbations(regionId),
getAllTravaux(regionId).catch(() => ({ data: [] })),
getAllMateriels().catch(() => ({ data: [] }))
]);
setHoraires(horairesRes?.data || []);
setPerturbations(perturbationsRes?.data || []);
setTravaux(travauxRes?.data || []);
setMateriels(materielsRes?.data || []);
setLastRefreshed(new Date());
setRefreshCountdown(30);
} catch (error) {
console.error('[Dashboard] Erreur de chargement des données ferroviaires:', error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadDashboardData();
}, [loadDashboardData]);
// Synchronisation WCS Tabs
useEffect(() => {
const tabsEl = tabsRef.current;
if (tabsEl) {
const handleTabChange = (e) => {
const key = e.detail?.selectedKey || e.detail?.tabName;
if (key) setActiveTab(key);
};
tabsEl.addEventListener('tabChange', handleTabChange);
return () => tabsEl.removeEventListener('tabChange', handleTabChange);
}
}, []);
// Synchronisation WCS Modal
useEffect(() => {
const modalEl = modalRef.current;
if (modalEl) {
const handleModalClose = () => setSelectedItem(null);
modalEl.addEventListener('wcsDialogClosed', handleModalClose);
return () => modalEl.removeEventListener('wcsDialogClosed', handleModalClose);
}
}, []);
// Synchronisation WCS Switch Auto-Refresh
useEffect(() => {
const switchEl = switchRef.current;
if (switchEl) {
const handleSwitch = (e) => {
setAutoRefresh(!!e.detail?.checked);
};
switchEl.addEventListener('wcsChange', handleSwitch);
return () => switchEl.removeEventListener('wcsChange', handleSwitch);
}
}, []);
// Synchronisation WCS Radio-Group (Punctuality mode)
useEffect(() => {
const rgEl = radioGroupRef.current;
if (rgEl) {
const handleRadioChange = (e) => {
if (e.detail?.value) {
setPunctualityFilterMode(e.detail.value);
}
};
rgEl.addEventListener('wcsChange', handleRadioChange);
return () => rgEl.removeEventListener('wcsChange', handleRadioChange);
}
}, [activeTab]);
// Synchronisation WCS Checkboxes
useEffect(() => {
const stopsEl = checkStopsRef.current;
const trackEl = checkTrackRef.current;
const handleStopsChange = (e) => setOnlyWithStops(!!e.detail?.checked);
const handleTrackChange = (e) => setOnlyWithTrack(!!e.detail?.checked);
if (stopsEl) stopsEl.addEventListener('wcsChange', handleStopsChange);
if (trackEl) trackEl.addEventListener('wcsChange', handleTrackChange);
return () => {
if (stopsEl) stopsEl.removeEventListener('wcsChange', handleStopsChange);
if (trackEl) trackEl.removeEventListener('wcsChange', handleTrackChange);
};
}, [activeTab]);
// Synchronisation WCS Counter (Nombre de trains affichés)
useEffect(() => {
const counterEl = counterRef.current;
if (counterEl) {
const handleCounterChange = (e) => {
if (typeof e.detail?.value === 'number') {
setImminentLimit(e.detail.value);
}
};
counterEl.addEventListener('wcsChange', handleCounterChange);
return () => counterEl.removeEventListener('wcsChange', handleCounterChange);
}
}, [activeTab]);
// Timer de rafraîchissement automatique
useEffect(() => {
if (!autoRefresh) return undefined;
const interval = setInterval(() => {
setRefreshCountdown((prev) => {
if (prev <= 1) {
loadDashboardData();
return 30;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(interval);
}, [autoRefresh, loadDashboardData]);
// ---------------------------------------------------------------------------
// CALCULS STATISTIQUES & INDICATEURS MÉTIERS SNCF
// ---------------------------------------------------------------------------
const totalHoraires = horaires.length;
const activePerturbations = perturbations.filter(p => p.actif);
const countRetards = activePerturbations.filter(p => p.type_perturbation === 'retard').length;
const countSuppressions = activePerturbations.filter(p => p.type_perturbation === 'suppression').length;
const countModifs = activePerturbations.filter(p => p.type_perturbation === 'modification_parcours').length;
// Calcul du taux de ponctualité contractuel SNCF
const punctualityRate = totalHoraires > 0
? Math.max(0, Math.min(100, Math.round(((totalHoraires - activePerturbations.length) / totalHoraires) * 100)))
: 100;
// Extraction des gares, lignes et types uniques
const uniqueGares = new Set();
const uniqueLignes = new Set();
const trainTypes = new Set();
horaires.forEach(h => {
if (h.gare_depart_nom) uniqueGares.add(h.gare_depart_nom);
if (h.gare_arrivee_nom) uniqueGares.add(h.gare_arrivee_nom);
if (h.ligne_nom) uniqueLignes.add(h.ligne_nom);
if (h.type_train) trainTypes.add(h.type_train);
if (Array.isArray(h.gares_desservies)) {
h.gares_desservies.forEach(g => {
if (g.gare_nom) uniqueGares.add(g.gare_nom);
});
}
});
const linesList = Array.from(uniqueLignes).sort();
const typesList = Array.from(trainTypes).sort();
// Navigation applicative
const navigate = (path) => {
window.location.href = path;
};
const formatHeure = (h) => {
if (!h) return '—';
return String(h).substring(0, 5);
};
// Circulations filtrées
const filteredHoraires = horaires.filter(h => {
const pert = activePerturbations.find(p => p.horaire_id === h.id || p.numero_train === h.numero_train);
const hasIssue = !!pert;
// Filtre ponctualité (radio group)
if (punctualityFilterMode === 'delayed' && !hasIssue) return false;
if (punctualityFilterMode === 'ontime' && hasIssue) return false;
// Filtre checkboxes
if (onlyWithStops && (!Array.isArray(h.gares_desservies) || h.gares_desservies.length === 0)) return false;
if (onlyWithTrack && !h.quai_depart && (!Array.isArray(h.attributions_quais) || h.attributions_quais.length === 0)) return false;
// Filtre texte
if (searchTerm.trim()) {
const q = searchTerm.toLowerCase();
const matchNum = String(h.numero_train || '').toLowerCase().includes(q);
const matchDepart = String(h.gare_depart_nom || '').toLowerCase().includes(q);
const matchArrivee = String(h.gare_arrivee_nom || '').toLowerCase().includes(q);
const matchLigne = String(h.ligne_nom || '').toLowerCase().includes(q);
if (!matchNum && !matchDepart && !matchArrivee && !matchLigne) return false;
}
// Filtre type de train
if (selectedTypeFilter !== 'all' && h.type_train !== selectedTypeFilter) {
return false;
}
// Filtre ligne
if (selectedLineFilter && h.ligne_nom !== selectedLineFilter) {
return false;
}
return true;
});
// Perturbations filtrées
const filteredPerturbations = perturbations.filter(p => {
if (selectedPerturbationType === 'retard' && p.type_perturbation !== 'retard') return false;
if (selectedPerturbationType === 'suppression' && p.type_perturbation !== 'suppression') return false;
if (selectedPerturbationType === 'active' && !p.actif) return false;
return true;
});
// Départs imminents (triés par heure de départ) avec limite ajustable via wcs-counter
const imminentDepartures = [...horaires]
.sort((a, b) => String(a.heure_depart || '').localeCompare(String(b.heure_depart || '')))
.slice(0, imminentLimit);
return (
<div className="dashboard-page-root sncf-groupe">
{/* ====================================================================
0. BANDEAU GALACTIQUE OFFICIEL SNCF (WCS-GALACTIC & WCS-GALACTIC-MENU)
==================================================================== */}
<wcs-galactic text="FERROVIA · Système National d'Exploitation & Supervision Ferroviaire">
<wcs-galactic-menu text="Portails Réseau & Partenaires">
<a href="https://www.sncf.com" target="_blank" rel="noreferrer">
<wcs-mat-icon icon="open_in_new" class="mr-1"></wcs-mat-icon> Portail Groupe SNCF
</a>
<a href="https://www.sncf-reseau.com" target="_blank" rel="noreferrer">
<wcs-mat-icon icon="open_in_new" class="mr-1"></wcs-mat-icon> SNCF Réseau Infra
</a>
<a href="https://www.sncf-voyageurs.com" target="_blank" rel="noreferrer">
<wcs-mat-icon icon="open_in_new" class="mr-1"></wcs-mat-icon> SNCF Voyageurs Mobilités
</a>
</wcs-galactic-menu>
</wcs-galactic>
<div className="dashboard-mastcontainer">
{/* ====================================================================
1. FIL D'ARIANE WCS (WCS-BREADCRUMB)
==================================================================== */}
<div className="dashboard-breadcrumb-wrapper mb-3">
<wcs-breadcrumb aria-label="Navigation hiérarchique">
<wcs-breadcrumb-item href="/">Console Ferrovia</wcs-breadcrumb-item>
<wcs-breadcrumb-item href="/dashboard">Supervision & Exploitation</wcs-breadcrumb-item>
<wcs-breadcrumb-item aria-current="page">
{scopedRegionId ? `Secteur Régional ${scopedRegionId}` : 'Réseau National Unifié'}
</wcs-breadcrumb-item>
</wcs-breadcrumb>
</div>
{/* ====================================================================
2. CONTROLBAR SNCF MÉTIERS & HEADER SUPÉRIEUR
==================================================================== */}
<header className="sncf-metier-controlbar controlbar" role="banner">
<div className="controlbar-top-row">
<div className="controlbar-identity">
<div className="controlbar-badges-row">
<span className="controlbar-kicker">
<wcs-mat-icon icon="speed"></wcs-mat-icon>
Supervision Réseau & Exploitation
</span>
{scopedRegionId ? (
<wcs-badge shape="rounded" className="wcs-primary">
<wcs-mat-icon icon="location_on" class="mr-1"></wcs-mat-icon>
Secteur Régional : {scopedRegionId}
</wcs-badge>
) : (
<wcs-badge shape="rounded" className="wcs-info">
<wcs-mat-icon icon="public" class="mr-1"></wcs-mat-icon>
Réseau National & Multi-Régions
</wcs-badge>
)}
<span className={`controlbar-status-pill ${activePerturbations.length > 0 ? 'is-disrupted' : 'is-nominal'}`}>
<wcs-mat-icon icon={activePerturbations.length > 0 ? 'warning' : 'check_circle'}></wcs-mat-icon>
{activePerturbations.length > 0
? `${activePerturbations.length} Aléa(s) d'exploitation en cours`
: 'Exploitation Nominale'}
</span>
</div>
<h1 className="controlbar-title">
Console de Pilotage Ferroviaire
{currentUser?.prenom || currentUser?.nom ? (
<span style={{ fontWeight: 400, opacity: 0.9 }}>
, {currentUser.prenom || currentUser.nom}
</span>
) : null}
</h1>
<p className="controlbar-subtitle">
Supervision en temps réel des circulations, régulation des voies en gare, suivi de la ponctualité contractuelle et information voyageurs.
</p>
</div>
<div className="controlbar-actions">
<div className="controlbar-clock-badge">
<span className="controlbar-pulse-dot"></span>
<span>
Synchro : {lastRefreshed.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
</span>
{autoRefresh && (
<span className="controlbar-refresh-timer">({refreshCountdown}s)</span>
)}
</div>
<div className="d-flex align-items-center gap-2" id="switch-autosync-container">
<span style={{ fontSize: '12px', fontWeight: 600, color: 'rgba(255,255,255,0.85)' }}>
Auto-sync :
</span>
<wcs-switch
id="switch-autosync"
ref={switchRef}
checked={autoRefresh}
></wcs-switch>
<wcs-tooltip for="switch-autosync" position="left">
Actualisation automatique de la console toutes les 30 secondes
</wcs-tooltip>
</div>
<div className="controlbar-buttons">
<wcs-button
id="btn-refresh"
className="wcs-primary"
onClick={loadDashboardData}
disabled={loading}
loading={loading}
>
<wcs-mat-icon icon="refresh"></wcs-mat-icon>
<span>Actualiser</span>
</wcs-button>
<wcs-tooltip for="btn-refresh" position="bottom">
Interroger immédiatement la base de données Supabase
</wcs-tooltip>
<wcs-button
id="btn-declare-alert"
className="wcs-warning"
onClick={() => navigate('/perturbations/creer')}
>
<wcs-mat-icon icon="warning"></wcs-mat-icon>
Signaler un aléa
</wcs-button>
<wcs-tooltip for="btn-declare-alert" position="bottom">
Ouvrir le formulaire de déclaration d'incident et régulation
</wcs-tooltip>
{/* Actions Métiers avec wcs-dropdown-header */}
<wcs-dropdown>
<div slot="placeholder">
<div style={{ display: 'inline-flex', alignItems: 'center', gap: '6px' }}>
<wcs-mat-icon icon="bolt"></wcs-mat-icon>
<span>Actions Métiers</span>
</div>
</div>
<wcs-dropdown-header>Circulations & Voies</wcs-dropdown-header>
<wcs-dropdown-item onClick={() => navigate('/horaires')}>
<wcs-mat-icon icon="add_circle_outline" class="mr-2"></wcs-mat-icon> Nouvelle circulation
</wcs-dropdown-item>
<wcs-dropdown-item onClick={() => navigate('/horaires/attribution-quais')}>
<wcs-mat-icon icon="alt_route" class="mr-2"></wcs-mat-icon> Régulation voies & quais
</wcs-dropdown-item>
<wcs-dropdown-item onClick={() => navigate('/attribution-materiel-roulant')}>
<wcs-mat-icon icon="directions_railway" class="mr-2"></wcs-mat-icon> Affectation rames
</wcs-dropdown-item>
<wcs-dropdown-divider></wcs-dropdown-divider>
<wcs-dropdown-header>Aléas & Infrastructure</wcs-dropdown-header>
<wcs-dropdown-item onClick={() => navigate('/perturbations/creer')}>
<wcs-mat-icon icon="warning" class="mr-2"></wcs-mat-icon> Déclarer un incident
</wcs-dropdown-item>
<wcs-dropdown-item onClick={() => navigate('/travaux/creer')}>
<wcs-mat-icon icon="construction" class="mr-2"></wcs-mat-icon> Déclarer des travaux
</wcs-dropdown-item>
<wcs-dropdown-divider></wcs-dropdown-divider>
<wcs-dropdown-header>Information Voyageurs</wcs-dropdown-header>
<wcs-dropdown-item onClick={() => navigate('/fiches-horaires')}>
<wcs-mat-icon icon="description" class="mr-2"></wcs-mat-icon> Fiches horaires & affiches
</wcs-dropdown-item>
<wcs-dropdown-item onClick={() => navigate('/annonces-sonores')}>
<wcs-mat-icon icon="volume_up" class="mr-2"></wcs-mat-icon> Annonces sonores en gare
</wcs-dropdown-item>
</wcs-dropdown>
</div>
</div>
</div>
</header>
{/* ====================================================================
3. ALERTE DE SUPERVISION OPÉRATIONNELLE (WCS ALERT)
==================================================================== */}
<div className="dashboard-alert-wrapper">
{activePerturbations.length > 0 ? (
<wcs-alert intent="warning" show>
<div slot="title" style={{ fontWeight: 700 }}>
Alerte Réseau : {activePerturbations.length} perturbation(s) active(s) nécessitant une régulation
</div>
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2">
<span>
{countRetards} retard(s) cumulé(s), {countSuppressions} circulation(s) supprimée(s){countModifs > 0 ? ` et ${countModifs} modification(s) de tracé` : ''}. Les plans de transport de substitution et les affichages voyageurs sont actualisés.
</span>
<wcs-button
className="wcs-secondary"
size="s"
onClick={() => setActiveTab('incidents')}
>
<wcs-mat-icon icon="visibility"></wcs-mat-icon>
Examiner les perturbations
</wcs-button>
</div>
</wcs-alert>
) : (
<wcs-alert intent="info" show>
<div slot="title" style={{ fontWeight: 700 }}>
Situation Nominale : Aucune perturbation active signalée
</div>
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2">
<span>
L'ensemble des circulations ferroviaires s'effectue dans le respect des plages horaires et de la régularité cible.
</span>
<wcs-badge className="wcs-success" shape="rounded">
100% de conformité observée
</wcs-badge>
</div>
</wcs-alert>
)}
</div>
{/* ====================================================================
4. CARTES KPI EXÉCUTIVES (WCS CARDS + SKELETONS LORS DU CHARGEMENT)
==================================================================== */}
{loading ? (
<section className="dashboard-kpis-container" aria-label="Chargement des indicateurs">
{[1, 2, 3, 4, 5].map(i => (
<wcs-card key={i} mode="raised" className="dashboard-wcs-card">
<wcs-card-body>
<div className="d-flex justify-content-between align-items-center mb-3">
<wcs-skeleton-circle style={{ width: '42px', height: '42px' }}></wcs-skeleton-circle>
<wcs-skeleton-rectangle style={{ width: '80px', height: '22px', borderRadius: '12px' }}></wcs-skeleton-rectangle>
</div>
<wcs-skeleton-text style={{ width: '50%', height: '32px', marginBottom: '8px' }}></wcs-skeleton-text>
<wcs-skeleton-text style={{ width: '70%', height: '14px', marginBottom: '14px' }}></wcs-skeleton-text>
<wcs-skeleton-rectangle style={{ width: '100%', height: '6px', borderRadius: '4px' }}></wcs-skeleton-rectangle>
</wcs-card-body>
</wcs-card>
))}
</section>
) : (
<section className="dashboard-kpis-container" aria-label="Indicateurs clés de performance">
{/* KPI 1 : Régularité / Ponctualité */}
<wcs-card
mode="raised"
className="dashboard-wcs-card kpi-card-radial"
onClick={() => setActiveTab('overview')}
id="kpi-punctuality-card"
>
<wcs-card-body>
<div className="kpi-card-header">
<div className="kpi-icon-square bg-punctuality">
<wcs-mat-icon icon="analytics"></wcs-mat-icon>
</div>
<wcs-badge
className={punctualityRate >= 95 ? 'wcs-success' : 'wcs-warning'}
shape="rounded"
>
{punctualityRate >= 95 ? 'Conforme SLA' : 'Sous surveillance'}
</wcs-badge>
</div>
<div className="kpi-main-body">
<div className="kpi-value-row">
<div>
<div className="kpi-big-number">{punctualityRate}%</div>
<div className="kpi-title">Taux de Régularité</div>
</div>
<wcs-progress-radial
value={punctualityRate}
size={64}
show-label="false"
></wcs-progress-radial>
</div>
<p className="kpi-subtitle">
Objectif cible contractuel SNCF : 95.0%
</p>
</div>
<wcs-divider></wcs-divider>
<div className="kpi-card-footer">
<span>Détail ponctualité</span>
<wcs-mat-icon icon="arrow_forward"></wcs-mat-icon>
</div>
</wcs-card-body>
</wcs-card>
<wcs-tooltip for="kpi-punctuality-card" position="top">
Taux calculé d'après les circulations théoriques moins les incidents actifs
</wcs-tooltip>
{/* KPI 2 : Circulations programmées */}
<wcs-card
mode="raised"
className="dashboard-wcs-card"
onClick={() => setActiveTab('schedules')}
id="kpi-trains-card"
>
<wcs-card-body>
<div className="kpi-card-header">
<div className="kpi-icon-square bg-trains">
<wcs-mat-icon icon="schedule"></wcs-mat-icon>
</div>
<wcs-badge className="wcs-primary" shape="rounded">
{totalHoraires} sillons
</wcs-badge>
</div>
<div className="kpi-main-body">
<div className="kpi-big-number">{totalHoraires}</div>
<div className="kpi-title">Circulations Actives</div>
<p className="kpi-subtitle">
{totalHoraires - activePerturbations.length} trains conformes à l'horaire
</p>
<div className="kpi-progress-wrap">
<wcs-progress-bar
value={punctualityRate}
size="s"
></wcs-progress-bar>
</div>
</div>
<wcs-divider></wcs-divider>
<div className="kpi-card-footer">
<span>Plan de transport</span>
<wcs-mat-icon icon="arrow_forward"></wcs-mat-icon>
</div>
</wcs-card-body>
</wcs-card>
{/* KPI 3 : Perturbations & Aléas */}
<wcs-card
mode="raised"
className="dashboard-wcs-card"
onClick={() => setActiveTab('incidents')}
id="kpi-alerts-card"
>
<wcs-card-body>
<div className="kpi-card-header">
<div className="kpi-icon-square bg-disruptions">
<wcs-mat-icon icon="warning_amber"></wcs-mat-icon>
</div>
<wcs-badge
className={activePerturbations.length > 0 ? 'wcs-danger' : 'wcs-success'}
shape="rounded"
>
{activePerturbations.length} active(s)
</wcs-badge>
</div>
<div className="kpi-main-body">
<div className="kpi-big-number">{perturbations.length}</div>
<div className="kpi-title">Aléas Enregistrés</div>
<p className="kpi-subtitle">
{countRetards} retard(s) · {countSuppressions} suppression(s)
</p>
<div className="d-flex gap-1 mt-2">
<wcs-chip label={`${countRetards} retards`} variant="secondary"></wcs-chip>
<wcs-chip label={`${countSuppressions} suppressions`} variant="secondary"></wcs-chip>
</div>
</div>
<wcs-divider></wcs-divider>
<div className="kpi-card-footer">
<span>Gestion des incidents</span>
<wcs-mat-icon icon="arrow_forward"></wcs-mat-icon>
</div>
</wcs-card-body>
</wcs-card>
{/* KPI 4 : Travaux & Chantiers */}
<wcs-card
mode="raised"
className="dashboard-wcs-card"
onClick={() => setActiveTab('works')}
id="kpi-works-card"
>
<wcs-card-body>
<div className="kpi-card-header">
<div className="kpi-icon-square bg-works">
<wcs-mat-icon icon="construction"></wcs-mat-icon>
</div>
<wcs-badge className="wcs-info" shape="rounded">
Infra & Voies
</wcs-badge>
</div>
<div className="kpi-main-body">
<div className="kpi-big-number">{travaux.length}</div>
<div className="kpi-title">Chantiers & Travaux</div>
<p className="kpi-subtitle">
Impact voies et bus de substitution
</p>
<div className="d-flex gap-1 mt-2">
<wcs-badge className="wcs-warning" shape="rounded">
{travaux.reduce((acc, t) => acc + (t.substitutions?.length || 0), 0)} substitution(s)
</wcs-badge>
</div>
</div>
<wcs-divider></wcs-divider>
<div className="kpi-card-footer">
<span>Suivi chantiers</span>
<wcs-mat-icon icon="arrow_forward"></wcs-mat-icon>
</div>
</wcs-card-body>
</wcs-card>
{/* KPI 5 : Réseau & Gares */}
<wcs-card
mode="raised"
className="dashboard-wcs-card"
onClick={() => setActiveTab('fleet')}
id="kpi-network-card"
>
<wcs-card-body>
<div className="kpi-card-header">
<div className="kpi-icon-square bg-network">
<wcs-mat-icon icon="alt_route"></wcs-mat-icon>
</div>
<wcs-badge className="wcs-secondary" shape="rounded">
Maillage
</wcs-badge>
</div>
<div className="kpi-main-body">
<div className="kpi-big-number">{uniqueGares.size}</div>
<div className="kpi-title">Gares & Points d'Arrêt</div>
<p className="kpi-subtitle">
{uniqueLignes.size} lignes · {materiels.length} rames affectées
</p>
<div className="d-flex gap-1 mt-2">
<wcs-badge className="wcs-primary" shape="rounded">
{uniqueLignes.size} lignes commerciales
</wcs-badge>
</div>
</div>
<wcs-divider></wcs-divider>
<div className="kpi-card-footer">
<span>Réseau & Matériels</span>
<wcs-mat-icon icon="arrow_forward"></wcs-mat-icon>
</div>
</wcs-card-body>
</wcs-card>
</section>
)}
{/* ====================================================================
5. NAVIGATION PAR ONGLETS AVEC WCS ACTION-BAR & WCS TABS
==================================================================== */}
<div className="dashboard-navigation-card">
<wcs-action-bar gutter className="dashboard-wcs-actionbar">
<div className="d-flex align-items-center gap-2">
<wcs-mat-icon icon="domain"></wcs-mat-icon>
<span style={{ fontSize: '18px', fontWeight: 800, color: '#002244' }}>
Poste de Commande & Exploitation
</span>
</div>
<div slot="actions" className="d-flex align-items-center gap-2">
<wcs-badge className="wcs-primary" shape="rounded">
{scopedRegionId ? `Secteur ${scopedRegionId}` : 'National'}
</wcs-badge>
<wcs-badge className={activePerturbations.length > 0 ? 'wcs-warning' : 'wcs-success'} shape="rounded">
{punctualityRate}% ponctualité
</wcs-badge>
</div>
</wcs-action-bar>
<div className="dashboard-tabs-wrapper">
<wcs-tabs
ref={tabsRef}
selected-key={activeTab}
headers-only="true"
>
<wcs-tab
header="Vue d'ensemble opérationnelle"
item-key="overview"
onClick={() => setActiveTab('overview')}
></wcs-tab>
<wcs-tab
header={`Circulations & Horaires (${totalHoraires})`}
item-key="schedules"
onClick={() => setActiveTab('schedules')}
></wcs-tab>
<wcs-tab
header={`Incidents & Aléas (${activePerturbations.length})`}
item-key="incidents"
onClick={() => setActiveTab('incidents')}
></wcs-tab>
<wcs-tab
header={`Chantiers & Travaux (${travaux.length})`}
item-key="works"
onClick={() => setActiveTab('works')}
></wcs-tab>
<wcs-tab
header={`Flotte & Gares (${materiels.length})`}
item-key="fleet"
onClick={() => setActiveTab('fleet')}
></wcs-tab>
</wcs-tabs>
</div>
{/* Barre de filtrage avancée WCS pour les onglets de circulations avec wcs-form-field */}
{(activeTab === 'schedules' || activeTab === 'overview') && (
<div className="dashboard-filter-toolbar">
<div className="filter-left-col">
<div className="filter-search-box">
<wcs-form-field>
<wcs-label>Recherche globale</wcs-label>
<wcs-input
icon="search"
placeholder="N° train, gare, ligne..."
value={searchTerm}
onInput={(e) => setSearchTerm(e.target.value)}
></wcs-input>
<wcs-hint>Filtrage temps réel du plan de transport</wcs-hint>
</wcs-form-field>
</div>
<div style={{ minWidth: '240px' }}>
<wcs-form-field>
<wcs-label>Ligne commerciale</wcs-label>
<wcs-native-select size="m">
<select
value={selectedLineFilter}
onChange={(e) => setSelectedLineFilter(e.target.value)}
>
<option value="">Toutes les lignes commerciales</option>
{linesList.map(l => (
<option key={l} value={l}>{l}</option>
))}
</select>
</wcs-native-select>
<wcs-hint>{linesList.length} ligne(s) répertoriée(s)</wcs-hint>
</wcs-form-field>
</div>
</div>
{/* Segmented Radio Group WCS pour filtrer l'état de ponctualité */}
<div className="filter-center-col">
<div style={{ display: 'flex', flexDirection: 'column', gap: '3px' }}>
<span style={{ fontSize: '11px', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.04em', color: '#64748b' }}>
État de circulation
</span>
<wcs-radio-group
ref={radioGroupRef}
mode="option"
value={punctualityFilterMode}
name="punctuality-mode"
>
<wcs-radio value="all" label="Tous"></wcs-radio>
<wcs-radio value="ontime" label="À l'heure"></wcs-radio>
<wcs-radio value="delayed" label="Aléas"></wcs-radio>
</wcs-radio-group>
</div>
</div>
<div className="filter-right-col">
<div className="filter-chips-row">
<wcs-chip
label="Tous les types"
mode="selectable"
selected={selectedTypeFilter === 'all'}
onClick={() => setSelectedTypeFilter('all')}
></wcs-chip>
{typesList.map(t => (
<wcs-chip
key={t}
label={t}
mode="selectable"
selected={selectedTypeFilter === t}
onClick={() => setSelectedTypeFilter(t)}
></wcs-chip>
))}
</div>
<div className="d-flex align-items-center gap-3 ml-2 border-left pl-3">
<wcs-checkbox
ref={checkStopsRef}
checked={onlyWithStops}
label="Arrêts"
></wcs-checkbox>
<wcs-checkbox
ref={checkTrackRef}
checked={onlyWithTrack}
label="Voie assignée"
></wcs-checkbox>
</div>
</div>
</div>
)}
{/* ==================================================================
6. CONTENU DES ONGLETS
================================================================== */}
<div className="dashboard-tab-content-area">
{/* ----------------------------------------------------------------
ONGLET 1 : VUE D'ENSEMBLE
---------------------------------------------------------------- */}
{activeTab === 'overview' && (
<div className="overview-grid">
{/* Colonne Gauche : Départs & Incidents */}
<div className="d-flex flex-column gap-3">
{/* Widget Départs Imminents avec WCS-COUNTER */}
<wcs-card mode="flat" className="p-0">
<wcs-card-body>
<wcs-card-header>
<div className="dashboard-section-title-wrap">
<h3 className="dashboard-section-title">
<wcs-mat-icon icon="schedule"></wcs-mat-icon>
Départs Imminents & Plan de Transport
</h3>
<div className="d-flex align-items-center gap-2">
<wcs-button
className="wcs-secondary"
size="s"
onClick={() => setActiveTab('schedules')}
>
Voir tout ({totalHoraires})
</wcs-button>
</div>
</div>
</wcs-card-header>
{/* Contrôle du volume avec WCS Counter */}
<div className="d-flex justify-content-between align-items-center mb-3 p-2 bg-light rounded">
<div className="d-flex align-items-center gap-2">
<wcs-mat-icon icon="tune" style={{ color: '#0088ce' }}></wcs-mat-icon>
<span style={{ fontSize: '13px', fontWeight: 600, color: '#334155' }}>
Volume de circulations affichées :
</span>
</div>
<div className="d-flex align-items-center gap-2">
<wcs-counter
ref={counterRef}
size="m"
min={3}
max={16}
step={1}
value={imminentLimit}
label="Nombre de départs affichés"
></wcs-counter>
<wcs-badge className="wcs-secondary" shape="rounded">
{imminentDepartures.length} / {totalHoraires}
</wcs-badge>
</div>
</div>
<wcs-card-content>
{loading ? (
<div className="p-3">
{[1, 2, 3].map(i => (
<div key={i} className="mb-3">
<wcs-skeleton-rectangle style={{ width: '100%', height: '54px', borderRadius: '8px' }}></wcs-skeleton-rectangle>
</div>
))}
</div>
) : imminentDepartures.length === 0 ? (
<div className="dashboard-empty-state">
<wcs-mat-icon icon="train"></wcs-mat-icon>
<h4>Aucune circulation enregistrée</h4>
<p>Ajoutez des horaires ou importez un plan de transport régional.</p>
<wcs-button className="wcs-primary" onClick={() => navigate('/horaires')}>
Ajouter un horaire
</wcs-button>
</div>
) : (
<div className="sncf-management-list">
{imminentDepartures.map(h => {
const pert = activePerturbations.find(p => p.horaire_id === h.id || p.numero_train === h.numero_train);
const isDelayed = pert?.type_perturbation === 'retard';
const isCancelled = pert?.type_perturbation === 'suppression';
return (
<div
key={h.id}
className={`management-item sncf-management-item ${isCancelled ? 'item-suppression' : isDelayed ? 'item-disruption' : 'item-ontime'}`}
onClick={() => setSelectedItem({ type: 'horaire', data: h })}
>
<div className="item-top-header">
<div className="d-flex align-items-center gap-2">
<span className="item-train-badge">
<wcs-mat-icon icon="train"></wcs-mat-icon>
{h.numero_train}
</span>
{h.type_train && (
<wcs-badge className="wcs-primary" shape="rounded">
{h.type_train}
</wcs-badge>
)}
{h.ligne_nom && (
<span style={{ fontSize: '12px', color: '#64748b' }}>
· {h.ligne_nom}
</span>
)}
</div>
<div>
{isCancelled ? (
<wcs-badge className="wcs-danger" shape="rounded">
<wcs-mat-icon icon="block" class="mr-1"></wcs-mat-icon>
Supprimé
</wcs-badge>
) : isDelayed ? (
<wcs-badge className="wcs-warning" shape="rounded">
<wcs-mat-icon icon="schedule" class="mr-1"></wcs-mat-icon>
+{pert.duree_retard_minutes || 0} min
</wcs-badge>
) : (
<wcs-badge className="wcs-success" shape="rounded">
À l'heure
</wcs-badge>
)}
</div>
</div>
<div className="item-route-display">
<span className="item-station-time">{formatHeure(h.heure_depart)}</span>
<span className="item-station-name">{h.gare_depart_nom || 'Départ'}</span>
<wcs-mat-icon icon="trending_flat" class="item-route-arrow"></wcs-mat-icon>
<span className="item-station-time">{formatHeure(h.heure_arrivee)}</span>
<span className="item-station-name">{h.gare_arrivee_nom || 'Arrivée'}</span>
</div>
<div className="item-bottom-meta">
<div className="d-flex align-items-center gap-2">
{h.quai_depart && (
<span className="item-quai-badge">Voie {h.quai_depart}</span>
)}
{Array.isArray(h.gares_desservies) && h.gares_desservies.length > 0 && (
<span>{h.gares_desservies.length} arrêt(s) intermédiaire(s)</span>
)}
</div>
<div className="d-flex align-items-center gap-1 text-primary">
<span style={{ fontSize: '11.5px', fontWeight: 600 }}>Détails & Régulation</span>
<wcs-mat-icon icon="chevron_right"></wcs-mat-icon>
</div>
</div>
</div>
);
})}
</div>
)}
</wcs-card-content>
</wcs-card-body>
</wcs-card>
{/* Widget Accordion Perturbations Actives */}
<wcs-card mode="flat" className="p-0">
<wcs-card-body>
<wcs-card-header>
<div className="dashboard-section-title-wrap">
<h3 className="dashboard-section-title">
<wcs-mat-icon icon="warning"></wcs-mat-icon>
Aléas & Incidents en Cours ({activePerturbations.length})
</h3>
<wcs-button
className="wcs-secondary"
size="s"
onClick={() => setActiveTab('incidents')}
>
Tous ({perturbations.length})
</wcs-button>
</div>
</wcs-card-header>
<wcs-card-content>
{activePerturbations.length === 0 ? (
<div className="dashboard-empty-state" style={{ padding: '32px 20px' }}>
<wcs-mat-icon icon="check_circle" style={{ color: '#10b981' }}></wcs-mat-icon>
<h4>Aucune perturbation active</h4>
<p>Le trafic est entièrement régulier sans retard majeur signalé sur le réseau.</p>
</div>
) : (
<wcs-accordion>
{activePerturbations.slice(0, 5).map(p => (
<wcs-accordion-panel key={p.id}>
<wcs-accordion-header>
<div className="d-flex justify-content-between align-items-center w-100 pr-3">
<div className="d-flex align-items-center gap-2">
<wcs-mat-icon icon="train"></wcs-mat-icon>
<strong>Train {p.horaire?.numero_train || p.numero_train || 'N/A'}</strong>
<span>({p.horaire?.gare_depart_nom} → {p.horaire?.gare_arrivee_nom})</span>
</div>
<wcs-badge
className={p.type_perturbation === 'suppression' ? 'wcs-danger' : 'wcs-warning'}
shape="rounded"
>
{p.type_perturbation === 'retard' ? `Retard +${p.duree_retard_minutes || 0} min` :
p.type_perturbation === 'suppression' ? 'Supprimé' : 'Modifié'}
</wcs-badge>
</div>
</wcs-accordion-header>
<wcs-accordion-content>
<div className="p-3 bg-light rounded">
<div className="mb-2">
<strong>Motif d'exploitation :</strong> {p.cause || 'Non spécifié'}
</div>
{p.cause_details && (
<p className="text-muted mb-2">{p.cause_details}</p>
)}
<div className="d-flex justify-content-between align-items-center mt-3 pt-2 border-top">
<span style={{ fontSize: '12px', color: '#64748b' }}>
Signalé le {p.date_debut ? new Date(p.date_debut).toLocaleTimeString() : '—'}
</span>
<div className="d-flex gap-2">
<wcs-button
size="s"
className="wcs-secondary"
onClick={() => setSelectedItem({ type: 'perturbation', data: p })}
>
Détails complets
</wcs-button>
<wcs-button
size="s"
className="wcs-warning"
onClick={() => navigate(`/perturbations/modifier/${p.id}`)}
>
Réguler
</wcs-button>
</div>
</div>
</div>
</wcs-accordion-content>
</wcs-accordion-panel>
))}
</wcs-accordion>
)}
</wcs-card-content>
</wcs-card-body>
</wcs-card>
</div>
{/* Colonne Droite : Raccourcis Opérationnels & Synthèse Métiers */}
<div className="d-flex flex-column gap-3">
{/* Centre d'Actions d'Exploitation */}
<wcs-card mode="flat" className="p-0">
<wcs-card-body>
<wcs-card-header>
<div className="dashboard-section-title-wrap">
<h3 className="dashboard-section-title">
<wcs-mat-icon icon="bolt"></wcs-mat-icon>
Centre d'Actions d'Exploitation
</h3>
</div>
</wcs-card-header>
<wcs-card-content>
<div className="sncf-actions-grid">
<a className="sncf-action-tile" href="/perturbations/creer">
<div className="action-tile-icon bg-red">
<wcs-mat-icon icon="add_alert"></wcs-mat-icon>
</div>
<div className="action-tile-content">
<strong>Déclarer un incident / retard</strong>
<span>Ajuster les horaires et diffuser les alertes</span>
</div>
<wcs-mat-icon icon="chevron_right" class="action-tile-chevron"></wcs-mat-icon>
</a>
<a className="sncf-action-tile" href="/travaux/creer">
<div className="action-tile-icon bg-amber">
<wcs-mat-icon icon="construction"></wcs-mat-icon>
</div>
<div className="action-tile-content">
<strong>Programmer des travaux</strong>
<span>Définir chantiers et bus de substitution</span>
</div>
<wcs-mat-icon icon="chevron_right" class="action-tile-chevron"></wcs-mat-icon>
</a>
<a className="sncf-action-tile" href="/horaires/attribution-quais">
<div className="action-tile-icon bg-blue">
<wcs-mat-icon icon="alt_route"></wcs-mat-icon>
</div>
<div className="action-tile-content">
<strong>Attribution des voies & quais</strong>
<span>Régulation schématique des départs et arrivées</span>
</div>
<wcs-mat-icon icon="chevron_right" class="action-tile-chevron"></wcs-mat-icon>
</a>
<a className="sncf-action-tile" href="/attribution-materiel-roulant">
<div className="action-tile-icon bg-indigo">
<wcs-mat-icon icon="directions_railway"></wcs-mat-icon>
</div>
<div className="action-tile-content">
<strong>Roulement du matériel</strong>
<span>Affectation des rames, compositions et capacités</span>
</div>
<wcs-mat-icon icon="chevron_right" class="action-tile-chevron"></wcs-mat-icon>
</a>
<a className="sncf-action-tile" href="/fiches-horaires">
<div className="action-tile-icon bg-emerald">
<wcs-mat-icon icon="picture_as_pdf"></wcs-mat-icon>
</div>
<div className="action-tile-content">
<strong>Générateur de fiches horaires</strong>
<span>Export PDF et affiches de gare certifiées SNCF</span>
</div>
<wcs-mat-icon icon="chevron_right" class="action-tile-chevron"></wcs-mat-icon>
</a>
<a className="sncf-action-tile" href="/annonces-sonores">
<div className="action-tile-icon bg-purple">
<wcs-mat-icon icon="volume_up"></wcs-mat-icon>
</div>
<div className="action-tile-content">
<strong>Diffusion des annonces sonores</strong>
<span>Messages automatiques et jingles SNCF en gare</span>
</div>
<wcs-mat-icon icon="chevron_right" class="action-tile-chevron"></wcs-mat-icon>
</a>
</div>
</wcs-card-content>
</wcs-card-body>
</wcs-card>
{/* Synthèse du Réseau & Indicateurs Métiers */}
<wcs-card mode="flat" className="p-0">
<wcs-card-body>
<wcs-card-header>
<div className="dashboard-section-title-wrap">
<h3 className="dashboard-section-title">
<wcs-mat-icon icon="domain"></wcs-mat-icon>
Indicateurs Régionaux SNCF
</h3>
</div>
</wcs-card-header>
<wcs-card-content>
<div className="d-flex flex-column gap-3">
<div>
<div className="d-flex justify-content-between mb-1" style={{ fontSize: '13px' }}>
<span className="font-weight-bold">Conformité Plan de Transport</span>
<span className="font-weight-bold text-primary">{punctualityRate}%</span>
</div>
<wcs-progress-bar value={punctualityRate} size="m"></wcs-progress-bar>
</div>
<wcs-divider></wcs-divider>
<div className="d-flex justify-content-between align-items-center">
<div>
<div style={{ fontSize: '12px', color: '#64748b' }}>Matériels Roulants Actifs</div>
<strong style={{ fontSize: '18px', color: '#002244' }}>{materiels.length} Rames</strong>
</div>
<wcs-badge className="wcs-primary" shape="rounded">Flotte OK</wcs-badge>
</div>
<wcs-divider></wcs-divider>
<div className="d-flex justify-content-between align-items-center">
<div>
<div style={{ fontSize: '12px', color: '#64748b' }}>Chantiers programmés</div>
<strong style={{ fontSize: '18px', color: '#002244' }}>{travaux.length} Sites</strong>
</div>
<wcs-badge className="wcs-warning" shape="rounded">Voies</wcs-badge>
</div>
<wcs-divider></wcs-divider>
<div className="d-flex justify-content-between align-items-center">
<div>
<div style={{ fontSize: '12px', color: '#64748b' }}>Points d'arrêt surveillés</div>
<strong style={{ fontSize: '18px', color: '#002244' }}>{uniqueGares.size} Gares</strong>
</div>
<wcs-badge className="wcs-success" shape="rounded">Opérationnel</wcs-badge>
</div>
</div>
</wcs-card-content>
</wcs-card-body>
</wcs-card>
</div>
</div>
)}
{/* ----------------------------------------------------------------
ONGLET 2 : CIRCULATIONS & HORAIRES (TABLE BOOTSTRAP SNCF MÉTIERS)
---------------------------------------------------------------- */}
{activeTab === 'schedules' && (
<div>
<div className="d-flex justify-content-between align-items-center mb-3">
<div>
<h3 className="dashboard-section-title">
<wcs-mat-icon icon="train"></wcs-mat-icon>
Plan de Transport & Circulations
</h3>
<span style={{ fontSize: '13px', color: '#64748b' }}>
{filteredHoraires.length} circulation(s) affichée(s) sur {totalHoraires} au référentiel
</span>
</div>
<wcs-button className="wcs-primary" onClick={() => navigate('/horaires')}>
<wcs-mat-icon icon="add"></wcs-mat-icon>
Nouvel Horaire
</wcs-button>
</div>
{filteredHoraires.length === 0 ? (
<div className="dashboard-empty-state">
<wcs-mat-icon icon="search_off"></wcs-mat-icon>
<h4>Aucune circulation trouvée</h4>
<p>Modifiez vos critères de recherche ou réinitialisez les filtres.</p>
<wcs-button
className="wcs-secondary"
onClick={() => {
setSearchTerm('');
setSelectedTypeFilter('all');
setSelectedLineFilter('');
setPunctualityFilterMode('all');
setOnlyWithStops(false);
setOnlyWithTrack(false);
}}
>
Réinitialiser tous les filtres
</wcs-button>
</div>
) : (
<div className="table-responsive sncf-metier-table-container">
<table className="table table-hover table-sm sncf-metier-table">
<thead>
<tr>
<th>N° Train</th>
<th>Type</th>
<th>Ligne Commerciale</th>
<th>Départ</th>
<th>Arrivée</th>
<th>Arrêts</th>
<th>Voie</th>
<th>Statut Régulation</th>
<th style={{ textAlign: 'right' }}>Actions</th>
</tr>
</thead>
<tbody>
{filteredHoraires.map(h => {
const pert = activePerturbations.find(p => p.horaire_id === h.id || p.numero_train === h.numero_train);
const isDelayed = pert?.type_perturbation === 'retard';
const isCancelled = pert?.type_perturbation === 'suppression';
return (
<tr
key={h.id}
onClick={() => setSelectedItem({ type: 'horaire', data: h })}
>
<td className="table-train-num">
<div className="d-flex align-items-center gap-1">
<wcs-mat-icon icon="train" style={{ fontSize: '16px' }}></wcs-mat-icon>
<strong>{h.numero_train}</strong>
</div>
</td>
<td>
<wcs-badge className="wcs-primary" shape="rounded">
{h.type_train || 'TER'}
</wcs-badge>
</td>
<td>{h.ligne_nom || '—'}</td>
<td>
<span className="table-time">{formatHeure(h.heure_depart)}</span>
<span className="text-muted ml-2">{h.gare_depart_nom}</span>
</td>
<td>
<span className="table-time">{formatHeure(h.heure_arrivee)}</span>
<span className="text-muted ml-2">{h.gare_arrivee_nom}</span>
</td>
<td>
<wcs-badge className="wcs-secondary" shape="rounded">
{Array.isArray(h.gares_desservies) ? h.gares_desservies.length : 0} arrêt(s)
</wcs-badge>
</td>
<td>
{h.quai_depart ? (
<span className="item-quai-badge">Voie {h.quai_depart}</span>
) : (
<span className="text-muted">—</span>
)}
</td>
<td>
{isCancelled ? (
<wcs-badge className="wcs-danger" shape="rounded">
<wcs-mat-icon icon="block" class="mr-1"></wcs-mat-icon>
Supprimé
</wcs-badge>
) : isDelayed ? (
<wcs-badge className="wcs-warning" shape="rounded">
<wcs-mat-icon icon="schedule" class="mr-1"></wcs-mat-icon>
+{pert.duree_retard_minutes} min
</wcs-badge>
) : (
<wcs-badge className="wcs-success" shape="rounded">
<wcs-mat-icon icon="check" class="mr-1"></wcs-mat-icon>
À l'heure
</wcs-badge>
)}
</td>
<td style={{ textAlign: 'right' }}>
<div className="d-inline-flex align-items-center gap-1">
<wcs-button
className="wcs-secondary"
size="s"
onClick={(e) => {
e.stopPropagation();
setSelectedItem({ type: 'horaire', data: h });
}}
>
Détails
</wcs-button>
<wcs-dropdown>
<div slot="placeholder">
<wcs-button size="s" mode="stroked" className="wcs-secondary" onClick={(e) => e.stopPropagation()}>
<wcs-mat-icon icon="more_vert"></wcs-mat-icon>
</wcs-button>
</div>
<wcs-dropdown-header>Train {h.numero_train}</wcs-dropdown-header>
<wcs-dropdown-item onClick={(e) => { e.stopPropagation(); navigate('/horaires/attribution-quais'); }}>
<wcs-mat-icon icon="alt_route" class="mr-2"></wcs-mat-icon> Attribuer voie
</wcs-dropdown-item>
<wcs-dropdown-item onClick={(e) => { e.stopPropagation(); navigate('/perturbations/creer'); }}>
<wcs-mat-icon icon="warning" class="mr-2"></wcs-mat-icon> Déclarer retard
</wcs-dropdown-item>
<wcs-dropdown-item onClick={(e) => { e.stopPropagation(); navigate('/attribution-materiel-roulant'); }}>
<wcs-mat-icon icon="directions_railway" class="mr-2"></wcs-mat-icon> Affecter rame
</wcs-dropdown-item>
</wcs-dropdown>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
)}
{/* ----------------------------------------------------------------
ONGLET 3 : INCIDENTS & ALÉAS (SUPERVISION DISRUPTIONS)
---------------------------------------------------------------- */}
{activeTab === 'incidents' && (
<div>
<div className="d-flex justify-content-between align-items-center mb-3">
<div>
<h3 className="dashboard-section-title">
<wcs-mat-icon icon="warning"></wcs-mat-icon>
Gestion des Perturbations & Aléas d'Exploitation
</h3>
<span style={{ fontSize: '13px', color: '#64748b' }}>
{filteredPerturbations.length} perturbation(s) enregistrée(s)
</span>
</div>
<div className="d-flex gap-2">
<wcs-button
className="wcs-warning"
onClick={() => navigate('/perturbations/creer')}
>
<wcs-mat-icon icon="add"></wcs-mat-icon>
Déclarer un retard
</wcs-button>
<wcs-button
className="wcs-secondary"
onClick={() => navigate('/perturbations')}
>
Module complet
</wcs-button>
</div>
</div>
<div className="dashboard-filter-toolbar mb-3 rounded border">
<div className="d-flex align-items-center gap-2">
<span style={{ fontSize: '12px', fontWeight: 700, color: '#64748b' }}>Filtres :</span>
<wcs-chip
label="Toutes"
mode="selectable"
selected={selectedPerturbationType === 'all'}
onClick={() => setSelectedPerturbationType('all')}
></wcs-chip>
<wcs-chip
label="Actives en cours"
mode="selectable"
selected={selectedPerturbationType === 'active'}
onClick={() => setSelectedPerturbationType('active')}
></wcs-chip>
<wcs-chip
label="Retards"
mode="selectable"
selected={selectedPerturbationType === 'retard'}
onClick={() => setSelectedPerturbationType('retard')}
></wcs-chip>
<wcs-chip
label="Suppressions"
mode="selectable"
selected={selectedPerturbationType === 'suppression'}
onClick={() => setSelectedPerturbationType('suppression')}
></wcs-chip>
</div>
</div>
{filteredPerturbations.length === 0 ? (
<div className="dashboard-empty-state">
<wcs-mat-icon icon="check_circle" style={{ color: '#10b981' }}></wcs-mat-icon>
<h4>Aucune perturbation dans cette vue</h4>
<p>Toutes les circulations respectent les plages horaires contractuelles.</p>
</div>
) : (
<div className="table-responsive sncf-metier-table-container">
<table className="table table-hover table-sm sncf-metier-table">
<thead>
<tr>
<th>Train</th>
<th>Type d'aléa</th>
<th>Trajet Affecté</th>
<th>Impact / Retard</th>
<th>Cause Déclarée</th>
<th>État</th>
<th style={{ textAlign: 'right' }}>Actions</th>
</tr>
</thead>
<tbody>
{filteredPerturbations.map(p => (
<tr
key={p.id}
onClick={() => setSelectedItem({ type: 'perturbation', data: p })}
>
<td className="table-train-num">
<div className="d-flex align-items-center gap-1">
<wcs-mat-icon icon="warning" style={{ fontSize: '16px', color: '#d97706' }}></wcs-mat-icon>
<strong>Train {p.horaire?.numero_train || p.numero_train || 'N/A'}</strong>
</div>
</td>
<td>
<wcs-badge
className={p.type_perturbation === 'suppression' ? 'wcs-danger' : 'wcs-warning'}
shape="rounded"
>
{p.type_perturbation === 'retard' ? 'Retard' :
p.type_perturbation === 'suppression' ? 'Suppression' : 'Modification'}
</wcs-badge>
</td>
<td>
{p.horaire?.gare_depart_nom || 'Départ'} → {p.horaire?.gare_arrivee_nom || 'Arrivée'}
</td>
<td>
{p.type_perturbation === 'retard' ? (
<strong className="text-warning">+{p.duree_retard_minutes || 0} min</strong>
) : p.type_perturbation === 'suppression' ? (
<strong className="text-danger">Circulation Annulée</strong>
) : (
<span>Itinéraire Adapté</span>
)}
</td>
<td style={{ maxWidth: '300px' }} className="text-truncate">
{p.cause || 'Motif non spécifié'}
</td>
<td>
<wcs-badge
className={p.actif ? 'wcs-danger' : 'wcs-secondary'}
shape="rounded"
>
{p.actif ? 'En cours' : 'Résolu'}
</wcs-badge>
</td>
<td style={{ textAlign: 'right' }}>
<wcs-button
className="wcs-secondary"
size="s"
onClick={(e) => {
e.stopPropagation();
navigate(`/perturbations/modifier/${p.id}`);
}}
>
Réguler
</wcs-button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{/* ----------------------------------------------------------------
ONGLET 4 : TRAVAUX & CHANTIERS D'INFRASTRUCTURE
---------------------------------------------------------------- */}
{activeTab === 'works' && (
<div>
<div className="d-flex justify-content-between align-items-center mb-3">
<div>
<h3 className="dashboard-section-title">
<wcs-mat-icon icon="construction"></wcs-mat-icon>
Chantiers d'Infrastructure & Substitutions Routières
</h3>
<span style={{ fontSize: '13px', color: '#64748b' }}>
{travaux.length} chantier(s) répertorié(s) sur le réseau
</span>
</div>
<wcs-button className="wcs-warning" onClick={() => navigate('/travaux/creer')}>
<wcs-mat-icon icon="add"></wcs-mat-icon>
Nouveau Chantier
</wcs-button>
</div>
{travaux.length === 0 ? (
<div className="dashboard-empty-state">
<wcs-mat-icon icon="construction"></wcs-mat-icon>
<h4>Aucun chantier programmé</h4>
<p>Aucune interruption de voie ni bus de substitution n'est actuellement déclaré.</p>
</div>
) : (
<div className="row">
{travaux.map(t => (
<div key={t.id} className="col-12 col-lg-6 mb-3">
<wcs-card mode="flat" className="h-100">
<wcs-card-body>
<wcs-card-header>
<div className="d-flex justify-content-between align-items-start w-100">
<div>
<h4 style={{ margin: '0 0 4px', fontSize: '16px', fontWeight: 700 }}>
{t.titre || 'Chantier SNCF Réseau'}
</h4>
<span style={{ fontSize: '12px', color: '#64748b' }}>
Du {t.date_debut ? new Date(t.date_debut).toLocaleDateString() : '—'} au {t.date_fin ? new Date(t.date_fin).toLocaleDateString() : '—'}
</span>
</div>
<wcs-badge className="wcs-warning" shape="rounded">
{t.substitutions?.length || 0} autocar(s)
</wcs-badge>
</div>
</wcs-card-header>
<wcs-card-content>
<p style={{ fontSize: '13px', color: '#475569', margin: '0 0 12px' }}>
{t.description || 'Travaux de modernisation et renouvellement d\'infrastructure des voies ferrées.'}
</p>
<wcs-divider></wcs-divider>
<div className="d-flex justify-content-between align-items-center pt-2">
<span style={{ fontSize: '12px', color: '#64748b' }}>
Section : {t.ligne_nom || t.gare_depart_nom || 'Secteur régional'}
</span>
<wcs-button
className="wcs-secondary"
size="s"
onClick={() => navigate('/travaux')}
>
Accéder au dossier
</wcs-button>
</div>
</wcs-card-content>
</wcs-card-body>
</wcs-card>
</div>
))}
</div>
)}
</div>
)}
{/* ----------------------------------------------------------------
ONGLET 5 : FLOTTE & GARES (WCS LIST ITEMS)
---------------------------------------------------------------- */}
{activeTab === 'fleet' && (
<div>
<div className="d-flex justify-content-between align-items-center mb-3">
<div>
<h3 className="dashboard-section-title">
<wcs-mat-icon icon="directions_railway"></wcs-mat-icon>
Parc de Matériel Roulant & Maillage Gares
</h3>
<span style={{ fontSize: '13px', color: '#64748b' }}>
{materiels.length} rame(s) · {uniqueGares.size} gare(s) connectée(s)
</span>
</div>
<div className="d-flex gap-2">
<wcs-button
className="wcs-primary"
onClick={() => navigate('/attribution-materiel-roulant')}
>
Affectation Rames
</wcs-button>
<wcs-button
className="wcs-secondary"
onClick={() => navigate('/gares')}
>
Gestion Gares
</wcs-button>
</div>
</div>
<div className="row">
{/* Flotte Rames (avec wcs-list-item) */}
<div className="col-12 col-lg-7 mb-3">
<wcs-card mode="flat">
<wcs-card-body>
<wcs-card-header>
<div className="d-flex justify-content-between align-items-center w-100">
<h4 style={{ fontSize: '15px', fontWeight: 700, margin: 0 }}>
Flotte de Matériel Roulant
</h4>
<wcs-badge className="wcs-primary" shape="rounded">
{materiels.length} unités actives
</wcs-badge>
</div>
</wcs-card-header>
<wcs-card-content>
{materiels.length === 0 ? (
<div className="dashboard-empty-state">
<wcs-mat-icon icon="train"></wcs-mat-icon>
<h4>Aucun matériel répertorié</h4>
<p>Configurez le parc de rames TER et TGV dans le module dédié.</p>
</div>
) : (
<div role="list" className="wcs-custom-list-group">
{materiels.slice(0, 6).map(m => (
<wcs-list-item key={m.id} role="listitem">
<span slot="title" className="d-flex align-items-center gap-2">
<wcs-mat-icon icon="train" style={{ color: '#0088ce' }}></wcs-mat-icon>
<strong>{m.nom || m.serie || 'Rame SNCF'}</strong>
{m.type && <wcs-badge className="wcs-primary" shape="rounded">{m.type}</wcs-badge>}
</span>
<div slot="description">
Vitesse limite : {m.vitesse_max || '160'} km/h · Motorisation : {m.alimentation || 'Électrique / Bi-mode'}
</div>
<wcs-list-item-properties slot="properties">
<wcs-list-item-property>Capacité : {m.capacite || m.places_assises || '280'} places</wcs-list-item-property>
<wcs-list-item-property>Série : {m.serie || 'SNCF Z-51500'}</wcs-list-item-property>
<wcs-list-item-property>Disponibilité : 100%</wcs-list-item-property>
</wcs-list-item-properties>
<div slot="actions">
<wcs-button
size="s"
className="wcs-secondary"
onClick={() => navigate('/attribution-materiel-roulant')}
>
Affecter
</wcs-button>
</div>
</wcs-list-item>
))}
</div>
)}
</wcs-card-content>
</wcs-card-body>
</wcs-card>
</div>
{/* Hubs & Gares (avec wcs-list-item) */}
<div className="col-12 col-lg-5 mb-3">
<wcs-card mode="flat">
<wcs-card-body>
<wcs-card-header>
<div className="d-flex justify-content-between align-items-center w-100">
<h4 style={{ fontSize: '15px', fontWeight: 700, margin: 0 }}>
Principaux Hubs Ferroviaires
</h4>
<wcs-badge className="wcs-success" shape="rounded">
{uniqueGares.size} gares
</wcs-badge>
</div>
</wcs-card-header>
<wcs-card-content>
<div role="list" className="wcs-custom-list-group">
{Array.from(uniqueGares).slice(0, 7).map(g => (
<wcs-list-item key={g} role="listitem">
<span slot="title" className="d-flex align-items-center gap-2">
<wcs-mat-icon icon="location_on" style={{ color: '#0088ce' }}></wcs-mat-icon>
<strong>{g}</strong>
</span>
<div slot="description">
Pôle d'échange multimodal & correspondances régionales
</div>
<wcs-list-item-properties slot="properties">
<wcs-list-item-property>Voies équipées</wcs-list-item-property>
<wcs-list-item-property>Accès PMR</wcs-list-item-property>
</wcs-list-item-properties>
<div slot="actions">
<wcs-button
className="wcs-secondary"
size="s"
onClick={() => navigate('/horaires/attribution-quais')}
>
Quais & Voies
</wcs-button>
</div>
</wcs-list-item>
))}
</div>
</wcs-card-content>
</wcs-card-body>
</wcs-card>
</div>
</div>
</div>
)}
</div>
</div>
{/* ====================================================================
7. MODAL INTERACTIF DÉTAIL TRAIN / PERTURBATION AVEC WCS STEPPER
==================================================================== */}
<wcs-modal
ref={modalRef}
show={!!selectedItem}
size="l"
show-close-button="true"
>
<div slot="header">
<div className="d-flex align-items-center gap-2">
<wcs-mat-icon icon={selectedItem?.type === 'perturbation' ? 'warning' : 'train'}></wcs-mat-icon>
<h3 style={{ margin: 0, fontSize: '18px', fontWeight: 700 }}>
{selectedItem?.type === 'perturbation'
? `Détail de l'Aléa — Train ${selectedItem?.data?.horaire?.numero_train || selectedItem?.data?.numero_train || ''}`
: `Fiche d'Exploitation — Train ${selectedItem?.data?.numero_train || ''}`}
</h3>
</div>
</div>
{selectedItem && (
<div style={{ padding: '10px 0' }}>
{/* Jalonnement Stepper SNCF pour un horaire */}
{selectedItem.type === 'horaire' && (() => {
const pert = perturbations.find(p => p.actif && (p.horaire_id === selectedItem.data.id || p.numero_train === selectedItem.data.numero_train));
const isCancelled = pert?.type_perturbation === 'suppression';
const isDelayed = pert?.type_perturbation === 'retard';
return (
<div className="modal-stepper-box mb-4 p-3 bg-light rounded border">
<div className="d-flex justify-content-between align-items-center mb-3">
<span style={{ fontSize: '12px', fontWeight: 700, textTransform: 'uppercase', color: '#002244', letterSpacing: '0.05em' }}>
<wcs-mat-icon icon="timeline" class="mr-1"></wcs-mat-icon>
Jalonnement Opérationnel du Train
</span>
<wcs-badge className={isCancelled ? 'wcs-danger' : isDelayed ? 'wcs-warning' : 'wcs-success'} shape="rounded">
{isCancelled ? 'Circulation Annulée' : isDelayed ? `Retard (+${pert.duree_retard_minutes || 0}m)` : 'Phase active : En circulation'}
</wcs-badge>
</div>
<div className="d-flex align-items-center justify-content-between px-2 pt-1">
<div className="d-flex flex-column align-items-center text-center">
<wcs-badge className="wcs-success" shape="rounded"><wcs-mat-icon icon="build" size="s"></wcs-mat-icon></wcs-badge>
<span style={{ fontSize: '11px', fontWeight: 600, marginTop: '4px', color: '#16a34a' }}>Préparation</span>
</div>
<wcs-progress-bar value={100} size="s" style={{ flex: 1, margin: '0 8px' }}></wcs-progress-bar>
<div className="d-flex flex-column align-items-center text-center">
<wcs-badge className="wcs-success" shape="rounded"><wcs-mat-icon icon="train" size="s"></wcs-mat-icon></wcs-badge>
<span style={{ fontSize: '11px', fontWeight: 600, marginTop: '4px', color: '#16a34a' }}>Mise à Quai</span>
</div>
<wcs-progress-bar value={isCancelled ? 0 : 100} size="s" style={{ flex: 1, margin: '0 8px' }}></wcs-progress-bar>
<div className="d-flex flex-column align-items-center text-center">
<wcs-badge className={isCancelled ? 'wcs-danger' : isDelayed ? 'wcs-warning' : 'wcs-primary'} shape="rounded">
<wcs-mat-icon icon={isCancelled ? 'block' : isDelayed ? 'schedule' : 'navigation'} size="s"></wcs-mat-icon>
</wcs-badge>
<span style={{ fontSize: '11px', fontWeight: 700, marginTop: '4px', color: isCancelled ? '#dc2626' : isDelayed ? '#d97706' : '#0088ce' }}>
{isCancelled ? 'Annulé' : isDelayed ? 'Retardé' : 'En route'}
</span>
</div>
<wcs-progress-bar value={0} size="s" style={{ flex: 1, margin: '0 8px' }}></wcs-progress-bar>
<div className="d-flex flex-column align-items-center text-center">
<wcs-badge className="wcs-secondary" shape="rounded"><wcs-mat-icon icon="done" size="s"></wcs-mat-icon></wcs-badge>
<span style={{ fontSize: '11px', fontWeight: 600, marginTop: '4px', color: '#64748b' }}>Terminus</span>
</div>
</div>
</div>
);
})()}
{/* Contenu Fiche Circulation */}
{selectedItem.type === 'horaire' && (
<div>
<div className="modal-detail-grid">
<div className="modal-detail-card">
<h5>Identité du Sillon Ferroviaire</h5>
<div className="d-flex align-items-center gap-2 mb-2">
<wcs-badge className="wcs-primary" shape="rounded">
{selectedItem.data.type_train || 'TER'}
</wcs-badge>
<strong>N° {selectedItem.data.numero_train}</strong>
</div>
<div style={{ fontSize: '13px', color: '#475569' }}>
Ligne : <strong>{selectedItem.data.ligne_nom || 'Non spécifiée'}</strong>
</div>
<div style={{ fontSize: '13px', color: '#475569' }}>
Voie au départ : <strong>{selectedItem.data.quai_depart ? `Voie ${selectedItem.data.quai_depart}` : 'Non attribuée'}</strong>
</div>
</div>
<div className="modal-detail-card">
<h5>Horaires Contractuels</h5>
<div style={{ fontSize: '13px', marginBottom: '4px' }}>
Départ de <strong>{selectedItem.data.gare_depart_nom}</strong> : <strong>{formatHeure(selectedItem.data.heure_depart)}</strong>
</div>
<div style={{ fontSize: '13px', marginBottom: '4px' }}>
Arrivée à <strong>{selectedItem.data.gare_arrivee_nom}</strong> : <strong>{formatHeure(selectedItem.data.heure_arrivee)}</strong>
</div>
<div style={{ fontSize: '12px', color: '#64748b' }}>
Jours de circulation : {selectedItem.data.jours_circulation || 'Tous les jours'}
</div>
</div>
</div>
{/* Arrêts intermédiaires */}
{Array.isArray(selectedItem.data.gares_desservies) && selectedItem.data.gares_desservies.length > 0 && (
<div className="mt-3">
<h5 style={{ fontSize: '13px', fontWeight: 700, textTransform: 'uppercase', color: '#475569' }}>
Dessertes & Arrêts Intermédiaires ({selectedItem.data.gares_desservies.length})
</h5>
<div className="modal-route-timeline">
<div className="modal-timeline-stop is-origin">
<strong>{selectedItem.data.gare_depart_nom}</strong>
<span className="table-time">{formatHeure(selectedItem.data.heure_depart)}</span>
</div>
{selectedItem.data.gares_desservies.map((g, idx) => (
<div key={idx} className="modal-timeline-stop">
<span>{g.gare_nom}</span>
<span style={{ color: '#64748b' }}>
{formatHeure(g.heure_passage || g.heure_depart || g.heure_arrivee)}
</span>
</div>
))}
<div className="modal-timeline-stop is-destination">
<strong>{selectedItem.data.gare_arrivee_nom}</strong>
<span className="table-time">{formatHeure(selectedItem.data.heure_arrivee)}</span>
</div>
</div>
</div>
)}
</div>
)}
{/* Contenu Perturbation */}
{selectedItem.type === 'perturbation' && (
<div>
<div className="modal-detail-grid">
<div className="modal-detail-card">
<h5>Nature de l'Incident</h5>
<wcs-badge
className={selectedItem.data.type_perturbation === 'suppression' ? 'wcs-danger' : 'wcs-warning'}
shape="rounded"
>
{selectedItem.data.type_perturbation === 'retard' ? `Retard +${selectedItem.data.duree_retard_minutes || 0} min` :
selectedItem.data.type_perturbation === 'suppression' ? 'Circulation Supprimée' : 'Modification d\'itinéraire'}
</wcs-badge>
<div style={{ marginTop: '10px', fontSize: '13px' }}>
Statut : <strong>{selectedItem.data.actif ? 'Incident Actif en cours' : 'Incident Résolu'}</strong>
</div>
</div>
<div className="modal-detail-card">
<h5>Circulation Affectée</h5>
<div style={{ fontSize: '13px' }}>
Train N° <strong>{selectedItem.data.horaire?.numero_train || selectedItem.data.numero_train}</strong>
</div>
<div style={{ fontSize: '13px', color: '#64748b' }}>
Trajet : {selectedItem.data.horaire?.gare_depart_nom} → {selectedItem.data.horaire?.gare_arrivee_nom}
</div>
</div>
</div>
<div className="modal-detail-card mt-2">
<h5>Cause & Diagnostic d'Exploitation</h5>
<p style={{ margin: 0, fontSize: '14px', color: '#1e293b' }}>
{selectedItem.data.cause || 'Aucun motif d\'exploitation renseigné dans le système.'}
</p>
</div>
</div>
)}
</div>
)}
<div slot="actions">
<wcs-button
mode="stroked"
className="wcs-secondary"
onClick={() => setSelectedItem(null)}
>
Fermer
</wcs-button>
{selectedItem?.type === 'perturbation' ? (
<wcs-button
className="wcs-primary"
onClick={() => navigate(`/perturbations/modifier/${selectedItem.data.id}`)}
>
Modifier la perturbation
</wcs-button>
) : (
<wcs-button
className="wcs-primary"
onClick={() => navigate('/horaires')}
>
Gérer les horaires
</wcs-button>
)}
</div>
</wcs-modal>
{/* ====================================================================
8. PIED DE PAGE CORPORATE SNCF (WCS-FOOTER)
==================================================================== */}
<wcs-footer className="sncf-dashboard-footer mt-5">
<div className="d-flex align-items-center gap-3">
<div className="footer-sncf-badge">
<wcs-mat-icon icon="train" style={{ fontSize: '20px', color: '#ffffff' }}></wcs-mat-icon>
</div>
<div>
<strong style={{ color: '#ffffff', fontSize: '14px', display: 'block' }}>
FERROVIA PANEL · DIRECTION DE L'EXPLOITATION FERROVIAIRE
</strong>
<span style={{ color: 'rgba(255, 255, 255, 0.75)', fontSize: '12px' }}>
Système certifié WCS SNCF (Web Component System) & Bootstrap SNCF Métiers
</span>
</div>
</div>
<div slot="end-left">
<span style={{ fontSize: '12px', color: 'rgba(255, 255, 255, 0.65)' }}>
© {new Date().getFullYear()} SNCF · Tous droits réservés · Supervision Réseau & Information Voyageurs
</span>
</div>
<div slot="end-right">
<div className="d-flex align-items-center gap-2">
<wcs-badge className="wcs-success" shape="rounded">
<wcs-mat-icon icon="cloud_done" class="mr-1"></wcs-mat-icon>
Supabase Connecté
</wcs-badge>
<span style={{ fontSize: '12px', color: 'rgba(255, 255, 255, 0.7)' }}>
v2.5.0-WCS
</span>
</div>
</div>
</wcs-footer>
</div>
</div>
);
}