diff --git a/public/type/data.json b/public/type/data.json
index 3bb7a4b..244b66d 100644
--- a/public/type/data.json
+++ b/public/type/data.json
@@ -3,133 +3,133 @@
"logos": [
{
"file": "logo-aleop.svg",
- "path": "/img/type/logo-aleop.svg",
+ "path": "/logo-aleop.svg",
"slug": "aleop",
"name": "Aleop"
},
{
"file": "logo-auvergerhonealpes.svg",
- "path": "/img/type/logo-auvergerhonealpes.svg",
+ "path": "/logo-auvergerhonealpes.svg",
"slug": "auvergerhonealpes",
"name": "Auvergerhonealpes"
},
{
"file": "logo-breizhgo.svg",
- "path": "/img/type/logo-breizhgo.svg",
+ "path": "/logo-breizhgo.svg",,
"slug": "breizhgo",
"name": "Breizhgo"
},
{
"file": "logo-cfl.svg",
- "path": "/img/type/logo-cfl.svg",
+ "path": "/logo-cfl.svg",
"slug": "cfl",
"name": "CFL"
},
{
"file": "logo-db.svg",
- "path": "/img/type/logo-db.svg",
+ "path": "/logo-db.svg",
"slug": "db",
"name": "DB"
},
{
"file": "logo-eurostar.svg",
- "path": "/img/type/logo-eurostar.svg",
+ "path": "/logo-eurostar.svg",
"slug": "eurostar",
"name": "Eurostar"
},
{
"file": "logo-fluo.svg",
- "path": "/img/type/logo-fluo.svg",
+ "path": "/logo-fluo.svg",
"slug": "fluo",
"name": "Fluo"
},
{
"file": "logo-frecciarossa.svg",
- "path": "/img/type/logo-frecciarossa.svg",
+ "path": "/logo-frecciarossa.svg",
"slug": "frecciarossa",
"name": "Frecciarossa"
},
{
"file": "logo-hautsdefrance.svg",
- "path": "/img/type/logo-hautsdefrance.svg",
+ "path": "/logo-hautsdefrance.svg",
"slug": "hautsdefrance",
"name": "Hautsdefrance"
},
{
"file": "logo-ice.svg",
- "path": "/img/type/logo-ice.svg",
+ "path": "/logo-ice.svg",
"slug": "ice",
"name": "ICE"
},
{
"file": "logo-intercites.svg",
- "path": "/img/type/logo-intercites.svg",
+ "path": "/logo-intercites.svg",
"slug": "intercites",
"name": "Intercités"
},
{
"file": "logo-ler.svg",
- "path": "/img/type/logo-ler.svg",
+ "path": "/logo-ler.svg",
"slug": "ler",
"name": "Ler"
},
{
"file": "logo-lio.svg",
- "path": "/img/type/logo-lio.svg",
+ "path": "/logo-lio.svg",
"slug": "lio",
"name": "Lio"
},
{
"file": "logo-lunea.svg",
- "path": "/img/type/logo-lunea.svg",
+ "path": "/logo-lunea.svg",
"slug": "lunea",
"name": "Lunea"
},
{
"file": "logo-metrolor.svg",
- "path": "/img/type/logo-metrolor.svg",
+ "path": "/logo-metrolor.svg",
"slug": "metrolor",
"name": "Metrolor"
},
{
"file": "logo-mobigo.svg",
- "path": "/img/type/logo-mobigo.svg",
+ "path": "/logo-mobigo.svg",
"slug": "mobigo",
"name": "Mobigo"
},
{
"file": "logo-nomad.svg",
- "path": "/img/type/logo-nomad.svg",
+ "path": "/logo-nomad.svg",
"slug": "nomad",
"name": "Nomad"
},
{
"file": "logo-occitanie.svg",
- "path": "/img/type/logo-occitanie.svg",
+ "path": "/logo-occitanie.svg",
"slug": "occitanie",
"name": "Occitanie"
},
{
"file": "logo-ouigo.svg",
- "path": "/img/type/logo-ouigo.svg",
+ "path": "/logo-ouigo.svg",
"slug": "ouigo",
"name": "TGV Ouigo"
},
{
"file": "logo-ouigo-classique.svg",
- "path": "/img/type/logo-ouigo-classique.svg",
+ "path": "/logo-ouigo-classique.svg",
"slug": "ouigo-classique",
"name": "OUIGO Trains Classiques"
},
{
"file": "logo-remi.svg",
- "path": "/img/type/logo-remi.svg",
+ "path": "/logo-remi.svg",
"slug": "remi",
"name": "Remi"
},
{
"file": "logo-renfeave.svg",
- "path": "/img/type/logo-renfeave.svg",
+ "path": "/logo-renfeave.svg",
"slug": "renfeave",
"name": "Renfe AVE"
},
@@ -255,8 +255,8 @@
},
{
"file": "sncf-voyageurs-logo.png",
- "path": "/img/type/sncf-voyageurs-logo.png",
- "slug": "ter",
+ "path": "/sncf-voyageurs-logo.png",
+ "slug": "TER",
"name": "TER"
},
{
diff --git a/public/type/quais/data-color.json b/public/type/quais/data-color.json
index 766a30d..1134e06 100644
--- a/public/type/quais/data-color.json
+++ b/public/type/quais/data-color.json
@@ -3,379 +3,379 @@
"logos": [
{
"file": "logo-aleop-color.svg",
- "path": "/img/type/logo-aleop-color.svg",
+ "path": "/type/quais/logo-aleop-color.svg",
"slug": "aleop",
"name": "Aleop"
},
{
"file": "logo-auvergerhonealpes-color.svg",
- "path": "/img/type/logo-auvergerhonealpes-color.svg",
+ "path": "/type/quais/logo-auvergerhonealpes-color.svg",
"slug": "auvergerhonealpes",
"name": "Auvergerhonealpes"
},
{
"file": "logo-breizhgo-color.svg",
- "path": "/img/type/logo-breizhgo-color.svg",
+ "path": "/type/quais/logo-breizhgo-color.svg",
"slug": "breizhgo",
"name": "Breizhgo"
},
{
"file": "logo-cfl-color.svg",
- "path": "/img/type/logo-cfl-color.svg",
+ "path": "/type/quais/logo-cfl-color.svg",
"slug": "cfl",
"name": "CFL"
},
{
"file": "logo-db-color.svg",
- "path": "/img/type/logo-db-color.svg",
+ "path": "/type/quais/logo-db-color.svg",
"slug": "db",
"name": "DB"
},
{
"file": "logo-eurostar-color.svg",
- "path": "/img/type/logo-eurostar-color.svg",
+ "path": "/type/quais/logo-eurostar-color.svg",
"slug": "eurostar",
"name": "Eurostar"
},
{
"file": "logo-fluo-color.svg",
- "path": "/img/type/logo-fluo-color.svg",
+ "path": "/type/quais/logo-fluo-color.svg",
"slug": "fluo",
"name": "Fluo"
},
{
"file": "logo-frecciarossa-color.svg",
- "path": "/img/type/logo-frecciarossa-color.svg",
+ "path": "/type/quais/logo-frecciarossa-color.svg",
"slug": "frecciarossa",
"name": "Frecciarossa"
},
{
"file": "logo-hautsdefrance-color.svg",
- "path": "/img/type/logo-hautsdefrance-color.svg",
+ "path": "/type/quais/logo-hautsdefrance-color.svg",
"slug": "hautsdefrance",
"name": "Hautsdefrance"
},
{
"file": "logo-ice-color.svg",
- "path": "/img/type/logo-ice-color.svg",
+ "path": "/type/quais/logo-ice-color.svg",
"slug": "ice",
"name": "ICE"
},
{
"file": "logo-intercites-color.svg",
- "path": "/img/type/logo-intercites-color.svg",
+ "path": "/type/quais/logo-intercites-color.svg",
"slug": "intercites",
"name": "Intercités"
},
{
"file": "logo-ler-color.svg",
- "path": "/img/type/logo-ler-color.svg",
+ "path": "/type/quais/logo-ler-color.svg",
"slug": "ler",
"name": "Ler"
},
{
"file": "logo-lio-color.svg",
- "path": "/img/type/logo-lio-color.svg",
+ "path": "/type/quais/logo-lio-color.svg",
"slug": "lio",
"name": "Lio"
},
{
"file": "logo-lunea-color.svg",
- "path": "/img/type/logo-lunea-color.svg",
+ "path": "/type/quais/logo-lunea-color.svg",
"slug": "lunea",
"name": "Lunea"
},
{
"file": "logo-metrolor-color.svg",
- "path": "/img/type/logo-metrolor-color.svg",
+ "path": "/type/quais/logo-metrolor-color.svg",
"slug": "metrolor",
"name": "Metrolor"
},
{
"file": "logo-mobigo-color.svg",
- "path": "/img/type/logo-mobigo-color.svg",
+ "path": "/type/quais/logo-mobigo-color.svg",
"slug": "mobigo",
"name": "Mobigo"
},
{
"file": "logo-nomad-color.svg",
- "path": "/img/type/logo-nomad-color.svg",
+ "path": "/type/quais/logo-nomad-color.svg",
"slug": "nomad",
"name": "Nomad"
},
{
"file": "logo-occitanie-color.svg",
- "path": "/img/type/logo-occitanie-color.svg",
+ "path": "/type/quais/logo-occitanie-color.svg",
"slug": "occitanie",
"name": "Occitanie"
},
{
"file": "logo-ouigo-color.svg",
- "path": "/img/type/logo-ouigo-color.svg",
+ "path": "/type/quais/logo-ouigo-color.svg",
"slug": "ouigo",
"name": "OUIGO"
},
{
"file": "logo-ouigo-classique-color.svg",
- "path": "/img/type/logo-ouigo-classique-color.svg",
+ "path": "/type/quais/logo-ouigo-classique-color.svg",
"slug": "ouigo-classique",
"name": "OUIGO Trains Classiques"
},
{
"file": "logo-remi-color.svg",
- "path": "/img/type/logo-remi-color.svg",
+ "path": "/type/quais/logo-remi-color.svg",
"slug": "remi",
"name": "Remi"
},
{
"file": "logo-renfeave-color.svg",
- "path": "/img/type/logo-renfeave-color.svg",
+ "path": "/type/quais/logo-renfeave-color.svg",
"slug": "renfeave",
"name": "Renfe AVE"
},
{
"file": "logo-rer-a-color.svg",
- "path": "/img/type/logo-rer-a-color.svg",
+ "path": "/type/quais/logo-rer-a-color.svg",
"slug": "rer-a",
"name": "RER A"
},
{
"file": "logo-rer-b-color.svg",
- "path": "/img/type/logo-rer-b-color.svg",
+ "path": "/type/quais/logo-rer-b-color.svg",
"slug": "rer-b",
"name": "RER B"
},
{
"file": "logo-rer-c-color.svg",
- "path": "/img/type/logo-rer-c-color.svg",
+ "path": "/type/quais/logo-rer-c-color.svg",
"slug": "rer-c",
"name": "RER C"
},
{
"file": "logo-rer-d-color.svg",
- "path": "/img/type/logo-rer-d-color.svg",
+ "path": "/type/quais/logo-rer-d-color.svg",
"slug": "rer-d",
"name": "RER D"
},
{
"file": "logo-rer-e-color.svg",
- "path": "/img/type/logo-rer-e-color.svg",
+ "path": "/type/quais/logo-rer-e-color.svg",
"slug": "rer-e",
"name": "RER E"
},
{
"file": "logo-sbb-color.svg",
- "path": "/img/type/logo-sbb-color.svg",
+ "path": "/type/quais/logo-sbb-color.svg",
"slug": "sbb",
"name": "SBB"
},
{
"file": "logo-sncb-color.svg",
- "path": "/img/type/logo-sncb-color.svg",
+ "path": "/type/quais/logo-sncb-color.svg",
"slug": "sncb",
"name": "SNCB"
},
{
"file": "logo-sncf-color.svg",
- "path": "/img/type/logo-sncf-color.svg",
+ "path": "/type/quais/logo-sncf-color.svg",
"slug": "sncf",
"name": "Sncf"
},
{
"file": "sncf-voyageurs-logo-color.png",
- "path": "/img/type/sncf-voyageurs-logo-color.png",
+ "path": "/type/quais/img/type/sncf-voyageurs-logo-color.png",
"slug": "sncf-voyageurs-logo",
"name": "SNCF"
},
{
"file": "logo-sncf1985.svg",
- "path": "/img/type/logo-sncf1985.svg",
+ "path": "/type/quais/img/type/logo-sncf1985.svg",
"slug": "sncf1985",
"name": "Sncf1985"
},
{
"file": "logo-sncf1992.svg",
- "path": "/img/type/logo-sncf1992.svg",
+ "path": "/type/quais/img/type/logo-sncf1992.svg",
"slug": "sncf1992",
"name": "Sncf1992"
},
{
"file": "logo-t-h.svg",
- "path": "/img/type/logo-t-h.svg",
+ "path": "/type/quais/img/type/logo-t-h.svg",
"slug": "t-h",
"name": "T H"
},
{
"file": "logo-t-j.svg",
- "path": "/img/type/logo-t-j.svg",
+ "path": "/type/quais/img/type/logo-t-j.svg",
"slug": "t-j",
"name": "T J"
},
{
"file": "logo-t-k.svg",
- "path": "/img/type/logo-t-k.svg",
+ "path": "/type/quais/img/type/logo-t-k.svg",
"slug": "t-k",
"name": "T K"
},
{
"file": "logo-t-l.svg",
- "path": "/img/type/logo-t-l.svg",
+ "path": "/type/quais/img/type/logo-t-l.svg",
"slug": "t-l",
"name": "T L"
},
{
"file": "logo-t-n.svg",
- "path": "/img/type/logo-t-n.svg",
+ "path": "/type/quais/img/type/logo-t-n.svg",
"slug": "t-n",
"name": "T N"
},
{
"file": "logo-t-p.svg",
- "path": "/img/type/logo-t-p.svg",
+ "path": "/type/quais/img/type/logo-t-p.svg",
"slug": "t-p",
"name": "T P"
},
{
"file": "logo-t-r.svg",
- "path": "/img/type/logo-t-r.svg",
+ "path": "/type/quais/img/type/logo-t-r.svg",
"slug": "t-r",
"name": "T R"
},
{
"file": "logo-t-u.svg",
- "path": "/img/type/logo-t-u.svg",
+ "path": "/type/quais/img/type/logo-t-u.svg",
"slug": "t-u",
"name": "T U"
},
{
"file": "logo-teoz.svg",
- "path": "/img/type/logo-teoz.svg",
+ "path": "/type/quais/img/type/logo-teoz.svg",
"slug": "teoz",
"name": "Teoz"
},
{
"file": "sncf-voyageurs-logo.png",
- "path": "/img/type/sncf-voyageurs-logo.png",
+ "path": "/type/quais/sncf-voyageurs-logo.png",
"slug": "ter",
"name": "TER"
},
{
"file": "logo-ter-rhone-alpes.svg",
- "path": "/img/type/logo-ter-rhone-alpes.svg",
+ "path": "/type/quais/logo-ter-rhone-alpes.svg",
"slug": "ter-rhone-alpes",
"name": "TER Rhone Alpes"
},
{
"file": "logo-teralsace.svg",
- "path": "/img/type/logo-teralsace.svg",
+ "path": "/type/quais/logo-teralsace.svg",
"slug": "teralsace",
"name": "Teralsace"
},
{
"file": "logo-teraquitaine.svg",
- "path": "/img/type/logo-teraquitaine.svg",
+ "path": "/type/quais/logo-teraquitaine.svg",
"slug": "teraquitaine",
"name": "Teraquitaine"
},
{
"file": "logo-terbassenormandie.svg",
- "path": "/img/type/logo-terbassenormandie.svg",
+ "path": "/type/quais/logo-terbassenormandie.svg",
"slug": "terbassenormandie",
"name": "Terbassenormandie"
},
{
"file": "logo-terbourgogne.svg",
- "path": "/img/type/logo-terbourgogne.svg",
+ "path": "/type/quais/logo-terbourgogne.svg",
"slug": "terbourgogne",
"name": "Terbourgogne"
},
{
"file": "logo-terbretagne.svg",
- "path": "/img/type/logo-terbretagne.svg",
+ "path": "/type/quais/logo-terbretagne.svg",
"slug": "terbretagne",
"name": "Terbretagne"
},
{
"file": "logo-tercentre.svg",
- "path": "/img/type/logo-tercentre.svg",
+ "path": "/type/quais/logo-tercentre.svg",
"slug": "tercentre",
"name": "Tercentre"
},
{
"file": "logo-terlanguedocroussillon.svg",
- "path": "/img/type/logo-terlanguedocroussillon.svg",
+ "path": "/type/quais/logo-terlanguedocroussillon.svg",
"slug": "terlanguedocroussillon",
"name": "Terlanguedocroussillon"
},
{
"file": "logo-termidipyrenees.svg",
- "path": "/img/type/logo-termidipyrenees.svg",
+ "path": "/type/quais/logo-termidipyrenees.svg",
"slug": "termidipyrenees",
"name": "Termidipyrenees"
},
{
"file": "logo-ternordpasdecalais.svg",
- "path": "/img/type/logo-ternordpasdecalais.svg",
+ "path": "/type/quais/logo-ternordpasdecalais.svg",
"slug": "ternordpasdecalais",
"name": "Ternordpasdecalais"
},
{
"file": "logo-terpoitoucharentes.svg",
- "path": "/img/type/logo-terpoitoucharentes.svg",
+ "path": "/type/quais/logo-terpoitoucharentes.svg",
"slug": "terpoitoucharentes",
"name": "Terpoitoucharentes"
},
{
"file": "logo-tgv.svg",
- "path": "/img/type/logo-tgv.svg",
+ "path": "/type/quais/logo-tgv.svg",
"slug": "tgv",
"name": "TGV"
},
{
"file": "logo-inoui.svg",
- "path": "/img/type/logo-inoui.svg",
+ "path": "/type/quais/logo-inoui.svg",
"slug": "inoui",
"name": "TGV InOui"
},
{
"file": "logo-lyria.svg",
- "path": "/img/type/logo-lyria.svg",
+ "path": "/type/quais/logo-lyria.svg",
"slug": "lyria",
"name": "TGV Lyria"
},
{
"file": "logo-thalys.svg",
- "path": "/img/type/logo-thalys.svg",
+ "path": "/type/quais/logo-thalys.svg",
"slug": "thalys",
"name": "Thalys"
},
{
"file": "logo-thello.svg",
- "path": "/img/type/logo-thello.svg",
+ "path": "/type/quais/logo-thello.svg",
"slug": "thello",
"name": "Thello"
},
{
"file": "track-car.svg",
- "path": "/img/type/track-car.svg",
+ "path": "/type/quais/track-car.svg",
"slug": "track-car",
"name": "Track Car"
},
{
"file": "logo-tramtrain.svg",
- "path": "/img/type/logo-tramtrain.svg",
+ "path": "/type/quais/logo-tramtrain.svg",
"slug": "tramtrain",
"name": "Tramtrain"
},
{
"file": "logo-trenitalia.svg",
- "path": "/img/type/logo-trenitalia.svg",
+ "path": "/type/quais/logo-trenitalia.svg",
"slug": "trenitalia",
"name": "Trenitalia"
},
{
"file": "logo-zou.svg",
- "path": "/img/type/logo-zou.svg",
+ "path": "/type/quais/logo-zou.svg",
"slug": "zou",
"name": "Zou"
}
diff --git a/public/type/quais/sncf-voyageurs-logo.png b/public/type/quais/sncf-voyageurs-logo.png
new file mode 100644
index 0000000..88d4d34
Binary files /dev/null and b/public/type/quais/sncf-voyageurs-logo.png differ
diff --git a/src/App.js b/src/App.js
index f8796dd..f99cf66 100644
--- a/src/App.js
+++ b/src/App.js
@@ -9,6 +9,8 @@ import ClassiquesDeparts from './pages/afficheurs/classiques/ClassiquesDeparts';
import ClassiquesArrivees from './pages/afficheurs/classiques/ClassiquesArrivees';
import ClassiquesDepartsQuais from './pages/afficheurs/classiques/quais/ClassiquesDepartsQuais';
import ClassiquesArriveesQuais from './pages/afficheurs/classiques/quais/ClassiquesArriveesQuais';
+import ClassiquesFlashInfo from './pages/afficheurs/classiques/ClassiquesFlashInfo';
+import ClassiquesInformations from './pages/afficheurs/classiques/ClassiquesInformations';
import AFLDeparts from './pages/afficheurs/afl/AFLDeparts';
import AFLArrivees from './pages/afficheurs/afl/AFLArrivees';
import AFLDepartsQuais from './pages/afficheurs/afl/AFLDepartsQuais';
@@ -198,6 +200,22 @@ function App() {
}
// Routes pour afficheurs : supporte le mode racine ET le mode /ferrovia
+ if (
+ cleanRoute.startsWith('/afficheurs/classiques/flash-info') ||
+ cleanRoute.startsWith('/afficheurs/classiques/flash-circulation') ||
+ cleanRoute.startsWith('/afficheurs/departs/classiques/flash-info') ||
+ cleanRoute.startsWith('/afficheurs/arrivees/classiques/flash-info')
+ ) {
+ return ;
+ }
+ if (
+ cleanRoute.startsWith('/afficheurs/classiques/informations') ||
+ cleanRoute.startsWith('/afficheurs/classiques/infos') ||
+ cleanRoute.startsWith('/afficheurs/departs/classiques/informations') ||
+ cleanRoute.startsWith('/afficheurs/arrivees/classiques/informations')
+ ) {
+ return ;
+ }
if (cleanRoute.startsWith('/afficheurs/departs/classiques/quais')) {
return ;
}
diff --git a/src/components/afficheurs/AttentifsEnsemble.css b/src/components/afficheurs/AttentifsEnsemble.css
new file mode 100644
index 0000000..194c24f
--- /dev/null
+++ b/src/components/afficheurs/AttentifsEnsemble.css
@@ -0,0 +1,202 @@
+.aff-attentifs__container {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ user-select: none;
+ font-family: 'Achemine', 'Arial', sans-serif;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+/* COLONNE GAUCHE (36%) */
+.aff-attentifs__left-col {
+ flex: 0 0 36%;
+ background-color: #313131;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: 70px 50px;
+ box-sizing: border-box;
+}
+
+.aff-attentifs__title-group {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.aff-attentifs__title-word {
+ font-size: 78px;
+ font-weight: 900;
+ color: #fdb913;
+ letter-spacing: 2px;
+ line-height: 1.05;
+ text-transform: uppercase;
+}
+
+.aff-attentifs__vigipirate-wrap {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ margin-top: auto;
+}
+
+.aff-attentifs__vigipirate-svg {
+ width: 270px;
+ height: auto;
+ filter: drop-shadow(0 8px 18px rgba(0, 0, 0, 0.45));
+}
+
+/* COLONNE DROITE (64%) */
+.aff-attentifs__right-col {
+ flex: 1;
+ background-color: #4a4a4a;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ padding: 60px 80px;
+ box-sizing: border-box;
+ gap: 40px;
+}
+
+.aff-attentifs__info-section {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ color: #ffffff;
+}
+
+.aff-attentifs__info-desc {
+ font-size: 27px;
+ font-weight: 500;
+ line-height: 1.4;
+ margin: 0 0 36px 0;
+ color: #f8f8f8;
+}
+
+.aff-attentifs__call-action {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 30px;
+}
+
+.aff-attentifs__call-text {
+ font-size: 44px;
+ font-weight: 900;
+ letter-spacing: 0.5px;
+ color: #ffffff;
+}
+
+.aff-attentifs__call-hours {
+ font-size: 30px;
+ font-weight: 600;
+ color: #eaeaea;
+}
+
+.aff-attentifs__call-footnote {
+ font-size: 19px;
+ font-weight: 400;
+ color: #d0d0d0;
+}
+
+/* SECTION BULLE D'ALERTE 3117 */
+.aff-attentifs__bubble-section {
+ flex: 0 0 460px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.aff-attentifs__bubble-wrap {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.aff-attentifs__bubble-card {
+ position: relative;
+ background-color: #ffffff;
+ border: 7px solid #e2001a;
+ border-radius: 36px;
+ padding: 30px 42px;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
+ min-width: 360px;
+}
+
+.aff-attentifs__bubble-title {
+ font-size: 28px;
+ font-weight: 900;
+ color: #e2001a;
+ letter-spacing: 1px;
+}
+
+.aff-attentifs__bubble-rows {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.aff-attentifs__bubble-line {
+ display: flex;
+ align-items: baseline;
+ gap: 14px;
+}
+
+.aff-attentifs__bubble-label {
+ font-size: 30px;
+ font-weight: 800;
+ color: #e2001a;
+ min-width: 70px;
+}
+
+.aff-attentifs__bubble-number {
+ font-size: 58px;
+ font-weight: 900;
+ color: #e2001a;
+ letter-spacing: -1px;
+}
+
+.aff-attentifs__bubble-tail {
+ position: absolute;
+ bottom: -32px;
+ left: 54px;
+ width: 0;
+ height: 0;
+ border-left: 20px solid transparent;
+ border-right: 20px solid transparent;
+ border-top: 32px solid #e2001a;
+}
+
+.aff-attentifs__bubble-tail::after {
+ content: '';
+ position: absolute;
+ top: -38px;
+ left: -13px;
+ width: 0;
+ height: 0;
+ border-left: 13px solid transparent;
+ border-right: 13px solid transparent;
+ border-top: 24px solid #ffffff;
+}
+
+/* SMARTPHONE BADGE */
+.aff-attentifs__phone-badge {
+ position: absolute;
+ top: -34px;
+ right: -36px;
+ z-index: 10;
+ filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.4));
+}
+
+.aff-attentifs__phone-svg {
+ width: 86px;
+ height: auto;
+}
diff --git a/src/components/afficheurs/AttentifsEnsemble.js b/src/components/afficheurs/AttentifsEnsemble.js
new file mode 100644
index 0000000..dda1f19
--- /dev/null
+++ b/src/components/afficheurs/AttentifsEnsemble.js
@@ -0,0 +1,112 @@
+import React from 'react';
+import './AttentifsEnsemble.css';
+
+export default function AttentifsEnsemble() {
+ return (
+
+ {/* COLONNE GAUCHE : ANTHRACITE FONCÉ (ATTENTIFS ENSEMBLE + VIGIPIRATE) */}
+
+
+
+ {/* Insigne Vigipirate Officiel */}
+
+
+ {/* Triangle noir extérieur */}
+
+ {/* Triangle rouge intérieur */}
+
+ {/* Bandeau blanc en bas */}
+
+ {/* Texte VIGIPIRATE */}
+
+ VIGIPIRATE
+
+
+
+
+
+ {/* COLONNE DROITE : GRIS MOYEN (CONSIGNES + BULLE ALERTE 3117) */}
+
+ {/* Partie gauche des consignes */}
+
+
+ Si vous êtes témoin
+ d'une situation
+ qui présente un risque
+ pour votre sécurité
+ ou celle des autres
+ voyageurs,
+
+
+
+ APPELER* LE 3117
+ 24h/24 · 7j/7
+
+
+
+ * Appel non surtaxé et enregistré
+
+
+
+ {/* Partie droite avec la bulle Alerte SNCF 3117 et le smartphone */}
+
+
+ {/* Bulle blanche à bordure rouge */}
+
+
ALERTE SNCF
+
+
+ TÉL.
+ 3117*
+
+
+ SMS
+ 31177**
+
+
+
+ {/* Pointe de la bulle */}
+
+
+
+ {/* Smartphone Rouge d'alerte */}
+
+
+ {/* Corps de téléphone rouge arrondi */}
+
+ {/* Écran blanc */}
+
+ {/* Point d'exclamation rouge d'urgence */}
+
+
+ {/* Touches du pavé numérique */}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/afficheurs/DepartEnCoursAFL.css b/src/components/afficheurs/DepartEnCoursAFL.css
new file mode 100644
index 0000000..d2db5ed
--- /dev/null
+++ b/src/components/afficheurs/DepartEnCoursAFL.css
@@ -0,0 +1,90 @@
+.aff-dep-afl__container {
+ width: 100%;
+ height: 100%;
+ background-color: #d61e38;
+ color: #ffffff;
+ display: flex;
+ flex-direction: column;
+ user-select: none;
+ font-family: 'Achemine', 'Arial', sans-serif;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+/* FRISSE SUPÉRIEURE RAYÉE */
+.aff-dep-afl__hazard-stripes {
+ width: 100%;
+ height: 48px;
+ background: repeating-linear-gradient(
+ -45deg,
+ #ffffff,
+ #ffffff 28px,
+ #d61e38 28px,
+ #d61e38 56px
+ );
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
+ flex-shrink: 0;
+}
+
+/* CONTENU PRINCIPAL */
+.aff-dep-afl__content {
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ padding: 40px 100px;
+ gap: 120px;
+ box-sizing: border-box;
+}
+
+.aff-dep-afl__icon-col {
+ flex: 0 0 460px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.aff-dep-afl__no-entry-svg {
+ width: 100%;
+ max-width: 440px;
+ height: auto;
+ filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.35));
+}
+
+.aff-dep-afl__text-col {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 48px;
+}
+
+.aff-dep-afl__heading-group {
+ display: flex;
+ flex-direction: column;
+ line-height: 1.02;
+}
+
+.aff-dep-afl__title-main,
+.aff-dep-afl__title-sub {
+ font-size: 102px;
+ font-weight: 900;
+ letter-spacing: 2px;
+ color: #ffffff;
+ text-transform: uppercase;
+}
+
+.aff-dep-afl__instructions {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.aff-dep-afl__instruction-line {
+ font-size: 48px;
+ font-weight: 600;
+ line-height: 1.35;
+ color: #ffffff;
+ margin: 0;
+}
diff --git a/src/components/afficheurs/DepartEnCoursAFL.js b/src/components/afficheurs/DepartEnCoursAFL.js
new file mode 100644
index 0000000..148263b
--- /dev/null
+++ b/src/components/afficheurs/DepartEnCoursAFL.js
@@ -0,0 +1,63 @@
+import React from 'react';
+import './DepartEnCoursAFL.css';
+
+export default function DepartEnCoursAFL() {
+ return (
+
+ {/* RUBAN SUPÉRIEUR RAYÉ BLANC ET ROUGE */}
+
+
+ {/* CONTENU PRINCIPAL */}
+
+ {/* Panneau d'interdiction blanc à gauche */}
+
+
+ {/* Cercle blanc extérieur */}
+
+
+ {/* Silhouette du piéton marchant avec valise */}
+
+ {/* Tête */}
+
+ {/* Corps marchant */}
+
+ {/* Jambe droite enjambant */}
+
+ {/* Bras gauche */}
+
+ {/* Bras droit tenant la valise */}
+
+ {/* Valise inclinée */}
+
+
+
+
+
+
+ {/* Barre diagonale blanche d'interdiction */}
+
+
+
+
+ {/* Textes d'alerte et de consigne à droite */}
+
+
+
+
+
Pour votre sécurité
+
merci de vous éloigner
+
de la bordure du quai.
+
+
+
+
+ );
+}
diff --git a/src/components/afficheurs/DepartEnCoursClassique.css b/src/components/afficheurs/DepartEnCoursClassique.css
new file mode 100644
index 0000000..ecf9c77
--- /dev/null
+++ b/src/components/afficheurs/DepartEnCoursClassique.css
@@ -0,0 +1,75 @@
+.aff-dep-classique__container {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ user-select: none;
+ font-family: 'Achemine', 'Arial', sans-serif;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+/* ZONE DU HAUT : GRIS FONCÉ (~72%) */
+.aff-dep-classique__top {
+ flex: 1;
+ background-color: #222226;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ gap: 160px;
+ padding: 40px 100px;
+ box-sizing: border-box;
+}
+
+.aff-dep-classique__prohibition-sign {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.aff-dep-classique__no-entry-svg {
+ width: 480px;
+ height: 480px;
+ filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
+}
+
+.aff-dep-classique__train-wrap {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.aff-dep-classique__train-gif {
+ width: 480px;
+ height: auto;
+ max-height: 480px;
+ object-fit: contain;
+ filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
+}
+
+.aff-dep-classique__train-svg {
+ width: 460px;
+ height: 460px;
+ filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
+}
+
+/* BANDEAU ROUGE INFÉRIEUR (~28%) */
+.aff-dep-classique__bottom {
+ flex: 0 0 280px;
+ background-color: #d0102a;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
+}
+
+.aff-dep-classique__banner-text {
+ font-size: 110px;
+ font-weight: 900;
+ color: #ffffff;
+ margin: 0;
+ letter-spacing: 1px;
+ text-align: center;
+}
diff --git a/src/components/afficheurs/DepartEnCoursClassique.js b/src/components/afficheurs/DepartEnCoursClassique.js
new file mode 100644
index 0000000..1fb8ef4
--- /dev/null
+++ b/src/components/afficheurs/DepartEnCoursClassique.js
@@ -0,0 +1,81 @@
+import React, { useState } from 'react';
+import './DepartEnCoursClassique.css';
+
+const GIF_URL = 'https://cdn.discordapp.com/attachments/1265325000290992258/1282018214720835725/SNCF_-_Depart_Train.gif?ex=6a95af01&is=6a945d81&hm=14570f1423217cb52437d767b3125a698ac5955fc088028fbc3d24d820e9a76b&';
+
+export default function DepartEnCoursClassique() {
+ const [gifError, setGifError] = useState(false);
+
+ return (
+
+ {/* ZONE SUPÉRIEURE : PICTOS (FOND GRIS FONCÉ) */}
+
+ {/* Panneau d'interdiction : Ne pas monter (voyageur avec valise barré) */}
+
+
+ {/* Fond blanc du cercle */}
+
+
+ {/* Silhouette du voyageur avec valise */}
+
+ {/* Tête */}
+
+ {/* Corps */}
+
+ {/* Jambe gauche */}
+
+ {/* Jambe droite */}
+
+ {/* Bras gauche */}
+
+ {/* Bras droit tenant la poignée */}
+
+ {/* Poignée télescopique de valise */}
+
+ {/* Valise à roulettes */}
+
+ {/* Roulettes */}
+
+
+
+
+ {/* Barre diagonale rouge d'interdiction */}
+
+
+
+
+ {/* Animation du train au départ (GIF ou SVG de secours) */}
+
+ {!gifError ? (
+
setGifError(true)}
+ />
+ ) : (
+
+ {/* Rails et traverses */}
+
+
+
+
+ {/* Face avant du train */}
+
+ {/* Pare-brise */}
+
+ {/* Phares rouges */}
+
+
+
+ )}
+
+
+
+ {/* BANDEAU INFÉRIEUR ROUGE : DÉPART EN COURS */}
+
+
Départ en cours
+
+
+ );
+}
diff --git a/src/components/afficheurs/GareVideoprotection.css b/src/components/afficheurs/GareVideoprotection.css
new file mode 100644
index 0000000..c9abc47
--- /dev/null
+++ b/src/components/afficheurs/GareVideoprotection.css
@@ -0,0 +1,78 @@
+.aff-videoprot__container {
+ width: 100%;
+ height: 100%;
+ background-color: #141416;
+ color: #ffffff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 40px 80px;
+ box-sizing: border-box;
+ font-family: 'Achemine', 'Arial', sans-serif;
+ user-select: none;
+}
+
+.aff-videoprot__wrapper {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ gap: 80px;
+ width: 100%;
+ max-width: 1600px;
+}
+
+.aff-videoprot__icon-col {
+ flex: 0 0 420px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.aff-videoprot__camera-svg {
+ width: 100%;
+ max-width: 420px;
+ height: auto;
+ filter: drop-shadow(0 6px 16px rgba(242, 91, 42, 0.25));
+}
+
+.aff-videoprot__text-col {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.aff-videoprot__title-vigi {
+ font-size: 52px;
+ font-weight: 800;
+ color: #f25b2a;
+ letter-spacing: 1.5px;
+ margin-bottom: 24px;
+ text-transform: uppercase;
+}
+
+.aff-videoprot__main-heading {
+ font-size: 46px;
+ font-weight: 800;
+ color: #ffffff;
+ line-height: 1.28;
+ letter-spacing: 0.5px;
+ margin: 0 0 54px 0;
+ text-transform: uppercase;
+}
+
+.aff-videoprot__subtext-group {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.aff-videoprot__legal-line,
+.aff-videoprot__access-line {
+ font-size: 26px;
+ font-weight: 600;
+ line-height: 1.45;
+ color: #f0f0f0;
+ margin: 0;
+}
diff --git a/src/components/afficheurs/GareVideoprotection.js b/src/components/afficheurs/GareVideoprotection.js
new file mode 100644
index 0000000..a3d0a2b
--- /dev/null
+++ b/src/components/afficheurs/GareVideoprotection.js
@@ -0,0 +1,68 @@
+import React from 'react';
+import './GareVideoprotection.css';
+
+export default function GareVideoprotection() {
+ return (
+
+
+ {/* Caméra de surveillance stylisée orange */}
+
+
+ {/* Support mural */}
+
+
+
+ {/* Corps de la caméra orienté vers le bas à droite */}
+
+ {/* Corps principal */}
+
+ {/* Parasoleil / Visière supérieure */}
+
+ {/* Cône avant / Objectif */}
+
+ {/* Fentes d'aération & détails noirs */}
+
+
+
+
+ {/* Bague optique */}
+
+
+
+
+
+ {/* Textes d'information et réglementaires */}
+
+
VIGI360
+
+
+ POUR VOTRE SÉCURITÉ, CET ESPACE
+ EST ÉQUIPÉ DE VIDÉOPROTECTION.
+
+
+
+
+ En application des dispositions des textes législatifs et
+ règlementaires en vigueur.
+
+
+ Pour l'exercice du droit d'accès au enregistrements,
+ s'adresser à un agent d'accueil de la gare.
+
+
+
+
+
+ );
+}
diff --git a/src/pages/SelectAfficheur.js b/src/pages/SelectAfficheur.js
index 80db783..3f9237b 100644
--- a/src/pages/SelectAfficheur.js
+++ b/src/pages/SelectAfficheur.js
@@ -408,6 +408,73 @@ export default function SelectAfficheur({ onBack, navigate }) {
🚉 Lancer l'Afficheur de Quai
+
+ {/* Options 3 & 4 pour le mode Classiques : Flash Info & Informations */}
+ {typeParam === 'classiques' && (
+ <>
+ {/* Option 3 : Flash Information */}
+
+
+
⚡
+
+ Flash Info Trafic
+
+
Flash Information
+
+ Écran dédié aux perturbations de circulation en temps réel et travaux de maintenance sur le réseau.
+
+
+
+ ⚡ Conforme au design historique Flash Circulation avec fond ambre et bandeau bleu nuit.
+
+
+
+
+
{
+ const kebab = gareParam ? String(gareParam).trim() : '';
+ navigate(`${prefix}/afficheurs/classiques/flash-info?gare=${encodeURIComponent(kebab)}`);
+ }}
+ className="launcher-btn"
+ style={{ background: '#00337f', color: '#fab23e', border: 'none', fontWeight: 700 }}
+ >
+ ⚡ Lancer le Flash Information
+
+
+
+ {/* Option 4 : Informations (Messages Dynamiques) */}
+
+
+
ℹ️
+
+ Messages Gare
+
+
Informations Voyageurs
+
+ Écran d'annonces et consignes dynamiques de la gare (Vigipirate, étiquetage des bagages, sécurité en gare).
+
+
+
+ ℹ️ Conforme au design historique Information avec messages voyageurs de la gare de {decodedGare}.
+
+
+
+
+
{
+ const kebab = gareParam ? String(gareParam).trim() : '';
+ navigate(`${prefix}/afficheurs/classiques/informations?gare=${encodeURIComponent(kebab)}`);
+ }}
+ className="launcher-btn"
+ style={{ background: '#00337f', color: '#ffffff', border: 'none', fontWeight: 700 }}
+ >
+ ℹ️ Lancer l'Afficheur Informations
+
+
+ >
+ )}
{/* Accès direct aux pages fixes AFL */}
diff --git a/src/pages/afficheurs/afl/AFLArriveesQuais.js b/src/pages/afficheurs/afl/AFLArriveesQuais.js
index 9b982ac..f54bd70 100644
--- a/src/pages/afficheurs/afl/AFLArriveesQuais.js
+++ b/src/pages/afficheurs/afl/AFLArriveesQuais.js
@@ -6,6 +6,10 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import Logo from '../../../components/Logo';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
+import GareVideoprotection from '../../../components/afficheurs/GareVideoprotection';
+import AttentifsEnsemble from '../../../components/afficheurs/AttentifsEnsemble';
+import AFLInformationsPage from './components/AFLInformationsPage';
+import AFLArrivees from './AFLArrivees';
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
const formatTimeNoSeconds = (v) => {
@@ -28,12 +32,16 @@ const AFLArriveesQuais = () => {
const [currentTime, setCurrentTime] = useState(new Date());
const [nextArrival, setNextArrival] = useState(null);
const [loading, setLoading] = useState(true);
+ const [idlePhase, setIdlePhase] = useState(0); // 0 = Attentifs (20s), 1 = Quai (20s), 2 = Information (10s)
const searchParams = new URLSearchParams(window.location.search);
const gareParam = searchParams.get('gare') || '';
const quaiParam = searchParams.get('quai') || searchParams.get('voie') || '';
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
+ const pageParam = (searchParams.get('page') || searchParams.get('mode') || '').toLowerCase();
+ const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true';
+
useAnnonceSonoreRealtime(null, 'AFLArriveesQuais', quaiParam);
useEffect(() => {
@@ -669,7 +677,9 @@ const AFLArriveesQuais = () => {
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
}
const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
- return quaiParam && platform.toLowerCase() === quaiParam.toLowerCase();
+ if (!quaiParam) return true;
+ const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
+ return cleanP(platform) === cleanP(quaiParam);
});
if (candidatesOnPlatform.length > 0) {
@@ -684,13 +694,44 @@ const AFLArriveesQuais = () => {
}
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, gareParam, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
+ // Alternance par défaut (20s Attentifs -> 20s Quai -> 10s Information) si aucun train n'est prévu sur le quai
+ useEffect(() => {
+ if (nextArrival) return;
+ let duration = 20000;
+ if (idlePhase === 2) duration = 10000;
+ else duration = 20000;
+
+ const timer = setTimeout(() => {
+ setIdlePhase(prev => (prev + 1) % 3);
+ }, duration);
+ return () => clearTimeout(timer);
+ }, [idlePhase, nextArrival]);
+
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, 15000);
return () => clearInterval(interval);
}, [fetchData]);
- if (loading) return
;
+ // AFFICHAGE 1 : Page Vidéoprotection (via query)
+ if (isVideoprotection) {
+ return ;
+ }
+
+ // AFFICHAGE 2 : Alternance si aucun train n'est attribué à ce quai
+ if (!nextArrival && gareParam) {
+ if (idlePhase === 0) {
+ return ;
+ }
+ if (idlePhase === 1) {
+ return ;
+ }
+ if (idlePhase === 2) {
+ return ;
+ }
+ }
+
+ if (loading && !nextArrival) return
;
if (!nextArrival) {
return (
diff --git a/src/pages/afficheurs/afl/AFLDepartsQuais.js b/src/pages/afficheurs/afl/AFLDepartsQuais.js
index dd7bb53..0ceb318 100644
--- a/src/pages/afficheurs/afl/AFLDepartsQuais.js
+++ b/src/pages/afficheurs/afl/AFLDepartsQuais.js
@@ -6,6 +6,11 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import Logo from '../../../components/Logo';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
+import GareVideoprotection from '../../../components/afficheurs/GareVideoprotection';
+import AttentifsEnsemble from '../../../components/afficheurs/AttentifsEnsemble';
+import DepartEnCoursAFL from '../../../components/afficheurs/DepartEnCoursAFL';
+import AFLInformationsPage from './components/AFLInformationsPage';
+import AFLDeparts from './AFLDeparts';
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
const formatTimeNoSeconds = (v) => {
@@ -28,6 +33,7 @@ const AFLDepartsQuais = () => {
const [currentTime, setCurrentTime] = useState(new Date());
const [nextDeparture, setNextDeparture] = useState(null);
const [loading, setLoading] = useState(true);
+ const [idlePhase, setIdlePhase] = useState(0); // 0 = Attentifs (20s), 1 = Quai (20s), 2 = Information (10s)
// Parse URL manually since we are not in a React Router context
const searchParams = new URLSearchParams(window.location.search);
@@ -36,6 +42,10 @@ const AFLDepartsQuais = () => {
const quaiParam = searchParams.get('quai') || searchParams.get('voie') || '';
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
+ const pageParam = (searchParams.get('page') || searchParams.get('mode') || '').toLowerCase();
+ const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true';
+ const isForceDepartEnCours = pageParam === 'depart_en_cours' || searchParams.get('depart_en_cours') === 'true' || searchParams.get('test_depart') === 'true';
+
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
useAnnonceSonoreRealtime(resolvedGareId, 'AFLDepartsQuais', quaiParam);
@@ -743,7 +753,8 @@ const AFLDepartsQuais = () => {
}
const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
if (!quaiParam) return true;
- return platform.toLowerCase() === quaiParam.toLowerCase();
+ const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
+ return cleanP(platform) === cleanP(quaiParam);
});
if (candidatesOnPlatform.length > 0) {
@@ -758,13 +769,66 @@ const AFLDepartsQuais = () => {
}
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, gareParam, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
+ // Alternance par défaut (20s Attentifs -> 20s Quai -> 10s Information) si aucun train n'est prévu sur le quai
+ useEffect(() => {
+ if (nextDeparture) return;
+ let duration = 20000;
+ if (idlePhase === 2) duration = 10000;
+ else duration = 20000;
+
+ const timer = setTimeout(() => {
+ setIdlePhase(prev => (prev + 1) % 3);
+ }, duration);
+ return () => clearTimeout(timer);
+ }, [idlePhase, nextDeparture]);
+
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, 15000);
return () => clearInterval(interval);
}, [fetchData]);
- if (loading) return
;
+ // 1. Détection "Départ en cours" (2 minutes avant l'heure de départ réelle)
+ let isDepartEnCours = isForceDepartEnCours;
+ if (!isDepartEnCours && nextDeparture) {
+ const rawTime = nextDeparture._rawTime || nextDeparture.heure_depart || nextDeparture.time;
+ const depHM = parseTimeHM(rawTime);
+ const delayMin = nextDeparture._status?.delayMinutes || nextDeparture.retard_minutes || 0;
+ if (depHM != null) {
+ const nowSec = currentTime.getHours() * 3600 + currentTime.getMinutes() * 60 + currentTime.getSeconds();
+ const depSec = (depHM + delayMin) * 60;
+ let diffSec = depSec - nowSec;
+ if (diffSec < -43200) diffSec += 86400;
+ if (diffSec <= 120 && diffSec >= 0) {
+ isDepartEnCours = true;
+ }
+ }
+ }
+
+ // AFFICHAGE 1 : Page Vidéoprotection (via query)
+ if (isVideoprotection) {
+ return ;
+ }
+
+ // AFFICHAGE 2 : Départ en cours (T - 2 min)
+ if (isDepartEnCours) {
+ return ;
+ }
+
+ // AFFICHAGE 3 : Alternance si aucun train n'est attribué à ce quai
+ if (!nextDeparture && gareParam) {
+ if (idlePhase === 0) {
+ return ;
+ }
+ if (idlePhase === 1) {
+ return ;
+ }
+ if (idlePhase === 2) {
+ return ;
+ }
+ }
+
+ if (loading && !nextDeparture) return
;
if (!nextDeparture) {
return (
diff --git a/src/pages/afficheurs/classiques/ClassiquesArrivees.js b/src/pages/afficheurs/classiques/ClassiquesArrivees.js
index f81d324..054114d 100644
--- a/src/pages/afficheurs/classiques/ClassiquesArrivees.js
+++ b/src/pages/afficheurs/classiques/ClassiquesArrivees.js
@@ -143,7 +143,7 @@ function Clock(){
);
}
-export default function ClassiquesArrivees(){
+export default function ClassiquesArrivees({ defilement = null } = {}){
// lire le paramètre de requête ?gare=Nom-de-la-gare
const params = new URLSearchParams(window.location.search);
const gareParam = params.get('gare');
@@ -151,8 +151,8 @@ export default function ClassiquesArrivees(){
const gareIdParam = params.get('gare_id') || params.get('gareId');
// gérer les +, décoder et remplacer les tirets par des espaces
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
- // Mode défilement (marquee vertical infini) si ?defilement=true dans l'URL
- const defilementMode = params.get('defilement') === 'true';
+ // Mode défilement (marquee vertical infini) si ?defilement=true dans l'URL ou passé en prop
+ const defilementMode = defilement !== null ? Boolean(defilement) : (params.get('defilement') === 'true');
// debug flag activable via ?debug=1 or ?debug=true
const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');
diff --git a/src/pages/afficheurs/classiques/ClassiquesDeparts.js b/src/pages/afficheurs/classiques/ClassiquesDeparts.js
index 87a836a..9101d99 100644
--- a/src/pages/afficheurs/classiques/ClassiquesDeparts.js
+++ b/src/pages/afficheurs/classiques/ClassiquesDeparts.js
@@ -143,7 +143,7 @@ function Clock(){
);
}
-export default function ClassiquesDeparts(){
+export default function ClassiquesDeparts({ defilement = null } = {}){
// lire le paramètre de requête ?gare=Nom-de-la-gare
const params = new URLSearchParams(window.location.search);
const gareParam = params.get('gare');
@@ -151,8 +151,8 @@ export default function ClassiquesDeparts(){
const gareIdParam = params.get('gare_id') || params.get('gareId');
// gérer les +, décoder et remplacer les tirets par des espaces
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
- // Mode défilement (marquee vertical infini) si ?defilement=true dans l'URL
- const defilementMode = params.get('defilement') === 'true';
+ // Mode défilement (marquee vertical infini) si ?defilement=true dans l'URL ou passé en prop
+ const defilementMode = defilement !== null ? Boolean(defilement) : (params.get('defilement') === 'true');
// debug flag activable via ?debug=1 or ?debug=true
const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');
diff --git a/src/pages/afficheurs/classiques/ClassiquesFlashInfo.css b/src/pages/afficheurs/classiques/ClassiquesFlashInfo.css
new file mode 100644
index 0000000..0ae3467
--- /dev/null
+++ b/src/pages/afficheurs/classiques/ClassiquesFlashInfo.css
@@ -0,0 +1,215 @@
+body, html {
+ margin: 0;
+ padding: 0;
+ background-color: #fab23e;
+ overflow: hidden;
+}
+
+/* =========================================================
+ CONTAINER (1920x1080)
+ ========================================================= */
+.aff-classique-flash__container {
+ position: relative;
+ width: 1920px;
+ height: 1080px;
+ background-color: #fab23e;
+ color: #00337f;
+ font-family: 'Achemine', 'Avenir', 'Helvetica Neue', Arial, sans-serif;
+ overflow: hidden;
+ box-sizing: border-box;
+ display: flex;
+}
+
+/* =========================================================
+ SIDEBAR GAUCHE : BLEU NUIT (Conforme Image 3)
+ ========================================================= */
+.aff-classique-flash__sidebar {
+ width: 320px;
+ height: 1080px;
+ background-color: #00337f;
+ box-sizing: border-box;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ flex-shrink: 0;
+}
+
+.aff-classique-flash__sidebar-text {
+ transform: rotate(-90deg);
+ transform-origin: center center;
+ white-space: nowrap;
+ display: flex;
+ align-items: baseline;
+ gap: 24px;
+ user-select: none;
+}
+
+.aff-classique-flash__title-flash {
+ color: #f5a623;
+ font-size: 10.5rem; /* ~125px */
+ font-weight: 700;
+ letter-spacing: 4px;
+ line-height: 1;
+}
+
+.aff-classique-flash__title-circulation {
+ color: #ffffff;
+ font-size: 10.5rem; /* ~125px */
+ font-weight: 700;
+ letter-spacing: 4px;
+ line-height: 1;
+}
+
+/* =========================================================
+ ZONE PRINCIPALE : JAUNE / AMBRE (Conforme Image 3)
+ ========================================================= */
+.aff-classique-flash__main {
+ flex: 1;
+ height: 1080px;
+ background-color: #fab23e;
+ position: relative;
+ box-sizing: border-box;
+ padding: 60px 80px;
+ display: flex;
+ flex-direction: column;
+}
+
+.aff-classique-flash__content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding-bottom: 90px;
+}
+
+/* AFFICHAGE NOMINAL (TRAFIC NORMAL) */
+.aff-classique-flash__nominal {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ max-width: 1300px;
+}
+
+.aff-classique-flash__nominal-title {
+ color: #00337f;
+ font-size: 6.5rem; /* ~78px */
+ font-weight: 700;
+ margin-bottom: 24px;
+ line-height: 1.1;
+ text-transform: capitalize;
+}
+
+.aff-classique-flash__nominal-subtitle {
+ color: #00337f;
+ font-size: 3.8rem; /* ~45px */
+ font-weight: 500;
+ line-height: 1.25;
+ margin-bottom: 24px;
+}
+
+.aff-classique-flash__nominal-detail {
+ color: #00337f;
+ font-size: 2.8rem; /* ~33px */
+ font-weight: 400;
+ line-height: 1.3;
+ opacity: 0.9;
+}
+
+/* CARTES D'ALERTE TRAFIC */
+.aff-classique-flash__alerts-container {
+ display: flex;
+ flex-direction: column;
+ gap: 28px;
+ max-width: 1350px;
+}
+
+.aff-classique-flash__alert-card {
+ background-color: #00337f;
+ color: #ffffff;
+ border-radius: 8px;
+ padding: 32px 40px;
+ box-shadow: 0 8px 24px rgba(0, 51, 127, 0.2);
+}
+
+.aff-classique-flash__alert-card--travaux {
+ background-color: #1a2332;
+}
+
+.aff-classique-flash__alert-header {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ margin-bottom: 14px;
+}
+
+.aff-classique-flash__alert-tag {
+ background-color: #d32f2f;
+ color: #ffffff;
+ font-size: 2rem;
+ font-weight: 700;
+ padding: 4px 14px;
+ border-radius: 4px;
+ text-transform: uppercase;
+}
+
+.aff-classique-flash__alert-tag--travaux {
+ background-color: #f5a623;
+ color: #00337f;
+}
+
+.aff-classique-flash__alert-line {
+ font-size: 2.6rem;
+ font-weight: 700;
+ color: #fab23e;
+}
+
+.aff-classique-flash__alert-title {
+ font-size: 3.6rem;
+ font-weight: 700;
+ margin-bottom: 12px;
+ line-height: 1.2;
+}
+
+.aff-classique-flash__alert-desc {
+ font-size: 2.8rem;
+ font-weight: 400;
+ line-height: 1.35;
+ color: #e5eaf3;
+}
+
+/* =========================================================
+ HORLOGE BAS DROITE (Conforme Image 3)
+ ========================================================= */
+.aff-classique-flash__footer-clock-box {
+ position: absolute;
+ bottom: 25px;
+ right: 35px;
+ background-color: #00337f;
+ border: 2.5px solid #ffffff;
+ border-radius: 4px;
+ padding: 4px 16px;
+ display: flex;
+ align-items: baseline;
+ z-index: 20;
+}
+
+.aff-classique-flash__clock {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
+ font-size: 3.4rem;
+ font-weight: 700;
+ line-height: 1;
+}
+
+.aff-classique-flash__clock-hm {
+ color: #ffffff;
+}
+
+.aff-classique-flash__clock-s {
+ color: #f5a623;
+}
diff --git a/src/pages/afficheurs/classiques/ClassiquesFlashInfo.js b/src/pages/afficheurs/classiques/ClassiquesFlashInfo.js
new file mode 100644
index 0000000..6b46885
--- /dev/null
+++ b/src/pages/afficheurs/classiques/ClassiquesFlashInfo.js
@@ -0,0 +1,117 @@
+import React, { useState, useEffect } from 'react';
+import './ClassiquesFlashInfo.css';
+import useAfficheurData from '../../../hooks/useAfficheurData';
+
+function DigitalClock() {
+ const [now, setNow] = useState(new Date());
+ useEffect(() => {
+ const id = setInterval(() => setNow(new Date()), 1000);
+ return () => clearInterval(id);
+ }, []);
+ const hh = String(now.getHours()).padStart(2, '0');
+ const mm = String(now.getMinutes()).padStart(2, '0');
+ const ss = String(now.getSeconds()).padStart(2, '0');
+ return (
+
+ {hh}:{mm}
+ {ss}
+
+ );
+}
+
+export default function ClassiquesFlashInfo() {
+ const params = new URLSearchParams(window.location.search || '');
+ const gareParam = params.get('gare');
+ const gareIdParam = params.get('gare_id') || params.get('gareId');
+
+ const gareDisplay = gareParam
+ ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ')
+ : 'Votre Gare';
+
+ const {
+ perturbations = [],
+ travaux = []
+ } = useAfficheurData({
+ gareName: gareDisplay,
+ gareId: gareIdParam,
+ mode: 'departs',
+ refreshIntervalMs: 20000
+ });
+
+ const activePerturbations = (perturbations || []).filter(p => {
+ const t = (p.type_perturbation || p.type || '').toString().toLowerCase();
+ return Boolean(t);
+ });
+
+ const hasTrafficAlerts = activePerturbations.length > 0 || (travaux && travaux.length > 0);
+
+ return (
+
+ {/* BANDEAU GAUCHE : BLEU NUIT (Conforme Image 3) */}
+
+
+ flash
+ circulation
+
+
+
+ {/* ZONE PRINCIPALE : AMBRE / JAUNE ORANGE (Conforme Image 3) */}
+
+
+ {hasTrafficAlerts ? (
+
+ {activePerturbations.map((p, idx) => (
+
+
+ Info Trafic
+ {p.ligne_nom || p.ligne || gareDisplay}
+
+
+ {p.type_perturbation || p.titre || 'Perturbation en cours'}
+
+
+ {p.message || p.description || p.detail || p.cause || 'Des adaptations de circulation peuvent survenir sur vos trajets. Veuillez écouter les annonces en gare.'}
+
+
+ ))}
+
+ {(travaux || []).slice(0, 2).map((t, idx) => (
+
+
+ Travaux
+ {t.nom || t.intitule || 'Maintenance du réseau'}
+
+
+ {t.intitule || t.nom || 'Travaux de modernisation'}
+
+
+ {t.description || t.motif || 'Des substitutions par autocars peuvent être mises en place. Merci de vérifier vos horaires.'}
+
+
+ ))}
+
+ ) : (
+
+
+
+ Trafic normal
+
+
+ La circulation des trains s'effectue normalement sur l'ensemble des lignes desservant la gare de {gareDisplay}.
+
+
+ Pour toute information complémentaire concernant vos trajets, nos agents en gare sont à votre disposition.
+
+
+
+ )}
+
+
+ {/* HORLOGE EN BAS À DROITE (Conforme Image 3) */}
+
+
+
+
+
+ );
+}
diff --git a/src/pages/afficheurs/classiques/ClassiquesInformations.css b/src/pages/afficheurs/classiques/ClassiquesInformations.css
new file mode 100644
index 0000000..92099fc
--- /dev/null
+++ b/src/pages/afficheurs/classiques/ClassiquesInformations.css
@@ -0,0 +1,133 @@
+body, html {
+ margin: 0;
+ padding: 0;
+ background-color: #fab23e;
+ overflow: hidden;
+}
+
+/* =========================================================
+ CONTAINER (1920x1080)
+ ========================================================= */
+.aff-classique-infos__container {
+ position: relative;
+ width: 1920px;
+ height: 1080px;
+ background-color: #fab23e;
+ color: #00337f;
+ font-family: 'Achemine', 'Avenir', 'Helvetica Neue', Arial, sans-serif;
+ overflow: hidden;
+ box-sizing: border-box;
+ display: flex;
+ user-select: none;
+}
+
+/* =========================================================
+ SIDEBAR GAUCHE : BLEU NUIT (Conforme Image 4)
+ ========================================================= */
+.aff-classique-infos__sidebar {
+ width: 320px;
+ height: 1080px;
+ background-color: #00337f;
+ box-sizing: border-box;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ flex-shrink: 0;
+}
+
+.aff-classique-infos__sidebar-text {
+ transform: rotate(-90deg);
+ transform-origin: center center;
+ white-space: nowrap;
+ user-select: none;
+}
+
+.aff-classique-infos__title-information {
+ color: #ffffff;
+ font-size: 10.5rem; /* ~125px */
+ font-weight: 700;
+ letter-spacing: 4px;
+ line-height: 1;
+}
+
+/* =========================================================
+ ZONE PRINCIPALE : CADRE JAUNE DE DROITE
+ ========================================================= */
+.aff-classique-infos__main {
+ flex: 1;
+ height: 1080px;
+ background-color: #fab23e;
+ position: relative;
+ box-sizing: border-box;
+ padding: 80px 100px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+/* CONTENEUR DU MESSAGE UNIQUE (TEXTE BLEU SUR FOND JAUNE) */
+.aff-classique-infos__single-message-container {
+ max-width: 1350px;
+ display: flex;
+ flex-direction: column;
+ gap: 36px;
+ padding-bottom: 90px;
+}
+
+.aff-classique-infos__single-title {
+ color: #00337f;
+ font-size: 64px;
+ font-weight: 900;
+ line-height: 1.15;
+ margin: 0;
+ letter-spacing: -0.5px;
+ text-transform: none;
+}
+
+.aff-classique-infos__single-body {
+ color: #00337f;
+ font-size: 46px;
+ font-weight: 600;
+ line-height: 1.45;
+ margin: 0;
+ max-width: 1250px;
+}
+
+/* =========================================================
+ HORLOGE DIGITALE EN BAS À DROITE (Conforme Image 4)
+ ========================================================= */
+.aff-classique-infos__footer-clock-box {
+ position: absolute;
+ bottom: 40px;
+ right: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.aff-classique-infos__clock {
+ display: inline-flex;
+ align-items: baseline;
+ background-color: #00337f;
+ border: 4px solid #ffffff;
+ border-radius: 8px;
+ padding: 10px 22px;
+ gap: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
+}
+
+.aff-classique-infos__clock-hm {
+ font-size: 3.4rem; /* ~54px */
+ font-weight: 800;
+ color: #ffffff;
+ letter-spacing: 1px;
+}
+
+.aff-classique-infos__clock-s {
+ font-size: 2.2rem; /* ~35px */
+ font-weight: 800;
+ color: #ff9e1b;
+ letter-spacing: 0.5px;
+}
diff --git a/src/pages/afficheurs/classiques/ClassiquesInformations.js b/src/pages/afficheurs/classiques/ClassiquesInformations.js
new file mode 100644
index 0000000..209b6f3
--- /dev/null
+++ b/src/pages/afficheurs/classiques/ClassiquesInformations.js
@@ -0,0 +1,120 @@
+import React, { useState, useEffect } from 'react';
+import './ClassiquesInformations.css';
+import supabase from '../../../lib/supabase';
+
+function DigitalClock() {
+ const [now, setNow] = useState(new Date());
+ useEffect(() => {
+ const id = setInterval(() => setNow(new Date()), 1000);
+ return () => clearInterval(id);
+ }, []);
+ const hh = String(now.getHours()).padStart(2, '0');
+ const mm = String(now.getMinutes()).padStart(2, '0');
+ const ss = String(now.getSeconds()).padStart(2, '0');
+ return (
+
+ {hh}:{mm}
+ {ss}
+
+ );
+}
+
+export default function ClassiquesInformations() {
+ const params = new URLSearchParams(window.location.search || '');
+ const gareParam = params.get('gare');
+
+ const gareDisplay = gareParam
+ ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ')
+ : 'Votre Gare';
+
+ const defaultMessages = [
+ {
+ title: 'Plan Vigipirate & Sécurité',
+ content: 'Ne laissez aucun bagage sans surveillance. Tout bagage abandonné sera détruit. Signalez tout colis ou comportement suspect aux agents SNCF ou aux forces de l\'ordre.'
+ },
+ {
+ title: 'Étiquetage obligatoire de vos bagages',
+ content: 'Pour votre tranquillité et la sécurité de tous, vos valises et sacs doivent impérativement comporter une étiquette visible indiquant vos nom, prénom, téléphone et adresse.'
+ },
+ {
+ title: 'Traversée des voies formellement interdite',
+ content: 'Pour votre sécurité, il est strictement interdit de descendre sur les voies ou de les traverser. Utilisez systématiquement les passages souterrains ou les passerelles.'
+ },
+ {
+ title: 'Espace sans tabac ni vapotage',
+ content: 'Conformément à la réglementation, il est strictement interdit de fumer ou de vapoter dans les espaces couverts, les bâtiments de la gare ainsi que sur les quais.'
+ }
+ ];
+
+ const [messagesList, setMessagesList] = useState(defaultMessages);
+ const [activeMessageIndex, setActiveMessageIndex] = useState(0);
+
+ // Chargement éventuel de messages dynamiques depuis Supabase
+ useEffect(() => {
+ let mounted = true;
+ const fetchStationMessages = async () => {
+ if (!supabase) return;
+ try {
+ const { data, error } = await supabase
+ .from('messages_dynamiques')
+ .select('*')
+ .order('id', { ascending: false });
+ if (!error && Array.isArray(data) && data.length > 0 && mounted) {
+ const formatted = data.map(m => ({
+ title: m.titre || m.title || 'Information Voyageurs',
+ content: m.contenu || m.content || m.message || ''
+ })).filter(m => Boolean(m.content));
+ if (formatted.length > 0) {
+ setMessagesList(formatted);
+ }
+ }
+ } catch (e) {
+ console.warn('Notice chargement messages_dynamiques:', e);
+ }
+ };
+ fetchStationMessages();
+ return () => { mounted = false; };
+ }, []);
+
+ // Défilement d'un seul message à la fois toutes les 10s si plusieurs messages
+ useEffect(() => {
+ if (messagesList.length <= 1) return;
+ const timer = setInterval(() => {
+ setActiveMessageIndex(prev => (prev + 1) % messagesList.length);
+ }, 10000);
+ return () => clearInterval(timer);
+ }, [messagesList.length]);
+
+ const activeMsg = messagesList[activeMessageIndex % messagesList.length] || defaultMessages[0];
+
+ return (
+
+ {/* BANDEAU GAUCHE : BLEU NUIT (Conforme Image 4) */}
+
+
+ {/* ZONE PRINCIPALE : CADRE JAUNE DE DROITE AVEC TEXTE BLEU (Conforme Image 4) */}
+
+
+ {activeMsg.title && (
+
+ {activeMsg.title}
+
+ )}
+
+
+ {activeMsg.content}
+
+
+
+ {/* HORLOGE EN BAS À DROITE */}
+
+
+
+
+
+ );
+}
diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.css b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.css
index e30b82c..5c3baf6 100644
--- a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.css
+++ b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.css
@@ -1,337 +1,315 @@
-@font-face {
- font-family: 'Achemine';
- src: url('/public/fonts/achemine_regular.ttf') format('truetype');
- font-weight: 400;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: 'Achemine';
- src: url('/public/fonts/achemine_bold.ttf') format('truetype');
- font-weight: 700;
- font-style: normal;
- font-display: swap;
-}
-
body, html {
margin: 0;
padding: 0;
- background-color: #006330; /* Green theme */
+ background-color: #116938;
overflow: hidden;
}
-.aff-quais-arrivees-redesign__container {
- display: flex;
- flex-direction: row;
- width: 100%;
- height: 100%;
- position: relative;
- font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
- background-color: #006330;
- color: #fff;
- overflow: hidden;
-}
-
-.aff-quais-arrivees-redesign__empty {
- display: flex;
- justify-content: center;
- align-items: center;
- flex-direction: column;
-}
-
-.aff-quais-arrivees-redesign__empty-text {
- font-size: 6rem;
- font-weight: 700;
- color: #ffffff;
- text-align: center;
-}
-
/* =========================================================
- LEFT PANEL
+ CONTAINER (1920x1080)
========================================================= */
-.aff-quais-arrivees-redesign__left {
+.aff-classique-quai-arr__container {
+ position: relative;
+ width: 1920px;
+ height: 1080px;
+ background-color: #116938;
+ color: #ffffff;
+ font-family: 'Achemine', 'Avenir', 'Helvetica Neue', Arial, sans-serif;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+/* =========================================================
+ LEFT PANEL (BLANC - Conforme Image 2)
+ ========================================================= */
+.aff-classique-quai-arr__left {
position: absolute;
top: 0;
left: 0;
- width: 35%;
- bottom: 80px;
- background-color: #f3eef4; /* Lilac/Light Grey background */
+ width: 672px;
+ bottom: 85px;
+ background-color: #ffffff;
box-sizing: border-box;
- padding: 30px 40px;
+ padding: 42px 48px;
display: flex;
flex-direction: column;
overflow: hidden;
-}
-
-.aff-quais-arrivees-redesign__sncf-logo {
- width: 90px;
- height: auto;
- margin-bottom: 25px;
z-index: 2;
}
-.aff-quais-arrivees-redesign__time-status {
+/* Logo Dynamique (TER Bleu / Opérateur) */
+.aff-classique-quai-arr__brand {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ margin-bottom: 24px;
+ z-index: 3;
+}
+
+.aff-classique-quai-arr__brand-logo {
+ height: 50px;
+ max-width: 220px;
+ object-fit: contain;
+ display: block;
+}
+
+.aff-classique-quai-arr__sncf-voyageurs {
+ font-size: 14px;
+ font-weight: 800;
+ color: #116938;
+ letter-spacing: 2px;
+ margin-top: 4px;
+}
+
+/* Ligne Heure + Statut */
+.aff-classique-quai-arr__time-row {
display: flex;
align-items: baseline;
- gap: 15px;
- margin-bottom: 5px;
- z-index: 2;
+ margin-bottom: 8px;
+ z-index: 3;
}
-.aff-quais-arrivees-redesign__time {
- color: #006330; /* Green theme text */
+.aff-classique-quai-arr__time {
+ color: #116938;
font-weight: 700;
- font-size: 7.5rem;
+ font-size: 105px;
+ line-height: 0.92;
+ letter-spacing: -1px;
+}
+
+.aff-classique-quai-arr__status {
+ color: #116938;
+ font-weight: 700;
+ font-size: 32px;
line-height: 1;
+ margin-left: 20px;
}
-.aff-quais-arrivees-redesign__status {
- color: #006330; /* Green theme status */
+.aff-classique-quai-arr__time--suppressed,
+.aff-classique-quai-arr__status--suppressed {
+ color: #d32f2f !important;
+}
+
+.aff-classique-quai-arr__time--delayed,
+.aff-classique-quai-arr__status--delayed {
+ color: #f5a623 !important;
+}
+
+/* Grande Destination / Gare Terminus */
+.aff-classique-quai-arr__destination {
+ color: #116938;
font-weight: 700;
- font-size: 2.4rem;
- line-height: 1;
+ font-size: 82px;
+ line-height: 1.05;
+ margin-top: 14px;
+ margin-bottom: 14px;
+ z-index: 3;
+ word-break: break-word;
}
-.aff-quais-arrivees-redesign__destination {
- color: #006330; /* Green theme destination (origin) */
- font-weight: 700;
- font-size: 5.5rem; /* Slightly smaller to fit 2 lines if needed */
- line-height: 1.1;
- margin-bottom: 5px;
- z-index: 2;
- white-space: normal; /* Allow wrapping */
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- text-overflow: ellipsis;
- max-height: 13rem; /* Prevent overflowing panel */
-}
-
-.aff-quais-arrivees-redesign__train-info {
+/* Info Train (Type + Numéro) */
+.aff-classique-quai-arr__train-info {
display: flex;
- align-items: center;
- gap: 10px;
- z-index: 2;
+ align-items: baseline;
+ z-index: 3;
}
-.aff-quais-arrivees-redesign__train-type {
- color: #006330; /* Green theme train type */
+.aff-classique-quai-arr__train-type {
+ color: #116938;
font-weight: 700;
- font-size: 2.2rem;
+ font-size: 38px;
letter-spacing: 0.5px;
}
-.aff-quais-arrivees-redesign__train-number {
- color: #7e8a80; /* Grey/Green train number */
+.aff-classique-quai-arr__train-number {
+ color: #7e859b;
font-weight: 400;
- font-size: 2.2rem;
+ font-size: 38px;
letter-spacing: 0.5px;
+ margin-left: 12px;
}
-.aff-quais-arrivees-redesign__icons {
+/* Filigrane Vertical "arrivée" géant (orienté bas vers haut) */
+.aff-classique-quai-arr__watermark {
position: absolute;
+ right: -5px;
bottom: 20px;
- left: 40px;
+ width: 0;
+ height: 0;
display: flex;
- align-items: center;
- z-index: 2;
-}
-
-.aff-quais-arrivees-redesign__watermark {
- position: absolute;
- right: 0;
- top: 50%;
- transform: translateY(-50%) rotate(-90deg);
- transform-origin: right center;
- font-size: 17rem; /* Massive size */
- color: #dbebdf; /* Light green/grey watermark */
- font-weight: 700;
- z-index: 1;
+ justify-content: flex-start;
+ align-items: flex-end;
pointer-events: none;
user-select: none;
- letter-spacing: 1.5rem;
+ z-index: 1;
+}
+
+.aff-classique-quai-arr__watermark span {
+ display: inline-block;
+ transform: rotate(-90deg);
+ transform-origin: 0 100%;
+ font-size: 168px;
+ font-weight: 700;
+ color: #d5e5d8;
line-height: 1;
+ letter-spacing: 0.05em;
white-space: nowrap;
}
/* =========================================================
- RIGHT PANEL
+ RIGHT PANEL (VERT FORÊT - Conforme Image 2)
========================================================= */
-.aff-quais-arrivees-redesign__right {
+.aff-classique-quai-arr__right {
position: absolute;
top: 0;
+ left: 672px;
right: 0;
- width: 65%;
- bottom: 80px;
- background-color: #006330; /* Green theme */
+ bottom: 85px;
+ background-color: #116938;
box-sizing: border-box;
- padding: 30px 0 30px 0;
+ padding: 42px 0 20px 42px;
overflow: hidden;
display: flex;
flex-direction: column;
}
-.aff-quais-arrivees-redesign__provenance-title {
- font-size: 3.4rem;
- color: #dbeb1a; /* Yellow provenance header */
- margin-left: 60px;
- margin-bottom: 20px;
- font-weight: 700;
+.aff-classique-quai-arr__stops-wrapper {
+ position: relative;
+ width: 100%;
+ height: 100%;
}
-.aff-quais-arrivees-redesign__stops-list {
+.aff-classique-quai-arr__stops-list {
list-style: none;
margin: 0;
- padding: 0 0 0 60px; /* Space from left edge */
- width: 100%;
- position: relative;
- z-index: 2;
+ padding: 0;
display: flex;
flex-direction: column;
- gap: 8px; /* Closer spacing between stops */
+ gap: 12px;
+ position: relative;
+ z-index: 2;
}
-.aff-quais-arrivees-redesign__stop {
+.aff-classique-quai-arr__stop-item {
display: flex;
align-items: center;
- font-size: 3.4rem;
- font-weight: 400;
- color: #ffffff;
position: relative;
- min-height: 42px;
- padding-left: 50px; /* Space for rail and bullet */
+ min-height: 56px;
+ padding-left: 65px;
}
-/* Bullet styling */
-.aff-quais-arrivees-redesign__stop-bullet {
+/* Rail vertical vert sauge (#5c8b6b) */
+.aff-classique-quai-arr__stop-item::before {
+ content: '';
position: absolute;
- left: 0; /* Centered on 24px rail: (24-16)/2 = 4 */
- margin-left: 4px;
- top: 50%;
- transform: translateY(-50%);
- width: 16px; /* Slightly larger bullet */
- height: 16px;
- background-color: #fbec22;
+ left: 20px;
+ width: 28px;
+ background-color: #5c8b6b;
+ top: -15px;
+ bottom: -15px;
+ z-index: 1;
+}
+
+/* Le rail monte jusqu'en haut au 1er arrêt */
+.aff-classique-quai-arr__stop-item:first-child::before {
+ top: -600px;
+}
+
+/* Le dernier arrêt n'a pas de rail qui continue en dessous */
+.aff-classique-quai-arr__stop-item:last-child::before {
+ display: none;
+}
+
+/* Pastille Jaune (#ffe500 / #fbec22) */
+.aff-classique-quai-arr__bullet {
+ position: absolute;
+ left: 24px;
+ width: 20px;
+ height: 20px;
+ background-color: #ffe500;
border-radius: 50%;
z-index: 4;
}
-.aff-quais-arrivees-redesign__stop-text {
- position: relative;
- z-index: 4;
+/* Nom de la station */
+.aff-classique-quai-arr__stop-name {
+ font-size: 42px;
+ font-weight: 600;
+ color: #ffffff;
white-space: nowrap;
letter-spacing: 0.5px;
- padding-left: 10px; /* Space from bullet */
+ z-index: 4;
}
-/* The vertical rail segments drawn behind each bullet */
-.aff-quais-arrivees-redesign__stop::before {
- content: '';
- position: absolute;
- left: 0;
- width: 24px; /* Much wider rail */
- background-color: #8fa993; /* Light green-grey rail color */
- top: -10px;
- bottom: -10px;
- z-index: 1;
+/* Gare terminus / mise en avant */
+.aff-classique-quai-arr__stop-item--dest {
+ font-weight: 700;
}
-/* Rail starts at the first bullet's center */
-.aff-quais-arrivees-redesign__stop:first-child::before {
- top: 50%;
-}
-
-/* Last stop has no rail continuing below */
-.aff-quais-arrivees-redesign__stop:last-child::before {
- bottom: 50%;
+.aff-classique-quai-arr__stop-item--dest .aff-classique-quai-arr__stop-name {
+ background-color: #5c8b6b;
+ padding: 8px 32px 8px 65px;
+ margin-left: -65px;
+ border-radius: 6px;
+ position: relative;
+ z-index: 3;
+ display: inline-block;
+ font-weight: 700;
+ font-size: 44px;
}
/* =========================================================
- FOOTER
+ FOOTER & HORLOGE (Conforme Image 2)
========================================================= */
-.aff-quais-arrivees-redesign__footer {
+.aff-classique-quai-arr__footer {
position: absolute;
bottom: 0;
left: 0;
- width: 100%;
- height: 80px;
- background-color: #006330; /* Green theme */
+ width: 1920px;
+ height: 85px;
+ background-color: #116938;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
- padding: 0 40px;
- z-index: 10;
+ padding: 0 35px;
+ z-index: 20;
}
-.aff-quais-arrivees-redesign__footer-left {
- font-size: 3rem;
+.aff-classique-quai-arr__footer-track {
+ font-size: 34px;
font-weight: 700;
color: #ffffff;
font-style: italic;
- font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
letter-spacing: 0.5px;
}
-.aff-quais-arrivees-redesign__footer-right {
+.aff-classique-quai-arr__footer-clock {
display: flex;
align-items: center;
}
-.aff-quais-arrivees-redesign__clock {
- border: 2px solid #ffffff;
- padding: 4px 14px;
- font-size: 3rem;
- font-weight: 700;
- color: #ffffff;
+.aff-classique-quai-arr__clock {
+ background-color: #00337f;
+ border: 2.5px solid #ffffff;
+ padding: 4px 16px;
+ border-radius: 3px;
display: flex;
- align-items: center;
- gap: 10px;
+ align-items: baseline;
+ gap: 4px;
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
+ font-size: 42px;
+ font-weight: 700;
+ line-height: 1;
}
-.aff-quais-arrivees-redesign__clock-s {
- color: #f5a623;
+.aff-classique-quai-arr__clock-hm {
+ color: #ffffff;
}
-.aff-quais-arrivees-redesign__time--long-text {
- font-size: 3.5rem !important;
- line-height: 1.2 !important;
+.aff-classique-quai-arr__clock-s {
+ color: #ff8c00;
+ margin-left: 2px;
}
-.aff-quais-arrivees-redesign__time--suppressed,
-.aff-quais-arrivees-redesign__status--suppressed {
- color: #ff4c5b !important;
-}
-
-.aff-quais-arrivees-redesign__time--delayed,
-.aff-quais-arrivees-redesign__status--delayed {
- color: #ff9e43 !important;
-}
-
-/* Disrupted Stop styling */
-.aff-quais-arrivees-redesign__stop-text--suppressed {
- text-decoration: line-through;
- color: #ff4c5b;
- opacity: 0.6;
-}
-
-.aff-quais-arrivees-redesign__stop-text--delayed {
- color: #ff9e43;
-}
-
-.aff-quais-arrivees-redesign__stop-status-badge {
- font-size: 2.2rem;
- font-weight: bold;
- margin-left: 10px;
-}
-
-.aff-quais-arrivees-redesign__stop-status-badge--suppressed {
- color: #ff4c5b;
-}
-
-.aff-quais-arrivees-redesign__stop-status-badge--delayed {
- color: #ff9e43;
-}
diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js
index 879ffdc..35ba995 100644
--- a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js
+++ b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js
@@ -6,133 +6,179 @@ import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../../services/railwayService';
import { getSncfTimetable } from '../../../../services/sncfApi';
-function Clock() {
+import GareVideoprotection from '../../../../components/afficheurs/GareVideoprotection';
+import AttentifsEnsemble from '../../../../components/afficheurs/AttentifsEnsemble';
+import ClassiquesInformations from '../ClassiquesInformations';
+import ClassiquesArrivees from '../ClassiquesArrivees';
+
+function DigitalClock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id);
}, []);
- const hh = String(now.getHours()).padStart(2,'0');
- const mm = String(now.getMinutes()).padStart(2,'0');
- const ss = String(now.getSeconds()).padStart(2,'0');
+ const hh = String(now.getHours()).padStart(2, '0');
+ const mm = String(now.getMinutes()).padStart(2, '0');
+ const ss = String(now.getSeconds()).padStart(2, '0');
return (
-
-
{hh}
-
{mm}
-
{ss}
+
+ {hh}:{mm}
+ {ss}
);
}
-export default function ClassiquesArriveesQuais() {
- const paramsForThis = new URLSearchParams(window.location.search || '');
- const gareParam = paramsForThis.get('gare');
- const gareIdParam = paramsForThis.get('gare_id') || paramsForThis.get('gareId');
- const quaiParam = paramsForThis.get('quai');
+function getTrainBrand(trainType = '', operator = '', isDeparture = false) {
+ const combined = `${trainType || ''} ${operator || ''}`.toLowerCase().trim();
- const gareDisplayParam = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g,' ')).replace(/-/g,' ') : null;
+ if (combined.includes('zou')) {
+ return { logo: '/type/quais/logo-zou-color.svg', label: 'ZOU!', showVoyageursText: false };
+ }
+ if (combined.includes('fluo')) {
+ return { logo: '/type/quais/logo-fluo-color.svg', label: 'TER Fluo', showVoyageursText: false };
+ }
+ if (combined.includes('aleop') || combined.includes('al\u00e9op')) {
+ return { logo: '/type/quais/logo-aleop-color.svg', label: 'Aléop', showVoyageursText: false };
+ }
+ if (combined.includes('breizh')) {
+ return { logo: '/type/quais/logo-breizhgo-color.svg', label: 'BreizhGo', showVoyageursText: false };
+ }
+ if (combined.includes('lio') || combined.includes('li\u00f4')) {
+ return { logo: '/type/quais/logo-lio-color.svg', label: 'liO', showVoyageursText: false };
+ }
+ if (combined.includes('mobigo')) {
+ return { logo: '/type/quais/logo-mobigo-color.svg', label: 'Mobigo', showVoyageursText: false };
+ }
+ if (combined.includes('nomad')) {
+ return { logo: '/type/quais/logo-nomad-color.svg', label: 'Nomad', showVoyageursText: false };
+ }
+ if (combined.includes('remi') || combined.includes('r\u00e9mi')) {
+ return { logo: '/type/quais/logo-remi-color.svg', label: 'Rémi', showVoyageursText: false };
+ }
+ if (combined.includes('inoui') || combined.includes('tgv inoui')) {
+ return { logo: '/type/quais/logo-inoui-color.svg', label: 'TGV INOUI', showVoyageursText: false };
+ }
+ if (combined.includes('ouigo classique')) {
+ return { logo: '/type/quais/logo-ouigo-classique-color.svg', label: 'OUIGO Classique', showVoyageursText: false };
+ }
+ if (combined.includes('ouigo')) {
+ return { logo: '/type/quais/logo-ouigo-color.svg', label: 'OUIGO', showVoyageursText: false };
+ }
+ if (combined.includes('intercite') || combined.includes('intercit\u00e9')) {
+ return { logo: '/type/quais/logo-intercite-color.svg', label: 'INTERCITÉS', showVoyageursText: false };
+ }
+ if (combined.includes('tgv')) {
+ return { logo: '/type/quais/logo-tgv-color.svg', label: 'TGV', showVoyageursText: false };
+ }
+ if (combined.includes('eurostar')) {
+ return { logo: '/type/quais/logo-eurostar-color.svg', label: 'Eurostar', showVoyageursText: false };
+ }
+ if (combined.includes('freccia') || combined.includes('trenitalia')) {
+ return { logo: '/type/quais/logo-frecciarossa-color.svg', label: 'Frecciarossa', showVoyageursText: false };
+ }
+ if (combined.includes('ice') || combined.includes('db')) {
+ return { logo: '/type/quais/logo-ice-color.svg', label: 'ICE', showVoyageursText: false };
+ }
+ if (combined.includes('ter')) {
+ return { logo: '/type/quais/logo-ter-color.svg', label: 'TER', showVoyageursText: false };
+ }
+
+ // Par défaut Arrivées : Logo TER Bleu (Image 2)
+ if (!isDeparture) {
+ return { logo: '/type/quais/logo-ter-color.svg', label: trainType || 'TER', showVoyageursText: false };
+ }
+ return { logo: '/type/quais/logo-sncf-color.svg', label: trainType || 'SNCF', showVoyageursText: true };
+}
+
+export default function ClassiquesArriveesQuais() {
+ const searchParams = new URLSearchParams(window.location.search || '');
+ let gareParam = searchParams.get('gare') || searchParams.get('station') || '';
+ const gareIdParam = searchParams.get('gare_id') || searchParams.get('gareId') || null;
+ const quaiParam = searchParams.get('quai') || searchParams.get('voie') || searchParams.get('track') || searchParams.get('platform') || '';
+
+ // Fallback si la gare est dans le chemin
+ if (!gareParam && typeof window !== 'undefined') {
+ const cleanPath = window.location.pathname.replace(/^\/ferrovia/, '');
+ const parts = cleanPath.split('/').filter(Boolean);
+ const qIdx = parts.findIndex(p => p === 'quais');
+ if (qIdx >= 0 && parts[qIdx + 1]) {
+ gareParam = parts.slice(qIdx + 1).join('/');
+ }
+ }
+
+ const gareDisplay = gareParam
+ ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ')
+ : null;
const mock = {
- time: '16h44',
+ time: '12h15',
status: "à l'heure",
- trainType: 'Fluo',
- trainNumber: '891811',
- destinationBig: 'Seurre',
- destinationStation: '',
+ trainType: 'TER',
+ trainNumber: '15622',
+ destination: 'Perpignan',
stops: [
- 'Ouges','Saulon','Longecourt','Aiserey','Brazey-en-Plaine','Saint-Jean-de-Losne','Chaugey','Pagny','Seurre','Mervans','Louhans','Saint-Amour','Bourg-en-Bresse'
- ],
+ 'Banyuls-Sur-Mer',
+ 'Port-Vendres',
+ 'Colliure',
+ 'Argelès-Sur-Mer',
+ 'Elne',
+ 'Perpignan'
+ ]
};
const [selectedHoraire, setSelectedHoraire] = useState(null);
- const [loadingHoraire, setLoadingHoraire] = useState(false);
+ const [, setLoadingHoraire] = useState(false);
const [, setHoraireError] = useState(null);
- const [, setCandidatesCount] = useState(0);
- const [, setStationType] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date());
+ const [idlePhase, setIdlePhase] = useState(0); // 0 = Attentifs (20s), 1 = Quai (20s), 2 = Information (10s)
+
+ const pageParam = (searchParams.get('page') || searchParams.get('mode') || '').toLowerCase();
+ const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true';
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
useAnnonceSonoreRealtime(resolvedGareId, 'ClassiquesArriveesQuais', quaiParam);
useAutoAnnonce(selectedHoraire ? [selectedHoraire] : [], currentTime, true);
- const [alternateDelayPhase, setAlternateDelayPhase] = useState(true);
- const [alternateCancelPhase, setAlternateCancelPhase] = useState(true);
useEffect(() => {
const id = setInterval(() => setCurrentTime(new Date()), 1000);
return () => clearInterval(id);
}, []);
+ // Alternance par défaut (20s Attentifs -> 20s Quai -> 10s Information) si aucun train n'est prévu sur le quai
useEffect(() => {
- const interval = setInterval(() => {
- setAlternateDelayPhase(prev => !prev);
- }, 5000);
- return () => clearInterval(interval);
- }, []);
+ if (selectedHoraire) return;
+ let duration = 20000;
+ if (idlePhase === 2) duration = 10000;
+ else duration = 20000;
- useEffect(() => {
- const interval = setInterval(() => {
- setAlternateCancelPhase(prev => !prev);
- }, 5000);
- return () => clearInterval(interval);
- }, []);
-
- const [perturbations, setPerturbations] = useState([]);
- const [, setTravaux] = useState([]);
-
- const circulatesOn = useCallback((h, d) => {
- if (!h || !d) return false;
- const pad = (n) => String(n).padStart(2, '0');
- const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
- const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
- const targetDay = d.getDay();
-
- if (Array.isArray(h.dates) && h.dates.length > 0) {
- return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
- }
-
- const customDaysRaw = h.jours_personnalises;
- if (customDaysRaw) {
- let customDays = [];
- if (Array.isArray(customDaysRaw)) {
- customDays = customDaysRaw;
- } else if (typeof customDaysRaw === 'string') {
- try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
- }
- if (Array.isArray(customDays) && customDays.length > 0) {
- const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
- if (match) return Boolean(match.circule);
- }
- }
-
- const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
- const colName = `circule_${dayNames[targetDay]}`;
- if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
- const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
- const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
- if (db && targetDateHyphen < db) return false;
- if (df && targetDateHyphen > df) return false;
- return Boolean(h[colName]);
- }
-
- if (Array.isArray(h.days) && h.days.length > 0) {
- if (!h.days.includes(targetDay)) return false;
- if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
- if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
- return true;
- }
-
- const singleDate = h.date_depart || h.date || h.service_date;
- if (singleDate) {
- const sStr = String(singleDate).slice(0, 10);
- return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
- }
-
- return true;
- }, []);
+ const timer = setTimeout(() => {
+ setIdlePhase(prev => (prev + 1) % 3);
+ }, duration);
+ return () => clearTimeout(timer);
+ }, [idlePhase, selectedHoraire]);
const normalizeName = useCallback((s) => {
if (!s) return '';
- try { return String(s).normalize('NFD').replace(/\p{M}/gu,'').toLowerCase().trim().replace(/[-_]+/g,' ').replace(/\s+/g,' '); } catch (e) { return String(s).toLowerCase().trim(); }
+ try {
+ return String(s)
+ .normalize('NFD')
+ .replace(/\p{M}/gu, '')
+ .toLowerCase()
+ .trim()
+ .replace(/[-_]+/g, ' ')
+ .replace(/\s+/g, ' ');
+ } catch (e) {
+ return String(s).toLowerCase().trim();
+ }
+ }, []);
+
+ const cleanPlatform = useCallback((p) => {
+ if (!p) return '';
+ return String(p)
+ .toLowerCase()
+ .replace(/^(?:voie|quai|track|platform)\s*/i, '')
+ .replace(/\s+/g, '')
+ .trim();
}, []);
const parseTimeHM = useCallback((s) => {
@@ -152,364 +198,129 @@ export default function ClassiquesArriveesQuais() {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
- const hh = String(tryDate.getHours() % 24).padStart(2,'0');
- const mm = String(tryDate.getMinutes()).padStart(2,'0');
+ const hh = String(tryDate.getHours() % 24).padStart(2, '0');
+ const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) {
const h = parseInt(m[1], 10) % 24;
- return `${String(h).padStart(2,'0')}h${m[2]}`;
+ return `${String(h).padStart(2, '0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) {
const h = parseInt(m2[1], 10) % 24;
- return `${String(h).padStart(2,'0')}h${m2[2]}`;
+ return `${String(h).padStart(2, '0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
- return `${String(h).padStart(2,'0')}h${m3[2]}`;
+ return `${String(h).padStart(2, '0')}h${m3[2]}`;
}
- return v.slice(0,5).replace(':','h');
+ return v.slice(0, 5).replace(':', 'h');
}
- return String(v).slice(0,5).replace(':','h');
+ return String(v).slice(0, 5).replace(':', 'h');
}, []);
- const formatTimeFromHM = useCallback((totalMinutes) => {
- if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return '';
- const m = ((totalMinutes % 1440) + 1440) % 1440;
- const hh = String(Math.floor(m / 60)).padStart(2, '0');
- const mm = String(m % 60).padStart(2, '0');
- return `${hh}h${mm}`;
+ const circulatesOn = useCallback((h, d) => {
+ if (!h || !d) return false;
+ const pad = (n) => String(n).padStart(2, '0');
+ const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
+ const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
+ const targetDay = d.getDay();
+
+ if (Array.isArray(h.dates) && h.dates.length > 0) {
+ return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
+ }
+
+ const customDaysRaw = h.jours_personnalises;
+ if (customDaysRaw) {
+ let customDays = [];
+ if (Array.isArray(customDaysRaw)) customDays = customDaysRaw;
+ else if (typeof customDaysRaw === 'string') {
+ try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
+ }
+ if (Array.isArray(customDays) && customDays.length > 0) {
+ const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
+ if (match) return Boolean(match.circule);
+ }
+ }
+
+ const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
+ const colName = `circule_${dayNames[targetDay]}`;
+ if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
+ const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
+ const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
+ if (db && targetDateHyphen < db) return false;
+ if (df && targetDateHyphen > df) return false;
+ return Boolean(h[colName]);
+ }
+
+ if (Array.isArray(h.days) && h.days.length > 0) {
+ if (!h.days.includes(targetDay)) return false;
+ if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
+ if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
+ return true;
+ }
+
+ const singleDate = h.date_arrivee || h.date || h.service_date;
+ if (singleDate) {
+ const sStr = String(singleDate).slice(0, 10);
+ return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
+ }
+
+ return true;
}, []);
-
-
-
-
- const getStops = (h) => {
+ const getStops = useCallback((h) => {
if (!h) return [];
if (Array.isArray(h.stops) && h.stops.length) return h.stops;
if (Array.isArray(h.gares_desservies) && h.gares_desservies.length) return h.gares_desservies;
if (Array.isArray(h.garesDesservies) && h.garesDesservies.length) return h.garesDesservies;
if (Array.isArray(h.arrets) && h.arrets.length) return h.arrets;
- const candidates = ['stops','gares_desservies','garesDesservies','gares_desservies_json','arrets','gares_desservies_json'];
+ const candidates = ['stops', 'gares_desservies', 'garesDesservies', 'gares_desservies_json', 'arrets'];
for (const key of candidates) {
const raw = h[key];
if (!raw) continue;
if (Array.isArray(raw)) return raw;
if (typeof raw === 'string') {
- try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) return parsed; } catch (e) {}
+ try {
+ const parsed = JSON.parse(raw);
+ if (Array.isArray(parsed)) return parsed;
+ } catch (e) {}
}
}
return [];
- };
-
- const safeParseArrets = (p) => {
- const raw = p && (p.perturbations_arrets || p.arrets || p.arrets_perturbations);
- if (!raw) return [];
- if (Array.isArray(raw)) return raw;
- try {
- const parsed = JSON.parse(raw);
- if (parsed && typeof parsed === 'object') {
- if (Array.isArray(parsed.arrets)) return parsed.arrets;
- return parsed;
- }
- return [];
- } catch (e) { return []; }
- };
-
- const parsePerturbArretsObject = useCallback((p) => {
- const raw = p && (p.perturbations_arrets || p.arrets || p.arrets_perturbations);
- if (!raw) return { arrets: [], jours: null };
- if (typeof raw === 'object') {
- if (Array.isArray(raw.arrets)) return { arrets: raw.arrets, jours: raw.jours_perturbation || raw.jours || null };
- if (Array.isArray(raw)) return { arrets: raw, jours: null };
- return { arrets: [], jours: null };
- }
- try {
- const parsed = JSON.parse(raw);
- if (parsed && typeof parsed === 'object') {
- return { arrets: Array.isArray(parsed.arrets) ? parsed.arrets : (Array.isArray(parsed) ? parsed : []), jours: parsed.jours_perturbation || parsed.jours || null };
- }
- return { arrets: [], jours: null };
- } catch (e) {
- return { arrets: [], jours: null };
- }
}, []);
- const getPerturbationJours = useCallback((p) => {
- if (!p) return null;
- const candidate = p.jours_perturbation || p.jours || null;
- const result = new Set();
- const pushDate = (d) => {
- if (!d) return;
- try { const dd = new Date(d); if (!isNaN(dd.getTime())) result.add(dd.toISOString().slice(0,10)); } catch(e){}
- };
- if (candidate) {
- if (Array.isArray(candidate)) candidate.forEach(item => pushDate(item.date || item));
- else if (typeof candidate === 'string') {
- try { const parsed = JSON.parse(candidate); if (Array.isArray(parsed)) parsed.forEach(it => pushDate(it.date || it)); }
- catch (e) { pushDate(candidate); }
- } else if (typeof candidate === 'object') { if (Array.isArray(candidate)) candidate.forEach(it => pushDate(it.date || it)); else pushDate(candidate.date || null); }
- }
- try {
- const parsedObj = parsePerturbArretsObject(p);
- const jp = parsedObj.jours;
- if (jp) {
- if (Array.isArray(jp)) jp.forEach(it => pushDate(it.date || it));
- else if (typeof jp === 'string') {
- try { const parsed = JSON.parse(jp); if (Array.isArray(parsed)) parsed.forEach(it => pushDate(it.date || it)); } catch (e) { pushDate(jp); }
- } else if (typeof jp === 'object') pushDate(jp.date || null);
- }
- } catch (e) {}
- return result.size ? result : null;
- }, [parsePerturbArretsObject]);
+ // Chargement du train à l'arrivée
+ const loadNextForQuai = useCallback(async () => {
+ setLoadingHoraire(true);
+ setHoraireError(null);
- const parseDelayMinutesFromArret = useCallback((arret) => {
- if (!arret) return null;
- const keys = ['retard_minutes','retard','delay_minutes','delay','retard_min'];
- for (const k of keys) {
- if (typeof arret[k] !== 'undefined' && arret[k] !== null && arret[k] !== '') {
- const n = Number(arret[k]); if (!isNaN(n)) return Math.round(n);
- }
- }
- return null;
- }, []);
+ const source = getSourceFromPath(window.location.pathname);
+ const targetQuaiClean = cleanPlatform(quaiParam);
- const buildPerturbMap = useCallback((list = []) => {
- const idx = { byHoraire: {}, byNumber: {}, byStopId: {}, byStopName: {}, global: [] };
- (list || []).forEach(p => {
- const horaireId = p.horaire_id || p.horaire || p.horaireId || p.horaireID;
- if (horaireId != null) { const k = String(horaireId); idx.byHoraire[k] = idx.byHoraire[k] || []; idx.byHoraire[k].push(p); }
- const number = p.number || p.train_number || p.train || p.numero || p.numero_train;
- if (number != null) { const kn = String(number); idx.byNumber[kn] = idx.byNumber[kn] || []; idx.byNumber[kn].push(p); }
- const arrets = safeParseArrets(p);
- if (Array.isArray(arrets) && arrets.length) {
- arrets.forEach(a => {
- const stopId = a.gare_id || a.gareId || a.id || a.station_id || a.stationId || a.uic || a.code_uic || a.gare_uic || a.code;
- const stopName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom;
- if (stopId != null) { const ks = String(stopId); idx.byStopId[ks] = idx.byStopId[ks] || []; idx.byStopId[ks].push({ perturbation: p, arret: a }); }
- if (stopName) { const key = normalizeName(stopName); idx.byStopName[key] = idx.byStopName[key] || []; idx.byStopName[key].push({ perturbation: p, arret: a }); }
+ let chosenTrain = null;
+
+ // 1. Tenter la source SNCF si applicable
+ if (source === 'sncf' || !supabase) {
+ try {
+ const { data: sncfData } = await getSncfTimetable({
+ gareName: gareDisplay || gareParam,
+ mode: 'arrivees',
+ quai: quaiParam
});
- }
- if ((horaireId == null && number == null && (!Array.isArray(arrets) || !arrets.length))) { idx.global.push(p); }
- });
- return idx;
- }, [normalizeName]);
-
- const findPerturbsForHoraireAndStop = useCallback((horaire, stationId, stationName, idx) => {
- const res = [];
- const map = idx || buildPerturbMap(perturbations || []);
- const hId = horaire && (horaire.id || horaire.horaire_id || horaire.numero || horaire.numero_train);
- const number = horaire && (horaire.numero_train || horaire.number || horaire.numero || horaire.train_number);
- if (hId != null) { const kh = String(hId); if (map.byHoraire && map.byHoraire[kh]) res.push(...map.byHoraire[kh]); }
- if (number != null) { const kn = String(number); if (map.byNumber && map.byNumber[kn]) res.push(...map.byNumber[kn]); }
- if (stationId != null) { const ks = String(stationId); if (map.byStopId && map.byStopId[ks]) map.byStopId[ks].forEach(x => res.push(x.perturbation)); }
- if (stationName && map.byStopName) {
- const key = normalizeName(stationName);
- if (map.byStopName[key]) map.byStopName[key].forEach(x => res.push(x.perturbation));
- try {
- const needle = key;
- for (const existingKey of Object.keys(map.byStopName || {})) {
- if (existingKey === needle) continue;
- if (existingKey.includes(needle) || needle.includes(existingKey)) { map.byStopName[existingKey].forEach(x => res.push(x.perturbation)); }
- }
- } catch (e) {}
- }
- return Array.from(new Set(res));
- }, [buildPerturbMap, perturbations, normalizeName]);
-
- const computeStatusForHoraireAtStop = useCallback((horaire, stationId, stationName, idx, serviceDate = null) => {
- const found = findPerturbsForHoraireAndStop(horaire, stationId, stationName, idx) || [];
- const toYMD = (v) => { if (!v) return null; const d = new Date(v); if (isNaN(d.getTime())) return null; return d.toISOString().slice(0,10); };
- let horaireYmd = toYMD(horaire && (horaire._date || horaire.date || horaire.date_service || horaire.service_date));
- if (!horaireYmd && serviceDate) horaireYmd = serviceDate;
- let maxDelay = null;
- for (const p of found) {
- try {
- if (horaireYmd) {
- const pDate = toYMD(p.date || p.date_service);
- const db = toYMD(p.date_debut || p.start_date);
- const df = toYMD(p.date_fin || p.end_date);
- if (pDate && pDate !== horaireYmd) continue;
- if (db && df && !(horaireYmd >= db && horaireYmd <= df)) continue;
- }
- } catch (e) {}
- try {
- const joursSet = getPerturbationJours(p);
- if (joursSet && horaireYmd) { if (!joursSet.has(horaireYmd)) continue; }
- } catch (e) {}
- const parsedObj = parsePerturbArretsObject(p) || { arrets: [], jours: null };
- const arrets = parsedObj.arrets || [];
- let matched = false;
- for (const a of arrets) {
- const aId = a.gare_id || a.gareId || a.id || a.station_id || a.stationId || a.uic || a.code;
- const aName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom;
- const na = normalizeName(aName || '');
- const ns = normalizeName(stationName || '');
- const nameMatches = stationName && (na === ns || na.includes(ns) || ns.includes(na));
- if ((stationId != null && String(aId) === String(stationId)) || nameMatches) {
- matched = true;
- const t = (a.type_perturbation || a.type || '').toString().toLowerCase();
- if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) return { label: 'supprimé', detail: '', delayMinutes: null };
- if (t.includes('retard') || t.includes('delay') || /retard/.test(t)) {
- const d = parseDelayMinutesFromArret(a) || parseDelayMinutesFromArret(p) || 0;
- if (maxDelay == null || d > maxDelay) maxDelay = d;
+ if (sncfData && sncfData.length > 0) {
+ let matchingTrains = sncfData;
+ if (targetQuaiClean) {
+ matchingTrains = sncfData.filter(t => cleanPlatform(t.quai || t.voie || t.quai_reel || t._displayPlatform) === targetQuaiClean);
}
- }
- }
- if (!matched) {
- const ref = p.horaire_id || p.horaire || p.horaireId || p.numero_train || p.train_number || p.numero;
- const numberRef = p.number || p.train_number || p.numero || p.numero_train;
- const appliesByRef = ref && (String(ref) === String(horaire.id) || String(ref) === String(horaire.numero_train) || String(ref) === String(horaire.numero));
- const appliesByNumber = numberRef && (String(numberRef) === String(horaire.numero_train) || String(numberRef) === String(horaire.numero) || String(numberRef) === String(horaire.number));
- const isGlobal = (!Array.isArray(arrets) || arrets.length === 0) && !ref && !numberRef;
- if (appliesByRef || appliesByNumber || isGlobal) {
- const t = (p.type_perturbation || p.type || '').toString().toLowerCase();
- if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) return { label: 'supprimé', detail: '', delayMinutes: null };
- if (t.includes('retard') || t.includes('delay') || /retard/.test(t)) {
- const d = parseDelayMinutesFromArret(p) || 0;
- if (maxDelay == null || d > maxDelay) maxDelay = d;
- }
- }
- }
- }
- if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay };
- return { label: "à l'heure", detail: '', delayMinutes: null };
- }, [findPerturbsForHoraireAndStop, parseDelayMinutesFromArret, getPerturbationJours, normalizeName, parsePerturbArretsObject]);
-
- const perturbMap = React.useMemo(() => buildPerturbMap(perturbations || []), [perturbations, buildPerturbMap]);
- const serviceDate = React.useMemo(() => {
- const todayYmd = new Date().toLocaleDateString('en-CA');
- const tomorrow = new Date();
- tomorrow.setDate(tomorrow.getDate() + 1);
- const tomorrowYmd = tomorrow.toLocaleDateString('en-CA');
- return selectedHoraire && selectedHoraire.isTomorrow ? tomorrowYmd : todayYmd;
- }, [selectedHoraire]);
-
- useEffect(() => {
- let mounted = true;
- let inFlight = false;
- const fetchPerturb = async () => {
- if (!mounted) return;
- if (inFlight) return;
- inFlight = true;
- try {
- if (!supabase) return;
- const [{ data: perturbData, error: pError }, { data: travauxData, error: tError }] = await Promise.all([
- supabase.from('perturbations').select('*'),
- supabase.from('travaux').select('*')
- ]);
- if (!mounted) return;
- if (pError || tError) {
- } else {
- const todayYmd = new Date().toLocaleDateString('en-CA');
- const tomorrow = new Date();
- tomorrow.setDate(tomorrow.getDate() + 1);
- const tomorrowYmd = tomorrow.toLocaleDateString('en-CA');
-
- const filteredPerturb = (perturbData || []).filter(p => {
- try {
- const toYMD = (v) => { if (!v) return null; const d = new Date(v); if (isNaN(d.getTime())) return null; return d.toISOString().slice(0,10); };
- const pDate = toYMD(p.date || p.date_service);
- const db = toYMD(p.date_debut || p.start_date);
- const df = toYMD(p.date_fin || p.end_date);
-
- const appliesToToday = (!pDate || pDate === todayYmd) && (!db || !df || (todayYmd >= db && todayYmd <= df));
- const appliesToTomorrow = (!pDate || pDate === tomorrowYmd) && (!db || !df || (tomorrowYmd >= db && tomorrowYmd <= df));
-
- if (!appliesToToday && !appliesToTomorrow) return false;
-
- try {
- const joursSet = getPerturbationJours(p);
- if (joursSet) {
- const hasToday = joursSet.has(todayYmd);
- const hasTomorrow = joursSet.has(tomorrowYmd);
- if (!hasToday && !hasTomorrow) return false;
- }
- } catch (e) {}
- return true;
- } catch (e) { return true; }
- });
- setPerturbations(filteredPerturb || []);
- setTravaux(travauxData || []);
- }
- } catch (e) {
- } finally {
- inFlight = false;
- }
- };
- fetchPerturb();
- const id = setInterval(fetchPerturb, 15000);
- return () => { mounted = false; clearInterval(id); };
- }, [getPerturbationJours]);
-
- useEffect(() => {
- if (!selectedHoraire || !selectedHoraire.horaire) return;
- const horaire = selectedHoraire.horaire;
- const stop = selectedHoraire.stop || null;
- const stationId = stop ? (stop.gare_id || stop.gareId || stop.id || stop.stop_id || stop.stopId) : null;
- const stationName = stop ? (typeof stop === 'string' ? stop : (stop.gare_nom || stop.nom || stop.name || stop.stop_name || '')) : null;
- try {
- const map = buildPerturbMap(perturbations || []);
-
- const todayYmd = new Date().toLocaleDateString('en-CA');
- const tomorrow = new Date();
- tomorrow.setDate(tomorrow.getDate() + 1);
- const tomorrowYmd = tomorrow.toLocaleDateString('en-CA');
- const serviceDate = selectedHoraire.isTomorrow ? tomorrowYmd : todayYmd;
-
- const status = computeStatusForHoraireAtStop(horaire, stationId, stationName, map, serviceDate);
- const originalBase = parseTimeHM(selectedHoraire.rawTime || horaire.heure_arrivee || horaire.time || horaire.heure);
- const displayTimeOriginal = (typeof originalBase === 'number') ? formatTimeFromHM(originalBase) : (selectedHoraire.rawTime ? formatTimeNoSeconds(selectedHoraire.rawTime) : (horaire.heure_arrivee || horaire.time || horaire.heure ? formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure) : null));
- let updatedTime = null;
- if (status && typeof status.delayMinutes === 'number' && !Number.isNaN(status.delayMinutes) && typeof originalBase === 'number') {
- updatedTime = formatTimeFromHM(originalBase + status.delayMinutes);
- }
-
- // Guard to prevent infinite render loop: only set selectedHoraire if status or time actually changed
- const statusChanged = !horaire._status || horaire._status.label !== status.label || horaire._status.delayMinutes !== status.delayMinutes;
- const timeChanged = selectedHoraire._displayTime !== displayTimeOriginal || selectedHoraire._updatedTime !== updatedTime;
-
- if (statusChanged || timeChanged) {
- setSelectedHoraire(prev => ({
- ...(prev||{}),
- horaire: { ...(prev && prev.horaire ? prev.horaire : {}), _status: status },
- _displayTime: displayTimeOriginal,
- _updatedTime: updatedTime
- }));
- }
- } catch (e) {}
- }, [selectedHoraire, perturbations, buildPerturbMap, computeStatusForHoraireAtStop, parseTimeHM, formatTimeFromHM, formatTimeNoSeconds]);
-
- useEffect(() => {
- let mounted = true;
- async function loadNextForQuai() {
- setLoadingHoraire(true);
- setHoraireError(null);
- setSelectedHoraire(null);
-
- const source = getSourceFromPath(window.location.pathname);
- if (source === 'sncf') {
- try {
- const { data: sncfData, error: sncfErr } = await getSncfTimetable({
- gareName: gareDisplayParam || gareParam,
- mode: 'arrivees',
- quai: quaiParam
- });
- if (!mounted) return;
- if (sncfErr) {
- setHoraireError(sncfErr);
- } else if (sncfData && sncfData.length > 0) {
- const train = sncfData[0];
- const now = new Date();
- const nowHM = now.getHours() * 60 + now.getMinutes();
- const arrHM = parseTimeHM(train.heure_arrivee || train.heure_depart);
-
- const chosen = {
+ const train = matchingTrains[0];
+ if (train) {
+ const arrHM = parseTimeHM(train.heure_arrivee || train.time || train.heure);
+ chosenTrain = {
horaire: {
...train,
_status: train._status || {
@@ -520,440 +331,294 @@ export default function ClassiquesArriveesQuais() {
}
},
stop: {
- gare_nom: train.gare_arrivee_nom || gareDisplayParam || gareParam,
- heure_arrivee: train.heure_arrivee || train.heure_depart,
- quai: train.quai || quaiParam,
- quai_reel: train.quai || quaiParam
+ gare_nom: train.gare_arrivee_nom || gareDisplay || gareParam,
+ heure_arrivee: train.heure_arrivee || train.time || train.heure,
+ quai: train.quai || train.voie || quaiParam
},
- arrHM: arrHM != null ? arrHM : nowHM,
- rawTime: train.heure_arrivee || train.heure_depart,
- circulatesToday: true,
- circulatesTomorrow: false,
- isTomorrow: false
+ arrHM: arrHM != null ? arrHM : 0,
+ rawTime: train.heure_arrivee || train.time || train.heure
};
- setSelectedHoraire(chosen);
- setCandidatesCount(sncfData.length);
- setStationType('interurbaine');
}
- } catch (e) {
- if (mounted) setHoraireError(e.message || String(e));
- } finally {
- if (mounted) setLoadingHoraire(false);
}
- return;
- }
-
- if (!supabase) {
- setHoraireError('Supabase non initialisé');
- setLoadingHoraire(false);
- return;
- }
- try {
- let fetchedType = null;
- try {
- let fetchedId = gareIdParam || null;
- if (gareIdParam) {
- const { data: g } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).single();
- if (g) {
- fetchedType = g.type;
- fetchedId = g.id;
- }
- } else if (gareDisplayParam) {
- const { data: gdata } = await supabase.from('gares').select('id,type,nom').ilike('nom', `%${gareDisplayParam}%`);
- if (gdata && gdata.length) {
- const exactMatch = gdata.find(g => {
- const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
- const n2 = String(gareDisplayParam || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
- return n1 === n2;
- });
- const chosen = exactMatch || gdata[0];
- fetchedType = chosen ? chosen.type : null;
- fetchedId = chosen ? chosen.id : null;
- }
- }
- if (mounted) {
- setStationType(fetchedType);
- setResolvedGareId(fetchedId);
- }
- } catch (e) {}
-
- const { data: all, error } = await fetchAllRows(supabase.from('horaires').select('*'));
- if (error) {
- if (mounted) setHoraireError(error.message || String(error));
- setLoadingHoraire(false);
- return;
- }
- if (!mounted) return;
- if (!Array.isArray(all) || all.length === 0) {
- setLoadingHoraire(false);
- return;
- }
-
- const normalizedTarget = gareDisplayParam ? normalizeName(gareDisplayParam) : null;
- const targetQuai = quaiParam ? String(quaiParam).toLowerCase().replace(/\s+/g,'') : null;
-
- const now = new Date();
- const tomorrow = new Date(now);
- tomorrow.setDate(now.getDate() + 1);
- const nowHM = now.getHours()*60 + now.getMinutes();
-
- const candidates = [];
-
- for (const h of all) {
- if (!h) continue;
-
- const circulatesToday = circulatesOn(h, now);
- const circulatesTomorrow = circulatesOn(h, tomorrow);
- if (!circulatesToday && !circulatesTomorrow) continue;
-
- const stops = getStops(h);
-
- let maxIdx = null;
- if (Array.isArray(stops) && stops.length) {
- for (const st of stops) {
- if (typeof st.index !== 'undefined' && st.index !== null) {
- const n = Number(st.index);
- if (!Number.isNaN(n)) maxIdx = maxIdx == null ? n : Math.max(maxIdx, n);
- }
- }
- }
-
- let stopMatch = null;
- if (normalizedTarget) {
- const departNameNorm = normalizeName(h.gare_depart_nom || h.gare_depart || h.origin_name || '');
- if (departNameNorm && (departNameNorm === normalizedTarget || departNameNorm.includes(normalizedTarget) || normalizedTarget.includes(departNameNorm))) {
- // Target is the origin of this train, which means it doesn't arrive here, skip.
- continue;
- } else {
- const destNameNorm = normalizeName(h.gare_arrivee_nom || h.gare_arrivee || '');
- if (destNameNorm && (destNameNorm === normalizedTarget || destNameNorm.includes(normalizedTarget) || normalizedTarget.includes(destNameNorm))) {
- let finalStop = { heure_arrivee: h.heure_arrivee || h.time || h.heure, gare_nom: h.gare_arrivee_nom || h.gare_arrivee };
- if (Array.isArray(stops) && stops.length) {
- for (const s of stops) {
- const rawName = (typeof s === 'string') ? s : (s.gare_nom || s.nom || s.name || s.stop_name || s.station || '');
- const nm = normalizeName(rawName);
- if (!nm) continue;
- if (nm === normalizedTarget || nm.includes(normalizedTarget) || normalizedTarget.includes(nm)) {
- if (typeof s === 'object') {
- if (!s.heure_arrivee && finalStop.heure_arrivee) s.heure_arrivee = finalStop.heure_arrivee;
- finalStop = s;
- } else {
- finalStop.gare_nom = s;
- }
- break;
- }
- }
- }
- stopMatch = finalStop;
- }
- }
- }
-
- if (!stopMatch && gareIdParam) {
- const gid = String(gareIdParam);
- const arriveeId = h.gare_arrivee_id || h.gare_id || h.destination_id || null;
- if (arriveeId != null && String(arriveeId) === gid) {
- stopMatch = { heure_arrivee: h.heure_arrivee || h.time || h.heure, gare_id: arriveeId, gare_nom: h.gare_arrivee_nom || h.gare_arrivee };
- }
- if (!stopMatch && Array.isArray(stops) && stops.length) {
- for (const s of stops) {
- const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
- if (sid != null && String(sid) === gid) { stopMatch = s; break; }
- }
- }
- }
-
- if (!stopMatch && normalizedTarget && Array.isArray(stops) && stops.length) {
- for (const s of stops) {
- const rawName = (typeof s === 'string') ? s : (s.gare_nom || s.nom || s.name || s.stop_name || s.stopName || s.station || s.station_name || '');
- const nm = normalizeName(rawName);
- if (!nm) continue;
- if (nm === normalizedTarget || nm.includes(normalizedTarget) || normalizedTarget.includes(nm)) {
- const isOrigin = Boolean((typeof s === 'object') && (s.isFirst || s.is_first || s.isOrigin || (typeof s.index !== 'undefined' && Number(s.index) === 0)));
- if (isOrigin) continue;
- stopMatch = s;
- break;
- }
- }
- }
- if (!stopMatch) continue;
-
- const platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.quay)) || h.quai_reel || h.platform || h.quai || '';
- const normPlatform = String(platform || '').toLowerCase().replace(/\s+/g,'');
-
- if (targetQuai) {
- if (!normPlatform) continue;
- if (!(normPlatform === targetQuai || normPlatform.includes(targetQuai) || targetQuai.includes(normPlatform))) continue;
- }
-
- const stopIsObject = stopMatch && typeof stopMatch === 'object';
- const hasArrival = stopIsObject && (
- typeof stopMatch.heure_arrivee !== 'undefined' || typeof stopMatch.arrival_time !== 'undefined' || typeof stopMatch.arrival !== 'undefined' || typeof stopMatch.time !== 'undefined' || typeof stopMatch.arrival_time !== 'undefined' || typeof stopMatch.horaire_time !== 'undefined'
- );
- const hasDepartureOnly = stopIsObject && !hasArrival && (
- typeof stopMatch.heure_depart !== 'undefined' || typeof stopMatch.departure_time !== 'undefined' || typeof stopMatch.departure !== 'undefined'
- );
- if (stopIsObject && hasDepartureOnly) continue;
-
- const rawTime = (hasArrival ? (stopMatch.heure_arrivee || stopMatch.time || stopMatch.heure || stopMatch.arrival_time || stopMatch.arrival) : (h.heure_arrivee || h.time || h.heure || h.arrival_time || h.arrival));
- const arrHM = parseTimeHM(rawTime);
- if (arrHM == null) continue;
-
- let minutesUntil = arrHM - nowHM;
- if (minutesUntil < 0) minutesUntil = arrHM + 24*60 - nowHM;
-
- candidates.push({ horaire: h, stop: stopMatch, arrHM, minutesUntil, rawTime, circulatesToday, circulatesTomorrow });
- }
-
- if (!candidates.length) {
- setCandidatesCount(0);
- setLoadingHoraire(false);
- setSelectedHoraire(null);
- return;
- }
-
- const todayCandidates = candidates.filter(c => c.circulatesToday && c.arrHM >= nowHM - 1).sort((a,b) => a.arrHM - b.arrHM);
- const tomorrowCandidates = candidates.filter(c => c.circulatesTomorrow && (c.arrHM < nowHM - 1 || !c.circulatesToday)).sort((a,b) => a.arrHM - b.arrHM);
-
- let chosen = null;
- if (todayCandidates.length > 0) {
- chosen = { ...todayCandidates[0], isTomorrow: false };
- } else if (tomorrowCandidates.length > 0) {
- chosen = { ...tomorrowCandidates[0], isTomorrow: true };
- }
-
- if (mounted) {
- setCandidatesCount(candidates.length);
- setSelectedHoraire(chosen || null);
- }
- } catch (err) {
- console.error('Erreur supabase quai', err);
- if (mounted) setHoraireError(err.message || String(err));
- } finally {
- if (mounted) setLoadingHoraire(false);
+ } catch (e) {
+ console.warn('Notice getSncfTimetable arrivees:', e);
}
}
- loadNextForQuai();
- return () => { mounted = false; };
- }, [gareDisplayParam, gareParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]);
+ // 2. Si pas trouvé via SNCF, ou si mode Supabase actif, interroger Supabase
+ if (!chosenTrain && supabase) {
+ try {
+ if (gareIdParam) {
+ setResolvedGareId(gareIdParam);
+ } else if (gareDisplay) {
+ const { data: gdata } = await supabase.from('gares').select('id,nom').ilike('nom', `%${gareDisplay}%`);
+ if (gdata && gdata.length) {
+ setResolvedGareId(gdata[0].id);
+ }
+ }
- // Rail is handled purely in CSS for better performance and alignment
- const showPlatform = true;
+ const { data: all, error } = await fetchAllRows(supabase.from('horaires').select('*'));
+ if (!error && Array.isArray(all) && all.length > 0) {
+ const normalizedTarget = gareDisplay ? normalizeName(gareDisplay) : null;
+ const now = new Date();
+ const tomorrow = new Date(now);
+ tomorrow.setDate(now.getDate() + 1);
+ const nowHM = now.getHours() * 60 + now.getMinutes();
- if (!loadingHoraire && !selectedHoraire) {
- return (
-
-
Aucune Arrivée Prévue
-
-
- );
- }
+ const candidates = [];
- return (
-
- {/* LEFT PANEL: Lilac/Light Grey Background */}
-
- {/* SNCF Logo */}
-
+ for (const h of all) {
+ if (!h) continue;
+ const circulatesToday = circulatesOn(h, now);
+ const circulatesTomorrow = circulatesOn(h, tomorrow);
-
- {(() => {
- const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
- const statusObj = horaire && horaire._status ? horaire._status : null;
- const isSuppressed = statusObj && statusObj.label === 'supprimé';
- const isDelayed = statusObj && typeof statusObj.delayMinutes === 'number';
- const delayMinutes = statusObj ? statusObj.delayMinutes : null;
+ const stops = getStops(h);
+ let stopMatch = null;
- let timeText = horaire ? formatTimeNoSeconds(selectedHoraire.rawTime || horaire.heure_arrivee || horaire.time || horaire.heure) : mock.time;
- let statusText = horaire ? (horaire.status || (statusObj && statusObj.label) || mock.status) : mock.status;
-
- if (showPlatform) {
- if (isSuppressed) {
- if (!alternateCancelPhase) {
- timeText = 'supprimé';
- statusText = '';
- } else {
- statusText = 'supprimé';
- }
- } else if (isDelayed) {
- if (!alternateDelayPhase) {
- timeText = `retardé +${delayMinutes} min`;
- statusText = '';
- } else {
- statusText = `retardé +${delayMinutes} min`;
+ if (normalizedTarget) {
+ const departNameNorm = normalizeName(h.gare_depart_nom || h.gare_depart || '');
+ if (departNameNorm && departNameNorm === normalizedTarget) {
+ // Target is the departure origin -> not an arrival here
+ continue;
+ }
+ const arriveeName = normalizeName(h.gare_arrivee_nom || h.gare_arrivee || '');
+ if (arriveeName && arriveeName === normalizedTarget) {
+ stopMatch = {
+ heure_arrivee: h.heure_arrivee || h.time || h.heure,
+ gare_nom: h.gare_arrivee_nom || h.gare_arrivee,
+ quai_reel: h.quai_reel || h.voie || h.quai
+ };
+ }
+ if (!stopMatch && Array.isArray(stops)) {
+ for (const s of stops) {
+ const sName = normalizeName(typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''));
+ if (sName === normalizedTarget) {
+ stopMatch = s;
+ break;
+ }
}
}
} else {
- timeText = '--h--';
- statusText = '';
+ stopMatch = {
+ heure_arrivee: h.heure_arrivee || h.time || h.heure,
+ gare_nom: h.gare_arrivee_nom || h.gare_arrivee,
+ quai_reel: h.quai_reel || h.voie || h.quai
+ };
}
- const isTimeSuppressed = isSuppressed && !alternateCancelPhase;
- const isTimeDelayed = isDelayed && !alternateDelayPhase;
+ if (!stopMatch) continue;
- const isLongText = timeText.length > 5;
- let timeClass = '';
- if (isLongText) timeClass += ' aff-quais-arrivees-redesign__time--long-text';
- if (isTimeSuppressed) timeClass += ' aff-quais-arrivees-redesign__time--suppressed';
- if (isTimeDelayed) timeClass += ' aff-quais-arrivees-redesign__time--delayed';
+ const platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.voie)) || h.quai_reel || h.voie || h.platform || h.quai || '';
+ const platClean = cleanPlatform(platform);
- let statusClass = '';
- if (isSuppressed) statusClass += ' aff-quais-arrivees-redesign__status--suppressed';
- if (isDelayed) statusClass += ' aff-quais-arrivees-redesign__status--delayed';
-
- return (
- <>
- {timeText}
- {statusText && {String(statusText).toLowerCase()} }
- >
- );
- })()}
-
-
-
- {(() => {
- const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
- return showPlatform ? (horaire ? (horaire.gare_depart_nom || horaire.gare_depart || horaire.origin_name || horaire.origin || '') : mock.destinationBig) : '--';
- })()}
-
-
-
- {(() => {
- const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
- const trainType = horaire ? (horaire.trainType || horaire.train_type || horaire.type_train || horaire.op || horaire.operator) : mock.trainType;
- const trainNumber = horaire ? (horaire.numero_train || horaire.numero || horaire.number || '') : mock.trainNumber;
- return showPlatform ? (
- <>
- {trainType}
- {trainNumber}
- >
- ) : '';
- })()}
-
-
- {/* Accessibility Icons */}
-
-
- {/* Watermark */}
-
arrivée
-
-
- {/* RIGHT PANEL: Green Background with Provenance Stops */}
-
- {showPlatform &&
gares de provenance :
}
-
- {(() => {
- if (!showPlatform) return null;
- const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
- let stops = horaire ? getStops(horaire) : [];
- if (!Array.isArray(stops) || stops.length === 0) {
- if (horaire && typeof horaire.gares_desservies === 'string') {
- try { const p = JSON.parse(horaire.gares_desservies); if (Array.isArray(p)) stops = p; } catch (e) { /* ignore */ }
+ if (targetQuaiClean) {
+ if (!platClean || platClean !== targetQuaiClean) {
+ continue;
}
}
- if (!Array.isArray(stops) || stops.length === 0) stops = [];
- const labeled = stops.map((stop, idx) => {
- let label = '';
- if (typeof stop === 'string') label = stop;
- else if (stop && typeof stop === 'object') {
- label = stop.gare_nom || stop.nom || stop.name || stop.stop_name || stop.stopName || stop.station_name || stop.station || stop.label || stop.stop || stop.name_gare || (stop.properties && (stop.properties.name || stop.properties.stop_name)) || '';
- if (!label && stop.stop && typeof stop.stop === 'object') {
- label = stop.stop.name || stop.stop.stop_name || '';
- }
- }
- label = String(label || '').trim();
- return { label, original: stop, idx };
- }).filter(it => it.label);
+ const rawTime = (stopMatch && typeof stopMatch === 'object' && (stopMatch.heure_arrivee || stopMatch.heure || stopMatch.time || stopMatch.heure_passage)) || h.heure_arrivee || h.time || h.heure;
+ const arrHM = parseTimeHM(rawTime);
+ if (arrHM == null) continue;
- if (labeled.length === 0) return null;
-
- let matchedIndex = -1;
- try {
- if (selectedHoraire && selectedHoraire.stop) {
- const target = selectedHoraire.stop;
- const targetName = (typeof target === 'string') ? normalizeName(target) : normalizeName((target.gare_nom || target.nom || target.name || target.stop_name || target.station || ''));
- const targetId = (typeof target === 'object') ? (target.gare_id || target.gareId || target.id || target.stop_id || target.stopId || null) : null;
- for (let i = 0; i < stops.length; i++) {
- const s = stops[i];
- if (targetId != null) {
- const sid = (s && typeof s === 'object') ? (s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null) : null;
- if (sid != null && String(sid) === String(targetId)) { matchedIndex = i; break; }
- }
- const name = (typeof s === 'string') ? normalizeName(s) : normalizeName((s && (s.gare_nom || s.nom || s.name || s.stop_name || s.station || '')) || '');
- if (name && targetName && name === targetName) { matchedIndex = i; break; }
- }
- }
- } catch (e) { /* ignore */ }
-
- let previousStops = labeled.filter(it => (matchedIndex >= 0) ? (it.idx <= matchedIndex) : true);
- previousStops = previousStops.sort((a,b) => a.idx - b.idx);
- if (previousStops.length > 1) {
- const collapsed = [];
- for (const it of previousStops) {
- const prev = collapsed.length ? String(collapsed[collapsed.length-1].label).trim().toLowerCase() : null;
- const cur = it.label ? String(it.label).trim().toLowerCase() : '';
- if (cur && prev && prev === cur) {
- continue;
- }
- collapsed.push(it);
- }
- previousStops = collapsed;
- }
- if (previousStops.length === 0) return null;
-
- return previousStops.map((item, mapIndex) => {
- const stop = item.original || {};
- const stopId = stop.gare_id || stop.gareId || stop.id || stop.stop_id || stop.stopId || null;
- const stopStatus = computeStatusForHoraireAtStop(horaire, stopId, item.label, perturbMap, serviceDate);
- const isStopSuppressed = stopStatus && stopStatus.label === 'supprimé';
- const isStopDelayed = stopStatus && typeof stopStatus.delayMinutes === 'number';
- const stopDelayMin = stopStatus ? stopStatus.delayMinutes : null;
-
- let stopTextClass = '';
- if (isStopSuppressed) stopTextClass += ' aff-quais-arrivees-redesign__stop-text--suppressed';
- if (isStopDelayed) stopTextClass += ' aff-quais-arrivees-redesign__stop-text--delayed';
-
- return (
-
-
-
- {item.label}
- {isStopSuppressed && (non desservi) }
- {isStopDelayed && (+{stopDelayMin} min) }
-
-
- );
+ candidates.push({
+ horaire: h,
+ stop: stopMatch,
+ arrHM,
+ rawTime,
+ platClean,
+ circulatesToday,
+ circulatesTomorrow
});
- })()}
-
+ }
+
+ // Filtrage strict par quai si demandé
+ let filteredCandidates = candidates;
+ if (targetQuaiClean) {
+ filteredCandidates = candidates.filter(c => c.platClean === targetQuaiClean);
+ }
+
+ const todayCandidates = filteredCandidates.filter(c => c.circulatesToday && c.arrHM >= nowHM - 1).sort((a, b) => a.arrHM - b.arrHM);
+ const tomorrowCandidates = filteredCandidates.filter(c => c.circulatesTomorrow && (c.arrHM < nowHM - 1 || !c.circulatesToday)).sort((a, b) => a.arrHM - b.arrHM);
+
+ const chosen = todayCandidates.length > 0
+ ? todayCandidates[0]
+ : (tomorrowCandidates.length > 0
+ ? tomorrowCandidates[0]
+ : (filteredCandidates.length > 0 ? filteredCandidates.sort((a, b) => a.arrHM - b.arrHM)[0] : null));
+
+ if (chosen) {
+ chosenTrain = chosen;
+ }
+ }
+ } catch (err) {
+ console.error('Erreur chargement horaire Supabase quai arrivée:', err);
+ }
+ }
+
+ setSelectedHoraire(chosenTrain);
+ setLoadingHoraire(false);
+ }, [gareDisplay, gareParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, cleanPlatform, circulatesOn, getStops]);
+
+ useEffect(() => {
+ loadNextForQuai();
+ const interval = setInterval(loadNextForQuai, 15000);
+ return () => clearInterval(interval);
+ }, [loadNextForQuai]);
+
+ // Données actives (réelles ou maquette Image 2 de secours)
+ const horaire = selectedHoraire?.horaire;
+ const rawArrivalTime = selectedHoraire ? (selectedHoraire.rawTime || horaire?.heure_arrivee || horaire?.time) : (gareParam ? null : mock.time);
+ const formattedTime = rawArrivalTime ? formatTimeNoSeconds(rawArrivalTime) : '';
+
+ const statusObj = horaire?._status;
+ const isSuppressed = statusObj?.label === 'supprimé' || horaire?.etat === 'supprime';
+ const isDelayed = (statusObj && typeof statusObj.delayMinutes === 'number' && statusObj.delayMinutes > 0) || (horaire?.retard_minutes > 0);
+ const delayMinutes = statusObj?.delayMinutes || horaire?.retard_minutes || 0;
+
+ let displayStatusText = "à l'heure";
+ if (isSuppressed) {
+ displayStatusText = 'supprimé';
+ } else if (isDelayed) {
+ displayStatusText = `retardé +${delayMinutes}min.`;
+ }
+
+ const destinationName = horaire ? (horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination || '') : (gareParam ? '' : mock.destination);
+ const trainType = horaire ? (horaire.type_train || horaire.train_type || horaire.op || horaire.commercial_mode || 'TER') : (gareParam ? '' : mock.trainType);
+ const trainNumber = horaire ? (horaire.numero_train || horaire.numero || horaire.number || '') : (gareParam ? '' : mock.trainNumber);
+ const operator = horaire ? (horaire.operator || horaire.op || '') : '';
+
+ // Résolution dynamique du logo couleur
+ const brand = getTrainBrand(trainType, operator, false);
+
+ // Calcul de la liste des arrêts de provenance
+ let stopsList = [];
+ if (horaire) {
+ const rawStops = getStops(horaire);
+ if (Array.isArray(rawStops) && rawStops.length > 0) {
+ stopsList = rawStops.map(s => (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''))).filter(Boolean);
+ }
+
+ // Filtrer les arrêts jusqu'à la gare courante
+ if (gareDisplay && stopsList.length > 0) {
+ const normCur = normalizeName(gareDisplay);
+ const foundIdx = stopsList.findIndex(s => normalizeName(s) === normCur);
+ if (foundIdx > 0) {
+ stopsList = stopsList.slice(0, foundIdx + 1);
+ }
+ }
+
+ // Garantir que le terminus est présent en dernier
+ if (destinationName) {
+ const destNorm = normalizeName(destinationName);
+ const lastNorm = stopsList.length > 0 ? normalizeName(stopsList[stopsList.length - 1]) : '';
+ if (lastNorm !== destNorm) {
+ stopsList.push(destinationName);
+ }
+ }
+ }
+
+ // Fallback propre
+ if (!stopsList.length) {
+ if (horaire && destinationName) {
+ stopsList = [destinationName];
+ } else if (!gareParam) {
+ stopsList = mock.stops;
+ }
+ }
+
+ // AFFICHAGE 1 : Page Vidéoprotection (via query)
+ if (isVideoprotection) {
+ return
;
+ }
+
+ // AFFICHAGE 2 : Alternance si aucun train n'est attribué à ce quai
+ if (!selectedHoraire && gareParam) {
+ if (idlePhase === 0) {
+ return
;
+ }
+ if (idlePhase === 1) {
+ return
;
+ }
+ if (idlePhase === 2) {
+ return
;
+ }
+ }
+
+ return (
+
+ {/* PANNEAU GAUCHE : BLANC (Conforme Image 2) */}
+
+ {/* Logo Couleur Dynamique */}
+
+
+ {brand.showVoyageursText && (
+
VOYAGEURS
+ )}
+
+
+ {/* Heure et Statut */}
+
+
+ {formattedTime || '--:--'}
+
+
+ {displayStatusText}
+
+
+
+ {/* Destination / Gare d'Arrivée Principale */}
+
+ {destinationName || 'Aucune arrivée prévue'}
+
+
+ {/* Type de Train & Numéro */}
+
+ {trainType && {trainType} }
+ {trainNumber && {trainNumber} }
+
+
+ {/* Filigrane Vertical "arrivée" géant (orienté bas vers haut) */}
+
+ arrivée
+
- {/* BOTTOM FOOTER: Full Width */}
-
-
- {quaiParam ? `Voie ${quaiParam}` : (selectedHoraire?.horaire?.quai ? `Voie ${selectedHoraire.horaire.quai}` : (selectedHoraire?.stop?.quai ? `Voie ${selectedHoraire.stop.quai}` : ''))}
+ {/* PANNEAU DROIT : VERT FORÊT AVEC DESSERTE (Conforme Image 2) */}
+
+
+ {stopsList.length > 0 ? (
+
+ {stopsList.map((stopName, idx) => {
+ const isLast = idx === stopsList.length - 1;
+ return (
+
+
+
+ {stopName}
+
+
+ );
+ })}
+
+ ) : (
+
+ Veuillez consulter les écrans du hall de la gare.
+
+ )}
-
-
+
+
+ {/* BANDEAU BAS : PLEINE LARGEUR & HORLOGE (Conforme Image 2) */}
+
+
+ {quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai ? `Voie ${cleanPlatform(horaire.quai).toUpperCase()}` : '')}
+
+
+
diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css
index a4b1bd0..95a4a6e 100644
--- a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css
+++ b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css
@@ -1,344 +1,315 @@
-@font-face {
- font-family: 'Achemine';
- src: url('/public/fonts/achemine_regular.ttf') format('truetype');
- font-weight: 400;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: 'Achemine';
- src: url('/public/fonts/achemine_bold.ttf') format('truetype');
- font-weight: 700;
- font-style: normal;
- font-display: swap;
-}
-
body, html {
margin: 0;
padding: 0;
- background-color: #0b376d;
+ background-color: #00337f;
overflow: hidden;
}
-.aff-quais-departs-redesign__container {
- display: flex;
- flex-direction: row;
- width: 100%;
- height: 100%;
- position: relative;
- font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
- background-color: #0b376d;
- color: #fff;
- overflow: hidden;
-}
-
-.aff-quais-departs-redesign__empty {
- display: flex;
- justify-content: center;
- align-items: center;
- flex-direction: column;
-}
-
-.aff-quais-departs-redesign__empty-text {
- font-size: 6rem;
- font-weight: 700;
- color: #ffffff;
- text-align: center;
-}
-
/* =========================================================
- LEFT PANEL
+ CONTAINER (1920x1080)
========================================================= */
-.aff-quais-departs-redesign__left {
+.aff-classique-quai-dep__container {
+ position: relative;
+ width: 1920px;
+ height: 1080px;
+ background-color: #00337f;
+ color: #ffffff;
+ font-family: 'Achemine', 'Avenir', 'Helvetica Neue', Arial, sans-serif;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+/* =========================================================
+ LEFT PANEL (BLANC - Conforme Image 1)
+ ========================================================= */
+.aff-classique-quai-dep__left {
position: absolute;
top: 0;
left: 0;
- width: 35%;
- bottom: 80px;
+ width: 672px;
+ bottom: 85px;
background-color: #ffffff;
box-sizing: border-box;
- padding: 30px 40px;
+ padding: 42px 48px;
display: flex;
flex-direction: column;
overflow: hidden;
-}
-
-.aff-quais-departs-redesign__sncf-logo {
- width: 90px;
- height: auto;
- margin-bottom: 25px;
z-index: 2;
}
-.aff-quais-departs-redesign__time-status {
+/* Logo Dynamique */
+.aff-classique-quai-dep__brand {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ margin-bottom: 24px;
+ z-index: 3;
+}
+
+.aff-classique-quai-dep__brand-logo {
+ height: 50px;
+ max-width: 220px;
+ object-fit: contain;
+ display: block;
+}
+
+.aff-classique-quai-dep__sncf-voyageurs {
+ font-size: 14px;
+ font-weight: 800;
+ color: #00337f;
+ letter-spacing: 2px;
+ margin-top: 4px;
+}
+
+/* Ligne Heure + Statut */
+.aff-classique-quai-dep__time-row {
display: flex;
align-items: baseline;
- gap: 15px;
- margin-bottom: 5px;
- z-index: 2;
+ margin-bottom: 8px;
+ z-index: 3;
}
-.aff-quais-departs-redesign__time {
- color: #0b376d;
+.aff-classique-quai-dep__time {
+ color: #00337f;
font-weight: 700;
- font-size: 7.5rem;
+ font-size: 105px;
+ line-height: 0.92;
+ letter-spacing: -1px;
+}
+
+.aff-classique-quai-dep__status {
+ color: #00337f;
+ font-weight: 700;
+ font-size: 32px;
line-height: 1;
+ margin-left: 20px;
}
-.aff-quais-departs-redesign__status {
- color: #0b376d;
+.aff-classique-quai-dep__time--suppressed,
+.aff-classique-quai-dep__status--suppressed {
+ color: #d32f2f !important;
+}
+
+.aff-classique-quai-dep__time--delayed,
+.aff-classique-quai-dep__status--delayed {
+ color: #f5a623 !important;
+}
+
+/* Grande Destination */
+.aff-classique-quai-dep__destination {
+ color: #00337f;
font-weight: 700;
- font-size: 2.4rem;
- line-height: 1;
+ font-size: 82px;
+ line-height: 1.05;
+ margin-top: 14px;
+ margin-bottom: 14px;
+ z-index: 3;
+ word-break: break-word;
}
-.aff-quais-departs-redesign__destination {
- color: #0b376d;
- font-weight: 700;
- font-size: 5.5rem; /* Slightly smaller to fit 2 lines if needed */
- line-height: 1.1;
- margin-bottom: 5px;
- z-index: 2;
- white-space: normal; /* Allow wrapping */
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- text-overflow: ellipsis;
- max-height: 13rem; /* Prevent overflowing panel */
-}
-
-.aff-quais-departs-redesign__train-info {
+/* Info Train (Type + Numéro) */
+.aff-classique-quai-dep__train-info {
display: flex;
- align-items: center;
- gap: 10px;
- z-index: 2;
+ align-items: baseline;
+ z-index: 3;
}
-.aff-quais-departs-redesign__train-type {
- color: #0b376d;
+.aff-classique-quai-dep__train-type {
+ color: #00337f;
font-weight: 700;
- font-size: 2.2rem;
+ font-size: 38px;
letter-spacing: 0.5px;
}
-.aff-quais-departs-redesign__train-number {
- color: #929bb0;
+.aff-classique-quai-dep__train-number {
+ color: #7e859b;
font-weight: 400;
- font-size: 2.2rem;
+ font-size: 38px;
letter-spacing: 0.5px;
+ margin-left: 12px;
}
-.aff-quais-departs-redesign__icons {
+/* Filigrane Vertical "départ" géant (orienté bas vers haut) */
+.aff-classique-quai-dep__watermark {
position: absolute;
+ right: -5px;
bottom: 20px;
- left: 40px;
+ width: 0;
+ height: 0;
display: flex;
- align-items: center;
- z-index: 2;
-}
-
-.aff-quais-departs-redesign__watermark {
- position: absolute;
- right: 0;
- top: 50%;
- transform: translateY(-50%) rotate(-90deg);
- transform-origin: right center;
- font-size: 17rem; /* Massive size */
- color: #e5e8ed;
- font-weight: 700;
- z-index: 1;
+ justify-content: flex-start;
+ align-items: flex-end;
pointer-events: none;
user-select: none;
- letter-spacing: 1.5rem;
+ z-index: 1;
+}
+
+.aff-classique-quai-dep__watermark span {
+ display: inline-block;
+ transform: rotate(-90deg);
+ transform-origin: 0 100%;
+ font-size: 168px;
+ font-weight: 700;
+ color: #e2e8f2;
line-height: 1;
+ letter-spacing: 0.05em;
white-space: nowrap;
}
/* =========================================================
- RIGHT PANEL
+ RIGHT PANEL (BLEU NUIT - Conforme Image 1)
========================================================= */
-.aff-quais-departs-redesign__right {
+.aff-classique-quai-dep__right {
position: absolute;
top: 0;
+ left: 672px;
right: 0;
- width: 65%;
- bottom: 80px;
- background-color: #0b376d;
+ bottom: 85px;
+ background-color: #00337f;
box-sizing: border-box;
- padding: 30px 0 30px 0;
+ padding: 42px 0 20px 42px;
overflow: hidden;
display: flex;
flex-direction: column;
}
-.aff-quais-departs-redesign__stops-list {
+.aff-classique-quai-dep__stops-wrapper {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+.aff-classique-quai-dep__stops-list {
list-style: none;
margin: 0;
- padding: 0 0 0 60px; /* Space from left edge */
- width: 100%;
- position: relative;
- z-index: 2;
+ padding: 0;
display: flex;
flex-direction: column;
- gap: 8px; /* Closer spacing between stops */
+ gap: 12px;
+ position: relative;
+ z-index: 2;
}
-.aff-quais-departs-redesign__stop {
+.aff-classique-quai-dep__stop-item {
display: flex;
align-items: center;
- font-size: 3.4rem;
- font-weight: 400;
- color: #ffffff;
position: relative;
- min-height: 42px;
- padding-left: 50px; /* Space for rail and bullet */
+ min-height: 56px;
+ padding-left: 65px;
}
-/* Bullet styling */
-.aff-quais-departs-redesign__stop-bullet {
+/* Rail vertical violet/indigo (#5c538f) */
+.aff-classique-quai-dep__stop-item::before {
+ content: '';
position: absolute;
- left: 0; /* Centered on 24px rail: (24-16)/2 = 4 */
- margin-left: 4px;
- top: 50%;
- transform: translateY(-50%);
- width: 16px; /* Slightly larger bullet */
- height: 16px;
- background-color: #fbec22;
+ left: 20px;
+ width: 28px;
+ background-color: #5c538f;
+ top: -15px;
+ bottom: -15px;
+ z-index: 1;
+}
+
+/* Le rail monte jusqu'en haut au 1er arrêt */
+.aff-classique-quai-dep__stop-item:first-child::before {
+ top: -600px;
+}
+
+/* Le dernier arrêt n'a pas de rail qui continue en dessous */
+.aff-classique-quai-dep__stop-item:last-child::before {
+ display: none;
+}
+
+/* Pastille Jaune (#ffe500 / #fbec22) */
+.aff-classique-quai-dep__bullet {
+ position: absolute;
+ left: 24px;
+ width: 20px;
+ height: 20px;
+ background-color: #ffe500;
border-radius: 50%;
z-index: 4;
}
-.aff-quais-departs-redesign__stop-text {
- position: relative;
- z-index: 4;
+/* Nom de la station */
+.aff-classique-quai-dep__stop-name {
+ font-size: 42px;
+ font-weight: 600;
+ color: #ffffff;
white-space: nowrap;
letter-spacing: 0.5px;
- padding-left: 10px; /* Space from bullet */
+ z-index: 4;
}
-/* The vertical rail segments drawn behind each bullet */
-.aff-quais-departs-redesign__stop::before {
- content: '';
- position: absolute;
- left: 0;
- width: 24px; /* Much wider rail */
- background-color: #5c538f;
- top: -10px;
- bottom: -10px;
- z-index: 1;
-}
-
-/* Rail extends all the way to the top */
-.aff-quais-departs-redesign__stop:first-child::before {
- top: -1000px;
-}
-
-/* Last stop has no rail continuing below */
-.aff-quais-departs-redesign__stop:last-child::before {
- display: none;
-}
-
-/* Highlighted Destination Stop */
-.aff-quais-departs-redesign__stop--dest {
+/* Gare terminus / mise en avant */
+.aff-classique-quai-dep__stop-item--dest {
font-weight: 700;
}
-.aff-quais-departs-redesign__stop--dest .aff-quais-departs-redesign__stop-text {
+.aff-classique-quai-dep__stop-item--dest .aff-classique-quai-dep__stop-name {
background-color: #5c538f;
- padding: 4px 20px 4px 60px; /* Increased left padding to cover bullet */
- margin-left: -60px; /* Pull back to exactly align with left:0 */
- border-radius: 4px;
+ padding: 8px 32px 8px 65px;
+ margin-left: -65px;
+ border-radius: 6px;
position: relative;
- z-index: 3; /* Behind the bullet (z-index 4) */
+ z-index: 3;
display: inline-block;
+ font-weight: 700;
+ font-size: 44px;
}
/* =========================================================
- FOOTER
+ FOOTER & HORLOGE (Conforme Image 1)
========================================================= */
-.aff-quais-departs-redesign__footer {
+.aff-classique-quai-dep__footer {
position: absolute;
bottom: 0;
left: 0;
- width: 100%;
- height: 80px;
- background-color: #0b376d;
+ width: 1920px;
+ height: 85px;
+ background-color: #00337f;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
- padding: 0 40px;
- z-index: 10;
+ padding: 0 35px;
+ z-index: 20;
}
-.aff-quais-departs-redesign__footer-left {
- font-size: 3rem;
+.aff-classique-quai-dep__footer-track {
+ font-size: 34px;
font-weight: 700;
color: #ffffff;
font-style: italic;
- font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
letter-spacing: 0.5px;
}
-.aff-quais-departs-redesign__footer-right {
+.aff-classique-quai-dep__footer-clock {
display: flex;
align-items: center;
}
-.aff-quais-departs-redesign__clock {
- border: 2px solid #ffffff;
- padding: 4px 14px;
- font-size: 3rem;
- font-weight: 700;
- color: #ffffff;
+.aff-classique-quai-dep__clock {
+ background-color: #00337f;
+ border: 2.5px solid #ffffff;
+ padding: 4px 16px;
+ border-radius: 3px;
display: flex;
- align-items: center;
- gap: 10px;
+ align-items: baseline;
+ gap: 4px;
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
+ font-size: 42px;
+ font-weight: 700;
+ line-height: 1;
}
-.aff-quais-departs-redesign__clock-s {
- color: #f5a623;
+.aff-classique-quai-dep__clock-hm {
+ color: #ffffff;
}
-.aff-quais-departs-redesign__time--long-text {
- font-size: 3.5rem !important;
- line-height: 1.2 !important;
+.aff-classique-quai-dep__clock-s {
+ color: #ff8c00;
+ margin-left: 2px;
}
-.aff-quais-departs-redesign__time--suppressed,
-.aff-quais-departs-redesign__status--suppressed {
- color: #ff4c5b !important;
-}
-
-.aff-quais-departs-redesign__time--delayed,
-.aff-quais-departs-redesign__status--delayed {
- color: #ff9e43 !important;
-}
-
-/* Disrupted Stop styling */
-.aff-quais-departs-redesign__stop-text--suppressed {
- text-decoration: line-through;
- color: #ff4c5b;
- opacity: 0.6;
-}
-
-.aff-quais-departs-redesign__stop-text--delayed {
- color: #ff9e43;
-}
-
-.aff-quais-departs-redesign__stop-status-badge {
- font-size: 2.2rem;
- font-weight: bold;
- margin-left: 10px;
-}
-
-.aff-quais-departs-redesign__stop-status-badge--suppressed {
- color: #ff4c5b;
-}
-
-.aff-quais-departs-redesign__stop-status-badge--delayed {
- color: #ff9e43;
-}
diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js
index ea96cbf..a844c01 100644
--- a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js
+++ b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js
@@ -6,133 +6,182 @@ import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../../services/railwayService';
import { getSncfTimetable } from '../../../../services/sncfApi';
-function Clock() {
+import GareVideoprotection from '../../../../components/afficheurs/GareVideoprotection';
+import AttentifsEnsemble from '../../../../components/afficheurs/AttentifsEnsemble';
+import DepartEnCoursClassique from '../../../../components/afficheurs/DepartEnCoursClassique';
+import ClassiquesInformations from '../ClassiquesInformations';
+import ClassiquesDeparts from '../ClassiquesDeparts';
+
+function DigitalClock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id);
}, []);
- const hh = String(now.getHours()).padStart(2,'0');
- const mm = String(now.getMinutes()).padStart(2,'0');
- const ss = String(now.getSeconds()).padStart(2,'0');
+ const hh = String(now.getHours()).padStart(2, '0');
+ const mm = String(now.getMinutes()).padStart(2, '0');
+ const ss = String(now.getSeconds()).padStart(2, '0');
return (
-
-
{hh}
-
{mm}
-
{ss}
+
+ {hh}:{mm}
+ {ss}
);
}
-export default function ClassiquesDepartsQuais() {
- const paramsForThis = new URLSearchParams(window.location.search || '');
- const gareParam = paramsForThis.get('gare');
- const gareIdParam = paramsForThis.get('gare_id') || paramsForThis.get('gareId');
- const quaiParam = paramsForThis.get('quai');
+function getTrainBrand(trainType = '', operator = '', isDeparture = true) {
+ const combined = `${trainType || ''} ${operator || ''}`.toLowerCase().trim();
- const gareDisplayParam = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g,' ')).replace(/-/g,' ') : null;
+ if (combined.includes('zou')) {
+ return { logo: '/type/quais/logo-zou-color.svg', label: 'ZOU!', showVoyageursText: false };
+ }
+ if (combined.includes('fluo')) {
+ return { logo: '/type/quais/logo-fluo-color.svg', label: 'TER Fluo', showVoyageursText: false };
+ }
+ if (combined.includes('aleop') || combined.includes('al\u00e9op')) {
+ return { logo: '/type/quais/logo-aleop-color.svg', label: 'Aléop', showVoyageursText: false };
+ }
+ if (combined.includes('breizh')) {
+ return { logo: '/type/quais/logo-breizhgo-color.svg', label: 'BreizhGo', showVoyageursText: false };
+ }
+ if (combined.includes('lio') || combined.includes('li\u00f4')) {
+ return { logo: '/type/quais/logo-lio-color.svg', label: 'liO', showVoyageursText: false };
+ }
+ if (combined.includes('mobigo')) {
+ return { logo: '/type/quais/logo-mobigo-color.svg', label: 'Mobigo', showVoyageursText: false };
+ }
+ if (combined.includes('nomad')) {
+ return { logo: '/type/quais/logo-nomad-color.svg', label: 'Nomad', showVoyageursText: false };
+ }
+ if (combined.includes('remi') || combined.includes('r\u00e9mi')) {
+ return { logo: '/type/quais/logo-remi-color.svg', label: 'Rémi', showVoyageursText: false };
+ }
+ if (combined.includes('inoui') || combined.includes('tgv inoui')) {
+ return { logo: '/type/quais/logo-inoui-color.svg', label: 'TGV INOUI', showVoyageursText: false };
+ }
+ if (combined.includes('ouigo classique')) {
+ return { logo: '/type/quais/logo-ouigo-classique-color.svg', label: 'OUIGO Classique', showVoyageursText: false };
+ }
+ if (combined.includes('ouigo')) {
+ return { logo: '/type/quais/logo-ouigo-color.svg', label: 'OUIGO', showVoyageursText: false };
+ }
+ if (combined.includes('intercite') || combined.includes('intercit\u00e9')) {
+ return { logo: '/type/quais/logo-intercite-color.svg', label: 'INTERCITÉS', showVoyageursText: false };
+ }
+ if (combined.includes('tgv')) {
+ return { logo: '/type/quais/logo-tgv-color.svg', label: 'TGV', showVoyageursText: false };
+ }
+ if (combined.includes('eurostar')) {
+ return { logo: '/type/quais/logo-eurostar-color.svg', label: 'Eurostar', showVoyageursText: false };
+ }
+ if (combined.includes('freccia') || combined.includes('trenitalia')) {
+ return { logo: '/type/quais/logo-frecciarossa-color.svg', label: 'Frecciarossa', showVoyageursText: false };
+ }
+ if (combined.includes('ice') || combined.includes('db')) {
+ return { logo: '/type/quais/logo-ice-color.svg', label: 'ICE', showVoyageursText: false };
+ }
+ if (combined.includes('ter')) {
+ return { logo: '/type/quais/logo-ter-color.svg', label: 'TER', showVoyageursText: false };
+ }
+
+ // Par défaut Départs : Logo SNCF Voyageurs (Image 1)
+ if (isDeparture) {
+ return { logo: '/type/quais/logo-sncf-color.svg', label: trainType || 'SNCF', showVoyageursText: true };
+ }
+ return { logo: '/type/quais/logo-ter-color.svg', label: trainType || 'TER', showVoyageursText: false };
+}
+
+export default function ClassiquesDepartsQuais() {
+ const searchParams = new URLSearchParams(window.location.search || '');
+ let gareParam = searchParams.get('gare') || searchParams.get('station') || '';
+ const gareIdParam = searchParams.get('gare_id') || searchParams.get('gareId') || null;
+ const quaiParam = searchParams.get('quai') || searchParams.get('voie') || searchParams.get('track') || searchParams.get('platform') || '';
+
+ // Fallback si la gare est dans le chemin
+ if (!gareParam && typeof window !== 'undefined') {
+ const cleanPath = window.location.pathname.replace(/^\/ferrovia/, '');
+ const parts = cleanPath.split('/').filter(Boolean);
+ const qIdx = parts.findIndex(p => p === 'quais');
+ if (qIdx >= 0 && parts[qIdx + 1]) {
+ gareParam = parts.slice(qIdx + 1).join('/');
+ }
+ }
+
+ const gareDisplay = gareParam
+ ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ')
+ : null;
const mock = {
- time: '16h44',
+ time: '09h11',
status: "à l'heure",
- trainType: 'Fluo',
- trainNumber: '891811',
- destinationBig: 'Seurre',
- destinationStation: '',
+ trainType: 'ZOU!',
+ trainNumber: '881027',
+ destination: 'Aubagne',
stops: [
- 'Ouges','Saulon','Longecourt','Aiserey','Brazey-en-Plaine','Saint-Jean-de-Losne','Chaugey','Pagny','Seurre','Mervans','Louhans','Saint-Amour','Bourg-en-Bresse'
- ],
+ 'Marseille Blancarde',
+ 'La Pomme',
+ 'St Marcel',
+ 'La Barasse',
+ 'La Penne sur Huveaune',
+ 'Aubagne'
+ ]
};
const [selectedHoraire, setSelectedHoraire] = useState(null);
- const [loadingHoraire, setLoadingHoraire] = useState(false);
+ const [, setLoadingHoraire] = useState(false);
const [, setHoraireError] = useState(null);
- const [, setCandidatesCount] = useState(0);
- const [, setStationType] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date());
+ const [idlePhase, setIdlePhase] = useState(0); // 0 = Attentifs (20s), 1 = Quai (20s), 2 = Information (10s)
+
+ const pageParam = (searchParams.get('page') || searchParams.get('mode') || '').toLowerCase();
+ const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true';
+ const isForceDepartEnCours = pageParam === 'depart_en_cours' || searchParams.get('depart_en_cours') === 'true' || searchParams.get('test_depart') === 'true';
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
useAnnonceSonoreRealtime(resolvedGareId, 'ClassiquesDepartsQuais', quaiParam);
useAutoAnnonce(selectedHoraire ? [selectedHoraire] : [], currentTime, false);
- const [alternateDelayPhase, setAlternateDelayPhase] = useState(true);
- const [alternateCancelPhase, setAlternateCancelPhase] = useState(true);
+ // Horloge 1s
useEffect(() => {
const id = setInterval(() => setCurrentTime(new Date()), 1000);
return () => clearInterval(id);
}, []);
+ // Alternance par défaut (20s Attentifs -> 20s Quai -> 10s Information) si aucun train n'est prévu sur le quai
useEffect(() => {
- const interval = setInterval(() => {
- setAlternateDelayPhase(prev => !prev);
- }, 5000);
- return () => clearInterval(interval);
- }, []);
+ if (selectedHoraire) return;
+ let duration = 20000;
+ if (idlePhase === 2) duration = 10000;
+ else duration = 20000;
- useEffect(() => {
- const interval = setInterval(() => {
- setAlternateCancelPhase(prev => !prev);
- }, 5000);
- return () => clearInterval(interval);
- }, []);
-
- const [perturbations, setPerturbations] = useState([]);
- const [, setTravaux] = useState([]);
-
- const circulatesOn = useCallback((h, d) => {
- if (!h || !d) return false;
- const pad = (n) => String(n).padStart(2, '0');
- const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
- const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
- const targetDay = d.getDay();
-
- if (Array.isArray(h.dates) && h.dates.length > 0) {
- return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
- }
-
- const customDaysRaw = h.jours_personnalises;
- if (customDaysRaw) {
- let customDays = [];
- if (Array.isArray(customDaysRaw)) {
- customDays = customDaysRaw;
- } else if (typeof customDaysRaw === 'string') {
- try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
- }
- if (Array.isArray(customDays) && customDays.length > 0) {
- const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
- if (match) return Boolean(match.circule);
- }
- }
-
- const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
- const colName = `circule_${dayNames[targetDay]}`;
- if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
- const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
- const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
- if (db && targetDateHyphen < db) return false;
- if (df && targetDateHyphen > df) return false;
- return Boolean(h[colName]);
- }
-
- if (Array.isArray(h.days) && h.days.length > 0) {
- if (!h.days.includes(targetDay)) return false;
- if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
- if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
- return true;
- }
-
- const singleDate = h.date_depart || h.date || h.service_date;
- if (singleDate) {
- const sStr = String(singleDate).slice(0, 10);
- return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
- }
-
- return true;
- }, []);
+ const timer = setTimeout(() => {
+ setIdlePhase(prev => (prev + 1) % 3);
+ }, duration);
+ return () => clearTimeout(timer);
+ }, [idlePhase, selectedHoraire]);
const normalizeName = useCallback((s) => {
if (!s) return '';
- try { return String(s).normalize('NFD').replace(/\p{M}/gu,'').toLowerCase().trim().replace(/[-_]+/g,' ').replace(/\s+/g,' '); } catch (e) { return String(s).toLowerCase().trim(); }
+ try {
+ return String(s)
+ .normalize('NFD')
+ .replace(/\p{M}/gu, '')
+ .toLowerCase()
+ .trim()
+ .replace(/[-_]+/g, ' ')
+ .replace(/\s+/g, ' ');
+ } catch (e) {
+ return String(s).toLowerCase().trim();
+ }
+ }, []);
+
+ const cleanPlatform = useCallback((p) => {
+ if (!p) return '';
+ return String(p)
+ .toLowerCase()
+ .replace(/^(?:voie|quai|track|platform)\s*/i, '')
+ .replace(/\s+/g, '')
+ .trim();
}, []);
const parseTimeHM = useCallback((s) => {
@@ -152,364 +201,128 @@ export default function ClassiquesDepartsQuais() {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
- const hh = String(tryDate.getHours() % 24).padStart(2,'0');
- const mm = String(tryDate.getMinutes()).padStart(2,'0');
+ const hh = String(tryDate.getHours() % 24).padStart(2, '0');
+ const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) {
const h = parseInt(m[1], 10) % 24;
- return `${String(h).padStart(2,'0')}h${m[2]}`;
+ return `${String(h).padStart(2, '0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) {
const h = parseInt(m2[1], 10) % 24;
- return `${String(h).padStart(2,'0')}h${m2[2]}`;
+ return `${String(h).padStart(2, '0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
- return `${String(h).padStart(2,'0')}h${m3[2]}`;
+ return `${String(h).padStart(2, '0')}h${m3[2]}`;
}
- return v.slice(0,5).replace(':','h');
+ return v.slice(0, 5).replace(':', 'h');
}
- return String(v).slice(0,5).replace(':','h');
+ return String(v).slice(0, 5).replace(':', 'h');
}, []);
- const formatTimeFromHM = useCallback((totalMinutes) => {
- if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return '';
- const m = ((totalMinutes % 1440) + 1440) % 1440;
- const hh = String(Math.floor(m / 60)).padStart(2, '0');
- const mm = String(m % 60).padStart(2, '0');
- return `${hh}h${mm}`;
+ const circulatesOn = useCallback((h, d) => {
+ if (!h || !d) return false;
+ const pad = (n) => String(n).padStart(2, '0');
+ const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
+ const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
+ const targetDay = d.getDay();
+
+ if (Array.isArray(h.dates) && h.dates.length > 0) {
+ return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
+ }
+
+ const customDaysRaw = h.jours_personnalises;
+ if (customDaysRaw) {
+ let customDays = [];
+ if (Array.isArray(customDaysRaw)) customDays = customDaysRaw;
+ else if (typeof customDaysRaw === 'string') {
+ try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
+ }
+ if (Array.isArray(customDays) && customDays.length > 0) {
+ const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
+ if (match) return Boolean(match.circule);
+ }
+ }
+
+ const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
+ const colName = `circule_${dayNames[targetDay]}`;
+ if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
+ const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
+ const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
+ if (db && targetDateHyphen < db) return false;
+ if (df && targetDateHyphen > df) return false;
+ return Boolean(h[colName]);
+ }
+
+ if (Array.isArray(h.days) && h.days.length > 0) {
+ if (!h.days.includes(targetDay)) return false;
+ if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
+ if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
+ return true;
+ }
+
+ const singleDate = h.date_depart || h.date || h.service_date;
+ if (singleDate) {
+ const sStr = String(singleDate).slice(0, 10);
+ return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
+ }
+
+ return true;
}, []);
-
-
-
-
- const getStops = (h) => {
+ const getStops = useCallback((h) => {
if (!h) return [];
if (Array.isArray(h.stops) && h.stops.length) return h.stops;
if (Array.isArray(h.gares_desservies) && h.gares_desservies.length) return h.gares_desservies;
if (Array.isArray(h.garesDesservies) && h.garesDesservies.length) return h.garesDesservies;
- if (Array.isArray(h.arrets) && h.arrets.length) return h.arrets;
- const candidates = ['stops','gares_desservies','garesDesservies','gares_desservies_json','arrets','gares_desservies_json'];
+ const candidates = ['stops', 'gares_desservies', 'garesDesservies', 'gares_desservies_json', 'arrets'];
for (const key of candidates) {
const raw = h[key];
if (!raw) continue;
if (Array.isArray(raw)) return raw;
if (typeof raw === 'string') {
- try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) return parsed; } catch (e) {}
+ try {
+ const parsed = JSON.parse(raw);
+ if (Array.isArray(parsed)) return parsed;
+ } catch (e) {}
}
}
return [];
- };
-
- const safeParseArrets = (p) => {
- const raw = p && (p.perturbations_arrets || p.arrets || p.arrets_perturbations);
- if (!raw) return [];
- if (Array.isArray(raw)) return raw;
- try {
- const parsed = JSON.parse(raw);
- if (parsed && typeof parsed === 'object') {
- if (Array.isArray(parsed.arrets)) return parsed.arrets;
- return parsed;
- }
- return [];
- } catch (e) { return []; }
- };
-
- const parsePerturbArretsObject = useCallback((p) => {
- const raw = p && (p.perturbations_arrets || p.arrets || p.arrets_perturbations);
- if (!raw) return { arrets: [], jours: null };
- if (typeof raw === 'object') {
- if (Array.isArray(raw.arrets)) return { arrets: raw.arrets, jours: raw.jours_perturbation || raw.jours || null };
- if (Array.isArray(raw)) return { arrets: raw, jours: null };
- return { arrets: [], jours: null };
- }
- try {
- const parsed = JSON.parse(raw);
- if (parsed && typeof parsed === 'object') {
- return { arrets: Array.isArray(parsed.arrets) ? parsed.arrets : (Array.isArray(parsed) ? parsed : []), jours: parsed.jours_perturbation || parsed.jours || null };
- }
- return { arrets: [], jours: null };
- } catch (e) {
- return { arrets: [], jours: null };
- }
}, []);
- const getPerturbationJours = useCallback((p) => {
- if (!p) return null;
- const candidate = p.jours_perturbation || p.jours || null;
- const result = new Set();
- const pushDate = (d) => {
- if (!d) return;
- try { const dd = new Date(d); if (!isNaN(dd.getTime())) result.add(dd.toISOString().slice(0,10)); } catch(e){}
- };
- if (candidate) {
- if (Array.isArray(candidate)) candidate.forEach(item => pushDate(item.date || item));
- else if (typeof candidate === 'string') {
- try { const parsed = JSON.parse(candidate); if (Array.isArray(parsed)) parsed.forEach(it => pushDate(it.date || it)); }
- catch (e) { pushDate(candidate); }
- } else if (typeof candidate === 'object') { if (Array.isArray(candidate)) candidate.forEach(it => pushDate(it.date || it)); else pushDate(candidate.date || null); }
- }
- try {
- const parsedObj = parsePerturbArretsObject(p);
- const jp = parsedObj.jours;
- if (jp) {
- if (Array.isArray(jp)) jp.forEach(it => pushDate(it.date || it));
- else if (typeof jp === 'string') {
- try { const parsed = JSON.parse(jp); if (Array.isArray(parsed)) parsed.forEach(it => pushDate(it.date || it)); } catch (e) { pushDate(jp); }
- } else if (typeof jp === 'object') pushDate(jp.date || null);
- }
- } catch (e) {}
- return result.size ? result : null;
- }, [parsePerturbArretsObject]);
+ // Chargement du train au départ
+ const loadNextForQuai = useCallback(async () => {
+ setLoadingHoraire(true);
+ setHoraireError(null);
- const parseDelayMinutesFromArret = useCallback((arret) => {
- if (!arret) return null;
- const keys = ['retard_minutes','retard','delay_minutes','delay','retard_min'];
- for (const k of keys) {
- if (typeof arret[k] !== 'undefined' && arret[k] !== null && arret[k] !== '') {
- const n = Number(arret[k]); if (!isNaN(n)) return Math.round(n);
- }
- }
- return null;
- }, []);
+ const source = getSourceFromPath(window.location.pathname);
+ const targetQuaiClean = cleanPlatform(quaiParam);
- const buildPerturbMap = useCallback((list = []) => {
- const idx = { byHoraire: {}, byNumber: {}, byStopId: {}, byStopName: {}, global: [] };
- (list || []).forEach(p => {
- const horaireId = p.horaire_id || p.horaire || p.horaireId || p.horaireID;
- if (horaireId != null) { const k = String(horaireId); idx.byHoraire[k] = idx.byHoraire[k] || []; idx.byHoraire[k].push(p); }
- const number = p.number || p.train_number || p.train || p.numero || p.numero_train;
- if (number != null) { const kn = String(number); idx.byNumber[kn] = idx.byNumber[kn] || []; idx.byNumber[kn].push(p); }
- const arrets = safeParseArrets(p);
- if (Array.isArray(arrets) && arrets.length) {
- arrets.forEach(a => {
- const stopId = a.gare_id || a.gareId || a.id || a.station_id || a.stationId || a.uic || a.code_uic || a.gare_uic || a.code;
- const stopName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom;
- if (stopId != null) { const ks = String(stopId); idx.byStopId[ks] = idx.byStopId[ks] || []; idx.byStopId[ks].push({ perturbation: p, arret: a }); }
- if (stopName) { const key = normalizeName(stopName); idx.byStopName[key] = idx.byStopName[key] || []; idx.byStopName[key].push({ perturbation: p, arret: a }); }
+ let chosenTrain = null;
+
+ // 1. Tenter la source SNCF si applicable
+ if (source === 'sncf' || !supabase) {
+ try {
+ const { data: sncfData } = await getSncfTimetable({
+ gareName: gareDisplay || gareParam,
+ mode: 'departs',
+ quai: quaiParam
});
- }
- if ((horaireId == null && number == null && (!Array.isArray(arrets) || !arrets.length))) { idx.global.push(p); }
- });
- return idx;
- }, [normalizeName]);
-
- const findPerturbsForHoraireAndStop = useCallback((horaire, stationId, stationName, idx) => {
- const res = [];
- const map = idx || buildPerturbMap(perturbations || []);
- const hId = horaire && (horaire.id || horaire.horaire_id || horaire.numero || horaire.numero_train);
- const number = horaire && (horaire.numero_train || horaire.number || horaire.numero || horaire.train_number);
- if (hId != null) { const kh = String(hId); if (map.byHoraire && map.byHoraire[kh]) res.push(...map.byHoraire[kh]); }
- if (number != null) { const kn = String(number); if (map.byNumber && map.byNumber[kn]) res.push(...map.byNumber[kn]); }
- if (stationId != null) { const ks = String(stationId); if (map.byStopId && map.byStopId[ks]) map.byStopId[ks].forEach(x => res.push(x.perturbation)); }
- if (stationName && map.byStopName) {
- const key = normalizeName(stationName);
- if (map.byStopName[key]) map.byStopName[key].forEach(x => res.push(x.perturbation));
- try {
- const needle = key;
- for (const existingKey of Object.keys(map.byStopName || {})) {
- if (existingKey === needle) continue;
- if (existingKey.includes(needle) || needle.includes(existingKey)) { map.byStopName[existingKey].forEach(x => res.push(x.perturbation)); }
- }
- } catch (e) {}
- }
- return Array.from(new Set(res));
- }, [buildPerturbMap, perturbations, normalizeName]);
-
- const computeStatusForHoraireAtStop = useCallback((horaire, stationId, stationName, idx, serviceDate = null) => {
- const found = findPerturbsForHoraireAndStop(horaire, stationId, stationName, idx) || [];
- const toYMD = (v) => { if (!v) return null; const d = new Date(v); if (isNaN(d.getTime())) return null; return d.toISOString().slice(0,10); };
- let horaireYmd = toYMD(horaire && (horaire._date || horaire.date || horaire.date_service || horaire.service_date));
- if (!horaireYmd && serviceDate) horaireYmd = serviceDate;
- let maxDelay = null;
- for (const p of found) {
- try {
- if (horaireYmd) {
- const pDate = toYMD(p.date || p.date_service);
- const db = toYMD(p.date_debut || p.start_date);
- const df = toYMD(p.date_fin || p.end_date);
- if (pDate && pDate !== horaireYmd) continue;
- if (db && df && !(horaireYmd >= db && horaireYmd <= df)) continue;
- }
- } catch (e) {}
- try {
- const joursSet = getPerturbationJours(p);
- if (joursSet && horaireYmd) { if (!joursSet.has(horaireYmd)) continue; }
- } catch (e) {}
- const parsedObj = parsePerturbArretsObject(p) || { arrets: [], jours: null };
- const arrets = parsedObj.arrets || [];
- let matched = false;
- for (const a of arrets) {
- const aId = a.gare_id || a.gareId || a.id || a.station_id || a.stationId || a.uic || a.code;
- const aName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom;
- const na = normalizeName(aName || '');
- const ns = normalizeName(stationName || '');
- const nameMatches = stationName && (na === ns || na.includes(ns) || ns.includes(na));
- if ((stationId != null && String(aId) === String(stationId)) || nameMatches) {
- matched = true;
- const t = (a.type_perturbation || a.type || '').toString().toLowerCase();
- if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) return { label: 'supprimé', detail: '', delayMinutes: null };
- if (t.includes('retard') || t.includes('delay') || /retard/.test(t)) {
- const d = parseDelayMinutesFromArret(a) || parseDelayMinutesFromArret(p) || 0;
- if (maxDelay == null || d > maxDelay) maxDelay = d;
+ if (sncfData && sncfData.length > 0) {
+ let matchingTrains = sncfData;
+ if (targetQuaiClean) {
+ matchingTrains = sncfData.filter(t => cleanPlatform(t.quai || t.voie || t.quai_reel || t._displayPlatform) === targetQuaiClean);
}
- }
- }
- if (!matched) {
- const ref = p.horaire_id || p.horaire || p.horaireId || p.numero_train || p.train_number || p.numero;
- const numberRef = p.number || p.train_number || p.numero || p.numero_train;
- const appliesByRef = ref && (String(ref) === String(horaire.id) || String(ref) === String(horaire.numero_train) || String(ref) === String(horaire.numero));
- const appliesByNumber = numberRef && (String(numberRef) === String(horaire.numero_train) || String(numberRef) === String(horaire.numero) || String(numberRef) === String(horaire.number));
- const isGlobal = (!Array.isArray(arrets) || arrets.length === 0) && !ref && !numberRef;
- if (appliesByRef || appliesByNumber || isGlobal) {
- const t = (p.type_perturbation || p.type || '').toString().toLowerCase();
- if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) return { label: 'supprimé', detail: '', delayMinutes: null };
- if (t.includes('retard') || t.includes('delay') || /retard/.test(t)) {
- const d = parseDelayMinutesFromArret(p) || 0;
- if (maxDelay == null || d > maxDelay) maxDelay = d;
- }
- }
- }
- }
- if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay };
- return { label: "à l'heure", detail: '', delayMinutes: null };
- }, [findPerturbsForHoraireAndStop, parseDelayMinutesFromArret, getPerturbationJours, normalizeName, parsePerturbArretsObject]);
-
- const perturbMap = React.useMemo(() => buildPerturbMap(perturbations || []), [perturbations, buildPerturbMap]);
- const serviceDate = React.useMemo(() => {
- const todayYmd = new Date().toLocaleDateString('en-CA');
- const tomorrow = new Date();
- tomorrow.setDate(tomorrow.getDate() + 1);
- const tomorrowYmd = tomorrow.toLocaleDateString('en-CA');
- return selectedHoraire && selectedHoraire.isTomorrow ? tomorrowYmd : todayYmd;
- }, [selectedHoraire]);
-
- useEffect(() => {
- let mounted = true;
- let inFlight = false;
- const fetchPerturb = async () => {
- if (!mounted) return;
- if (inFlight) return;
- inFlight = true;
- try {
- if (!supabase) return;
- const [{ data: perturbData, error: pError }, { data: travauxData, error: tError }] = await Promise.all([
- supabase.from('perturbations').select('*'),
- supabase.from('travaux').select('*')
- ]);
- if (!mounted) return;
- if (pError || tError) {
- } else {
- const todayYmd = new Date().toLocaleDateString('en-CA');
- const tomorrow = new Date();
- tomorrow.setDate(tomorrow.getDate() + 1);
- const tomorrowYmd = tomorrow.toLocaleDateString('en-CA');
-
- const filteredPerturb = (perturbData || []).filter(p => {
- try {
- const toYMD = (v) => { if (!v) return null; const d = new Date(v); if (isNaN(d.getTime())) return null; return d.toISOString().slice(0,10); };
- const pDate = toYMD(p.date || p.date_service);
- const db = toYMD(p.date_debut || p.start_date);
- const df = toYMD(p.date_fin || p.end_date);
-
- const appliesToToday = (!pDate || pDate === todayYmd) && (!db || !df || (todayYmd >= db && todayYmd <= df));
- const appliesToTomorrow = (!pDate || pDate === tomorrowYmd) && (!db || !df || (tomorrowYmd >= db && tomorrowYmd <= df));
-
- if (!appliesToToday && !appliesToTomorrow) return false;
-
- try {
- const joursSet = getPerturbationJours(p);
- if (joursSet) {
- const hasToday = joursSet.has(todayYmd);
- const hasTomorrow = joursSet.has(tomorrowYmd);
- if (!hasToday && !hasTomorrow) return false;
- }
- } catch (e) {}
- return true;
- } catch (e) { return true; }
- });
- setPerturbations(filteredPerturb || []);
- setTravaux(travauxData || []);
- }
- } catch (e) {
- } finally {
- inFlight = false;
- }
- };
- fetchPerturb();
- const id = setInterval(fetchPerturb, 15000);
- return () => { mounted = false; clearInterval(id); };
- }, [getPerturbationJours]);
-
- useEffect(() => {
- if (!selectedHoraire || !selectedHoraire.horaire) return;
- const horaire = selectedHoraire.horaire;
- const stop = selectedHoraire.stop || null;
- const stationId = stop ? (stop.gare_id || stop.gareId || stop.id || stop.stop_id || stop.stopId) : null;
- const stationName = stop ? (typeof stop === 'string' ? stop : (stop.gare_nom || stop.nom || stop.name || stop.stop_name || '')) : null;
- try {
- const map = buildPerturbMap(perturbations || []);
-
- const todayYmd = new Date().toLocaleDateString('en-CA');
- const tomorrow = new Date();
- tomorrow.setDate(tomorrow.getDate() + 1);
- const tomorrowYmd = tomorrow.toLocaleDateString('en-CA');
- const serviceDate = selectedHoraire.isTomorrow ? tomorrowYmd : todayYmd;
-
- const status = computeStatusForHoraireAtStop(horaire, stationId, stationName, map, serviceDate);
- const originalBase = parseTimeHM(selectedHoraire.rawTime || horaire.heure_depart || horaire.time || horaire.heure);
- const displayTimeOriginal = (typeof originalBase === 'number') ? formatTimeFromHM(originalBase) : (selectedHoraire.rawTime ? formatTimeNoSeconds(selectedHoraire.rawTime) : (horaire.heure_depart || horaire.time || horaire.heure ? formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure) : null));
- let updatedTime = null;
- if (status && typeof status.delayMinutes === 'number' && !Number.isNaN(status.delayMinutes) && typeof originalBase === 'number') {
- updatedTime = formatTimeFromHM(originalBase + status.delayMinutes);
- }
-
- // Guard to prevent infinite render loop: only set selectedHoraire if status or time actually changed
- const statusChanged = !horaire._status || horaire._status.label !== status.label || horaire._status.delayMinutes !== status.delayMinutes;
- const timeChanged = selectedHoraire._displayTime !== displayTimeOriginal || selectedHoraire._updatedTime !== updatedTime;
-
- if (statusChanged || timeChanged) {
- setSelectedHoraire(prev => ({
- ...(prev||{}),
- horaire: { ...(prev && prev.horaire ? prev.horaire : {}), _status: status },
- _displayTime: displayTimeOriginal,
- _updatedTime: updatedTime
- }));
- }
- } catch (e) {}
- }, [selectedHoraire, perturbations, buildPerturbMap, computeStatusForHoraireAtStop, parseTimeHM, formatTimeFromHM, formatTimeNoSeconds]);
-
- useEffect(() => {
- let mounted = true;
- async function loadNextForQuai() {
- setLoadingHoraire(true);
- setHoraireError(null);
- setSelectedHoraire(null);
-
- const source = getSourceFromPath(window.location.pathname);
- if (source === 'sncf') {
- try {
- const { data: sncfData, error: sncfErr } = await getSncfTimetable({
- gareName: gareDisplayParam || gareParam,
- mode: 'departs',
- quai: quaiParam
- });
- if (!mounted) return;
- if (sncfErr) {
- setHoraireError(sncfErr);
- } else if (sncfData && sncfData.length > 0) {
- const train = sncfData[0];
- const now = new Date();
- const nowHM = now.getHours() * 60 + now.getMinutes();
- const depHM = parseTimeHM(train.heure_depart);
-
- const chosen = {
+ const train = matchingTrains[0];
+ if (train) {
+ const depHM = parseTimeHM(train.heure_depart || train.time || train.heure);
+ chosenTrain = {
horaire: {
...train,
_status: train._status || {
@@ -520,438 +333,314 @@ export default function ClassiquesDepartsQuais() {
}
},
stop: {
- gare_nom: train.gare_depart_nom || gareDisplayParam || gareParam,
- heure_depart: train.heure_depart,
- quai: train.quai || quaiParam,
- quai_reel: train.quai || quaiParam
+ gare_nom: train.gare_depart_nom || gareDisplay || gareParam,
+ heure_depart: train.heure_depart || train.time || train.heure,
+ quai: train.quai || train.voie || quaiParam
},
- depHM: depHM != null ? depHM : nowHM,
- rawTime: train.heure_depart,
- circulatesToday: true,
- circulatesTomorrow: false,
- isTomorrow: false
+ depHM: depHM != null ? depHM : 0,
+ rawTime: train.heure_depart || train.time || train.heure
};
- setSelectedHoraire(chosen);
- setCandidatesCount(sncfData.length);
- setStationType('interurbaine');
}
- } catch (e) {
- if (mounted) setHoraireError(e.message || String(e));
- } finally {
- if (mounted) setLoadingHoraire(false);
}
- return;
- }
-
- if (!supabase) {
- setHoraireError('Supabase non initialisé');
- setLoadingHoraire(false);
- return;
- }
- try {
- let fetchedType = null;
- try {
- let fetchedId = gareIdParam || null;
- if (gareIdParam) {
- const { data: g } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).single();
- if (g) {
- fetchedType = g.type;
- fetchedId = g.id;
- }
- } else if (gareDisplayParam) {
- const { data: gdata } = await supabase.from('gares').select('id,type,nom').ilike('nom', `%${gareDisplayParam}%`);
- if (gdata && gdata.length) {
- const exactMatch = gdata.find(g => {
- const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
- const n2 = String(gareDisplayParam || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
- return n1 === n2;
- });
- const chosen = exactMatch || gdata[0];
- fetchedType = chosen ? chosen.type : null;
- fetchedId = chosen ? chosen.id : null;
- }
- }
- if (mounted) {
- setStationType(fetchedType);
- setResolvedGareId(fetchedId);
- }
- } catch (e) {}
-
- const { data: all, error } = await fetchAllRows(supabase.from('horaires').select('*'));
- if (error) {
- if (mounted) setHoraireError(error.message || String(error));
- setLoadingHoraire(false);
- return;
- }
- if (!mounted) return;
- if (!Array.isArray(all) || all.length === 0) {
- setLoadingHoraire(false);
- return;
- }
-
- const normalizedTarget = gareDisplayParam ? normalizeName(gareDisplayParam) : null;
- const targetQuai = quaiParam ? String(quaiParam).toLowerCase().replace(/\s+/g,'') : null;
-
- const now = new Date();
- const tomorrow = new Date(now);
- tomorrow.setDate(now.getDate() + 1);
- const nowHM = now.getHours()*60 + now.getMinutes();
-
- const candidates = [];
-
- for (const h of all) {
- if (!h) continue;
-
- const circulatesToday = circulatesOn(h, now);
- const circulatesTomorrow = circulatesOn(h, tomorrow);
- if (!circulatesToday && !circulatesTomorrow) continue;
-
- const stops = getStops(h);
-
- let maxIdx = null;
- if (Array.isArray(stops) && stops.length) {
- for (const st of stops) {
- if (typeof st.index !== 'undefined' && st.index !== null) {
- const n = Number(st.index);
- if (!Number.isNaN(n)) maxIdx = maxIdx == null ? n : Math.max(maxIdx, n);
- }
- }
- }
-
- let stopMatch = null;
- if (normalizedTarget) {
- const destNameNorm = normalizeName(h.gare_arrivee_nom || h.gare_arrivee || '');
- if (destNameNorm && (destNameNorm === normalizedTarget || destNameNorm.includes(normalizedTarget) || normalizedTarget.includes(destNameNorm))) {
- } else {
- const departName = normalizeName(h.gare_depart_nom || h.gare_depart || h.origin_name || '');
- if (departName && (departName === normalizedTarget || departName.includes(normalizedTarget) || normalizedTarget.includes(departName))) {
- let initialStop = { heure_depart: h.heure_depart || h.time || h.heure, gare_nom: h.gare_depart_nom || h.gare_depart };
- if (Array.isArray(stops) && stops.length) {
- for (const s of stops) {
- const rawName = (typeof s === 'string') ? s : (s.gare_nom || s.nom || s.name || s.stop_name || s.station || '');
- const nm = normalizeName(rawName);
- if (!nm) continue;
- if (nm === normalizedTarget || nm.includes(normalizedTarget) || normalizedTarget.includes(nm)) {
- if (typeof s === 'object') {
- if (!s.heure_depart && initialStop.heure_depart) s.heure_depart = initialStop.heure_depart;
- initialStop = s;
- } else {
- initialStop.gare_nom = s;
- }
- break;
- }
- }
- }
- stopMatch = initialStop;
- }
- }
- }
-
- if (!stopMatch && gareIdParam) {
- const gid = String(gareIdParam);
- const departId = h.gare_depart_id || h.gare_id || h.origin_id || null;
- if (departId != null && String(departId) === gid) {
- stopMatch = { heure_depart: h.heure_depart || h.time || h.heure, gare_id: departId, gare_nom: h.gare_depart_nom || h.gare_depart };
- }
- if (!stopMatch && Array.isArray(stops) && stops.length) {
- for (const s of stops) {
- const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
- if (sid != null && String(sid) === gid) { stopMatch = s; break; }
- }
- }
- }
-
- if (!stopMatch && normalizedTarget && Array.isArray(stops) && stops.length) {
- for (const s of stops) {
- const rawName = (typeof s === 'string') ? s : (s.gare_nom || s.nom || s.name || s.stop_name || s.stopName || s.station || s.station_name || '');
- const nm = normalizeName(rawName);
- if (!nm) continue;
- if (nm === normalizedTarget || nm.includes(normalizedTarget) || normalizedTarget.includes(nm)) {
- const isTerminus = Boolean((typeof s === 'object') && (s.isLast || s.is_last || s.isTerminus || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx)));
- if (isTerminus) continue;
- stopMatch = s;
- break;
- }
- }
- }
- if (!stopMatch) continue;
-
- const platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.quay)) || h.quai_reel || h.platform || h.quai || '';
- const normPlatform = String(platform || '').toLowerCase().replace(/\s+/g,'');
-
- if (targetQuai) {
- if (!normPlatform) continue;
- if (!(normPlatform === targetQuai || normPlatform.includes(targetQuai) || targetQuai.includes(normPlatform))) continue;
- }
-
- const stopIsObject = stopMatch && typeof stopMatch === 'object';
- const hasDeparture = stopIsObject && (
- typeof stopMatch.heure_depart !== 'undefined' || typeof stopMatch.departure_time !== 'undefined' || typeof stopMatch.departure !== 'undefined' || typeof stopMatch.time !== 'undefined' || typeof stopMatch.departureTime !== 'undefined' || typeof stopMatch.horaire_time !== 'undefined'
- );
- const hasArrivalOnly = stopIsObject && !hasDeparture && (
- typeof stopMatch.heure_arrivee !== 'undefined' || typeof stopMatch.arrival_time !== 'undefined' || typeof stopMatch.arrivee !== 'undefined' || typeof stopMatch.arrival !== 'undefined'
- );
- if (stopIsObject && hasArrivalOnly) continue;
-
- const rawTime = (hasDeparture ? (stopMatch.heure_depart || stopMatch.time || stopMatch.heure || stopMatch.departure_time || stopMatch.departure || stopMatch.departureTime || stopMatch.horaire_time) : (h.heure_depart || h.time || h.heure || h.departure_time || h.departure || h.departureTime || h.horaire_time));
- const depHM = parseTimeHM(rawTime);
- if (depHM == null) continue;
-
- let minutesUntil = depHM - nowHM;
- if (minutesUntil < 0) minutesUntil = depHM + 24*60 - nowHM;
-
- candidates.push({ horaire: h, stop: stopMatch, depHM, minutesUntil, rawTime, circulatesToday, circulatesTomorrow });
- }
-
- if (!candidates.length) {
- setCandidatesCount(0);
- setLoadingHoraire(false);
- setSelectedHoraire(null);
- return;
- }
-
- const todayCandidates = candidates.filter(c => c.circulatesToday && c.depHM >= nowHM - 1).sort((a,b) => a.depHM - b.depHM);
- const tomorrowCandidates = candidates.filter(c => c.circulatesTomorrow && (c.depHM < nowHM - 1 || !c.circulatesToday)).sort((a,b) => a.depHM - b.depHM);
-
- let chosen = null;
- if (todayCandidates.length > 0) {
- chosen = { ...todayCandidates[0], isTomorrow: false };
- } else if (tomorrowCandidates.length > 0) {
- chosen = { ...tomorrowCandidates[0], isTomorrow: true };
- }
-
- if (mounted) {
- setCandidatesCount(candidates.length);
- setSelectedHoraire(chosen || null);
- }
- } catch (err) {
- console.error('Erreur supabase quai', err);
- if (mounted) setHoraireError(err.message || String(err));
- } finally {
- if (mounted) setLoadingHoraire(false);
+ } catch (e) {
+ console.warn('Notice getSncfTimetable:', e);
}
}
- loadNextForQuai();
- return () => { mounted = false; };
- }, [gareDisplayParam, gareParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]);
+ // 2. Si pas trouvé via SNCF, ou si mode Supabase actif, interroger Supabase
+ if (!chosenTrain && supabase) {
+ try {
+ if (gareIdParam) {
+ setResolvedGareId(gareIdParam);
+ } else if (gareDisplay) {
+ const { data: gdata } = await supabase.from('gares').select('id,nom').ilike('nom', `%${gareDisplay}%`);
+ if (gdata && gdata.length) {
+ setResolvedGareId(gdata[0].id);
+ }
+ }
- // Rail is handled purely in CSS for better performance and alignment
- const showPlatform = true;
+ const { data: all, error } = await fetchAllRows(supabase.from('horaires').select('*'));
+ if (!error && Array.isArray(all) && all.length > 0) {
+ const normalizedTarget = gareDisplay ? normalizeName(gareDisplay) : null;
+ const now = new Date();
+ const tomorrow = new Date(now);
+ tomorrow.setDate(now.getDate() + 1);
+ const nowHM = now.getHours() * 60 + now.getMinutes();
- if (!loadingHoraire && !selectedHoraire) {
- return (
-
- );
- }
+ const candidates = [];
- return (
-
- {/* LEFT PANEL: White Background */}
-
- {/* SNCF Logo */}
-
+ for (const h of all) {
+ if (!h) continue;
+ const circulatesToday = circulatesOn(h, now);
+ const circulatesTomorrow = circulatesOn(h, tomorrow);
-
- {(() => {
- const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
- const statusObj = horaire && horaire._status ? horaire._status : null;
- const isSuppressed = statusObj && statusObj.label === 'supprimé';
- const isDelayed = statusObj && typeof statusObj.delayMinutes === 'number';
- const delayMinutes = statusObj ? statusObj.delayMinutes : null;
+ const stops = getStops(h);
+ let stopMatch = null;
- let timeText = horaire ? formatTimeNoSeconds(selectedHoraire.rawTime || horaire.heure_depart || horaire.time || horaire.heure) : mock.time;
- let statusText = horaire ? (horaire.status || (statusObj && statusObj.label) || mock.status) : mock.status;
-
- if (showPlatform) {
- if (isSuppressed) {
- if (!alternateCancelPhase) {
- timeText = 'supprimé';
- statusText = '';
- } else {
- statusText = 'supprimé';
- }
- } else if (isDelayed) {
- if (!alternateDelayPhase) {
- timeText = `retardé +${delayMinutes} min`;
- statusText = '';
- } else {
- statusText = `retardé +${delayMinutes} min`;
+ if (normalizedTarget) {
+ const destNameNorm = normalizeName(h.gare_arrivee_nom || h.gare_arrivee || '');
+ if (destNameNorm && destNameNorm === normalizedTarget) {
+ // Destination finale -> terminus ici, pas un départ
+ continue;
+ }
+ const departName = normalizeName(h.gare_depart_nom || h.gare_depart || '');
+ if (departName && departName === normalizedTarget) {
+ stopMatch = {
+ heure_depart: h.heure_depart || h.time || h.heure,
+ gare_nom: h.gare_depart_nom || h.gare_depart,
+ quai_reel: h.quai_reel || h.voie || h.quai
+ };
+ }
+ if (!stopMatch && Array.isArray(stops)) {
+ for (const s of stops) {
+ const sName = normalizeName(typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''));
+ if (sName === normalizedTarget) {
+ stopMatch = s;
+ break;
+ }
}
}
} else {
- timeText = '--h--';
- statusText = '';
+ stopMatch = {
+ heure_depart: h.heure_depart || h.time || h.heure,
+ gare_nom: h.gare_depart_nom || h.gare_depart,
+ quai_reel: h.quai_reel || h.voie || h.quai
+ };
}
- const isTimeSuppressed = isSuppressed && !alternateCancelPhase;
- const isTimeDelayed = isDelayed && !alternateDelayPhase;
+ if (!stopMatch) continue;
- const isLongText = timeText.length > 5;
- let timeClass = '';
- if (isLongText) timeClass += ' aff-quais-departs-redesign__time--long-text';
- if (isTimeSuppressed) timeClass += ' aff-quais-departs-redesign__time--suppressed';
- if (isTimeDelayed) timeClass += ' aff-quais-departs-redesign__time--delayed';
+ const platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.voie)) || h.quai_reel || h.voie || h.platform || h.quai || '';
+ const platClean = cleanPlatform(platform);
- let statusClass = '';
- if (isSuppressed) statusClass += ' aff-quais-departs-redesign__status--suppressed';
- if (isDelayed) statusClass += ' aff-quais-departs-redesign__status--delayed';
-
- return (
- <>
- {timeText}
- {statusText && {String(statusText).toLowerCase()} }
- >
- );
- })()}
-
-
-
- {(() => {
- const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
- return showPlatform ? (horaire ? (horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination || '') : mock.destinationBig) : '--';
- })()}
-
-
-
- {(() => {
- const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
- const trainType = horaire ? (horaire.trainType || horaire.train_type || horaire.type_train || horaire.op || horaire.operator) : mock.trainType;
- const trainNumber = horaire ? (horaire.numero_train || horaire.numero || horaire.number || '') : mock.trainNumber;
- return showPlatform ? (
- <>
- {trainType}
- {trainNumber}
- >
- ) : '';
- })()}
-
-
- {/* Accessibility Icons */}
-
-
- {/* Watermark */}
-
départ
-
-
- {/* RIGHT PANEL: Blue Background with Stops */}
-
-
- {(() => {
- if (!showPlatform) return null;
- const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
- let stops = horaire ? getStops(horaire) : [];
- if (!Array.isArray(stops) || stops.length === 0) {
- if (horaire && typeof horaire.gares_desservies === 'string') {
- try { const p = JSON.parse(horaire.gares_desservies); if (Array.isArray(p)) stops = p; } catch (e) { /* ignore */ }
+ if (targetQuaiClean) {
+ if (!platClean || platClean !== targetQuaiClean) {
+ continue;
}
}
- if (!Array.isArray(stops) || stops.length === 0) stops = [];
- const labeled = stops.map((stop, idx) => {
- let label = '';
- if (typeof stop === 'string') label = stop;
- else if (stop && typeof stop === 'object') {
- label = stop.gare_nom || stop.nom || stop.name || stop.stop_name || stop.stopName || stop.station_name || stop.station || stop.label || stop.stop || stop.name_gare || (stop.properties && (stop.properties.name || stop.properties.stop_name)) || '';
- if (!label && stop.stop && typeof stop.stop === 'object') {
- label = stop.stop.name || stop.stop.stop_name || '';
- }
- }
- label = String(label || '').trim();
- return { label, original: stop, idx };
- }).filter(it => it.label);
+ const rawTime = (stopMatch && typeof stopMatch === 'object' && (stopMatch.heure_depart || stopMatch.heure || stopMatch.time || stopMatch.heure_passage)) || h.heure_depart || h.time || h.heure;
+ const depHM = parseTimeHM(rawTime);
+ if (depHM == null) continue;
- if (labeled.length === 0) return null;
-
- let matchedIndex = -1;
- try {
- if (selectedHoraire && selectedHoraire.stop) {
- const target = selectedHoraire.stop;
- const targetName = (typeof target === 'string') ? normalizeName(target) : normalizeName((target.gare_nom || target.nom || target.name || target.stop_name || target.station || ''));
- const targetId = (typeof target === 'object') ? (target.gare_id || target.gareId || target.id || target.stop_id || target.stopId || null) : null;
- for (let i = 0; i < stops.length; i++) {
- const s = stops[i];
- if (targetId != null) {
- const sid = (s && typeof s === 'object') ? (s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null) : null;
- if (sid != null && String(sid) === String(targetId)) { matchedIndex = i; break; }
- }
- const name = (typeof s === 'string') ? normalizeName(s) : normalizeName((s && (s.gare_nom || s.nom || s.name || s.stop_name || s.station || '')) || '');
- if (name && targetName && name === targetName) { matchedIndex = i; break; }
- }
- }
- } catch (e) { /* ignore */ }
-
- let subsequent = labeled.filter(it => (matchedIndex >= 0) ? (it.idx >= matchedIndex) : true);
- subsequent = subsequent.sort((a,b) => a.idx - b.idx);
- if (subsequent.length > 1) {
- const collapsed = [];
- for (const it of subsequent) {
- const prev = collapsed.length ? String(collapsed[collapsed.length-1].label).trim().toLowerCase() : null;
- const cur = it.label ? String(it.label).trim().toLowerCase() : '';
- if (cur && prev && prev === cur) {
- continue;
- }
- collapsed.push(it);
- }
- subsequent = collapsed;
- }
- if (subsequent.length === 0) return null;
-
- return subsequent.map((item, mapIndex) => {
- const isLast = mapIndex === subsequent.length - 1;
- const stop = item.original || {};
- const stopId = stop.gare_id || stop.gareId || stop.id || stop.stop_id || stop.stopId || null;
- const stopStatus = computeStatusForHoraireAtStop(horaire, stopId, item.label, perturbMap, serviceDate);
- const isStopSuppressed = stopStatus && stopStatus.label === 'supprimé';
- const isStopDelayed = stopStatus && typeof stopStatus.delayMinutes === 'number';
- const stopDelayMin = stopStatus ? stopStatus.delayMinutes : null;
-
- let stopTextClass = '';
- if (isStopSuppressed) stopTextClass += ' aff-quais-departs-redesign__stop-text--suppressed';
- if (isStopDelayed) stopTextClass += ' aff-quais-departs-redesign__stop-text--delayed';
-
- return (
-
-
-
- {item.label}
- {isStopSuppressed && (non desservi) }
- {isStopDelayed && (+{stopDelayMin} min) }
-
-
- );
+ candidates.push({
+ horaire: h,
+ stop: stopMatch,
+ depHM,
+ rawTime,
+ platClean,
+ circulatesToday,
+ circulatesTomorrow
});
- })()}
-
+ }
+
+ // Filtrage strict par quai si demandé
+ let filteredCandidates = candidates;
+ if (targetQuaiClean) {
+ filteredCandidates = candidates.filter(c => c.platClean === targetQuaiClean);
+ }
+
+ const todayCandidates = filteredCandidates.filter(c => c.circulatesToday && c.depHM >= nowHM - 1).sort((a, b) => a.depHM - b.depHM);
+ const tomorrowCandidates = filteredCandidates.filter(c => c.circulatesTomorrow && (c.depHM < nowHM - 1 || !c.circulatesToday)).sort((a, b) => a.depHM - b.depHM);
+
+ const chosen = todayCandidates.length > 0
+ ? todayCandidates[0]
+ : (tomorrowCandidates.length > 0
+ ? tomorrowCandidates[0]
+ : (filteredCandidates.length > 0 ? filteredCandidates.sort((a, b) => a.depHM - b.depHM)[0] : null));
+
+ if (chosen) {
+ chosenTrain = chosen;
+ }
+ }
+ } catch (err) {
+ console.error('Erreur chargement horaire Supabase quai:', err);
+ }
+ }
+
+ setSelectedHoraire(chosenTrain);
+ setLoadingHoraire(false);
+ }, [gareDisplay, gareParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, cleanPlatform, circulatesOn, getStops]);
+
+ useEffect(() => {
+ loadNextForQuai();
+ const interval = setInterval(loadNextForQuai, 15000);
+ return () => clearInterval(interval);
+ }, [loadNextForQuai]);
+
+ // Données actives (réelles ou maquette Image 1 de secours)
+ const horaire = selectedHoraire?.horaire;
+ const rawDepartureTime = selectedHoraire ? (selectedHoraire.rawTime || horaire?.heure_depart || horaire?.time) : (gareParam ? null : mock.time);
+ const formattedTime = rawDepartureTime ? formatTimeNoSeconds(rawDepartureTime) : '';
+
+ const statusObj = horaire?._status;
+ const isSuppressed = statusObj?.label === 'supprimé' || horaire?.etat === 'supprime';
+ const isDelayed = (statusObj && typeof statusObj.delayMinutes === 'number' && statusObj.delayMinutes > 0) || (horaire?.retard_minutes > 0);
+ const delayMinutes = statusObj?.delayMinutes || horaire?.retard_minutes || 0;
+
+ let displayStatusText = "à l'heure";
+ if (isSuppressed) {
+ displayStatusText = 'supprimé';
+ } else if (isDelayed) {
+ displayStatusText = `retardé +${delayMinutes}min.`;
+ }
+
+ const destinationName = horaire ? (horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination || '') : (gareParam ? '' : mock.destination);
+ const trainType = horaire ? (horaire.type_train || horaire.train_type || horaire.op || horaire.commercial_mode || 'TER') : (gareParam ? '' : mock.trainType);
+ const trainNumber = horaire ? (horaire.numero_train || horaire.numero || horaire.number || '') : (gareParam ? '' : mock.trainNumber);
+ const operator = horaire ? (horaire.operator || horaire.op || '') : '';
+
+ // Résolution dynamique du logo couleur
+ const brand = getTrainBrand(trainType, operator, true);
+
+ // Calcul de la liste des arrêts pour le panneau droit
+ let stopsList = [];
+ if (horaire) {
+ const rawStops = getStops(horaire);
+ if (Array.isArray(rawStops) && rawStops.length > 0) {
+ stopsList = rawStops.map(s => (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''))).filter(Boolean);
+ }
+
+ // Filtrer les arrêts à partir de la gare courante
+ if (gareDisplay && stopsList.length > 0) {
+ const normCur = normalizeName(gareDisplay);
+ const foundIdx = stopsList.findIndex(s => normalizeName(s) === normCur);
+ if (foundIdx >= 0 && foundIdx < stopsList.length - 1) {
+ stopsList = stopsList.slice(foundIdx + 1);
+ }
+ }
+
+ // Garantir que le terminus est présent en dernier
+ if (destinationName) {
+ const destNorm = normalizeName(destinationName);
+ const lastNorm = stopsList.length > 0 ? normalizeName(stopsList[stopsList.length - 1]) : '';
+ if (lastNorm !== destNorm) {
+ stopsList.push(destinationName);
+ }
+ }
+ }
+
+ // Fallback propre
+ if (!stopsList.length) {
+ if (horaire && destinationName) {
+ stopsList = [destinationName];
+ } else if (!gareParam) {
+ stopsList = mock.stops;
+ }
+ }
+
+ // 1. Détection "Départ en cours" (2 minutes avant l'heure de départ réelle)
+ let isDepartEnCours = isForceDepartEnCours;
+ if (!isDepartEnCours && selectedHoraire && rawDepartureTime) {
+ const depHM = parseTimeHM(rawDepartureTime);
+ if (depHM != null) {
+ const nowSec = currentTime.getHours() * 3600 + currentTime.getMinutes() * 60 + currentTime.getSeconds();
+ const depSec = (depHM + delayMinutes) * 60;
+ let diffSec = depSec - nowSec;
+ if (diffSec < -43200) diffSec += 86400;
+ if (diffSec <= 120 && diffSec >= 0) {
+ isDepartEnCours = true;
+ }
+ }
+ }
+
+ // AFFICHAGE 1 : Page Vidéoprotection (via query)
+ if (isVideoprotection) {
+ return
;
+ }
+
+ // AFFICHAGE 2 : Départ en cours (T - 2 min)
+ if (isDepartEnCours) {
+ return
;
+ }
+
+ // AFFICHAGE 3 : Alternance si aucun train n'est attribué à ce quai
+ if (!selectedHoraire && gareParam) {
+ if (idlePhase === 0) {
+ return
;
+ }
+ if (idlePhase === 1) {
+ return
;
+ }
+ if (idlePhase === 2) {
+ return
;
+ }
+ }
+
+ return (
+
+ {/* PANNEAU GAUCHE : BLANC (Conforme Image 1) */}
+
+ {/* Logo Couleur Dynamique */}
+
+
+ {brand.showVoyageursText && (
+
VOYAGEURS
+ )}
+
+
+ {/* Heure et Statut */}
+
+
+ {formattedTime || '--:--'}
+
+
+ {displayStatusText}
+
+
+
+ {/* Destination Principale */}
+
+ {destinationName || 'Aucun départ prévu'}
+
+
+ {/* Type de Train & Numéro */}
+
+ {trainType && {trainType} }
+ {trainNumber && {trainNumber} }
+
+
+ {/* Filigrane Vertical "départ" géant (orienté bas vers haut) */}
+
+ départ
+
- {/* BOTTOM FOOTER: Full Width */}
-
-
- {quaiParam ? `Voie ${quaiParam}` : (selectedHoraire?.horaire?.quai ? `Voie ${selectedHoraire.horaire.quai}` : (selectedHoraire?.stop?.quai ? `Voie ${selectedHoraire.stop.quai}` : ''))}
+ {/* PANNEAU DROIT : BLEU NUIT AVEC DESSERTE (Conforme Image 1) */}
+
+
+ {stopsList.length > 0 ? (
+
+ {stopsList.map((stopName, idx) => {
+ const isLast = idx === stopsList.length - 1;
+ return (
+
+
+
+ {stopName}
+
+
+ );
+ })}
+
+ ) : (
+
+ Veuillez consulter les écrans du hall de la gare.
+
+ )}
-
-
+
+
+ {/* BANDEAU BAS : PLEINE LARGEUR & HORLOGE (Conforme Image 1) */}
+
+
+ {quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai ? `Voie ${cleanPlatform(horaire.quai).toUpperCase()}` : '')}
+
+
+
diff --git a/src/services/sncfApi.js b/src/services/sncfApi.js
index 8b40de5..db846a6 100644
--- a/src/services/sncfApi.js
+++ b/src/services/sncfApi.js
@@ -550,6 +550,14 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
resultList = remainingToday;
}
+ if (resultList.length === 0) {
+ resultList = (enrichedToday.length > 0 ? enrichedToday : rawList.map((t, idx) => mapTrain(t, idx, false))).sort((a, b) => {
+ const aHM = (mode === 'arrivees' ? a._arrHM : a._depHM) || 0;
+ const bHM = (mode === 'arrivees' ? b._arrHM : b._depHM) || 0;
+ return aHM - bHM;
+ });
+ }
+
return resultList;
} catch (err) {
console.warn('[sncfApi] Erreur de chargement GTFS station:', err);
@@ -587,19 +595,14 @@ export async function getSncfTimetable({ gareName, mode = 'departs', quai = null
finalTrains = [];
}
- // 3. Filtrage par quai si demandé
+ // 3. Filtrage strict par quai si demandé
if (quai) {
- const normQuai = String(quai).trim().toLowerCase();
- const matching = finalTrains.filter(t => String(t.quai || t.voie).trim().toLowerCase() === normQuai);
- if (matching.length > 0) {
- finalTrains = matching;
- } else if (finalTrains.length > 0) {
- finalTrains[0].quai = quai;
- finalTrains[0].voie = quai;
- finalTrains[0].quai_reel = quai;
- finalTrains[0]._displayPlatform = quai;
- finalTrains = [finalTrains[0]];
- }
+ const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
+ const targetP = cleanP(quai);
+ finalTrains = finalTrains.filter(t => {
+ const q = cleanP(t.quai || t.voie || t.quai_reel || t._displayPlatform);
+ return Boolean(q && q === targetP);
+ });
}
return { data: finalTrains, error: null };