From 9c6ac798f1ef2e5031ee0d61e6c24e971f88cc19 Mon Sep 17 00:00:00 2001 From: mathis Date: Mon, 31 Aug 2026 02:02:46 +0200 Subject: [PATCH] =?UTF-8?q?RELEASE=20V5.5.0=20:=20Voici=20les=20modificati?= =?UTF-8?q?ons=20apport=C3=A9es=20:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -> REFONTE TOTALE DE TOUS LES AFFICHEURS CLASSIQUES + QUAIS FONCTIONNALITÉS EN COURS DE DÉVELOPPEMENT : -> ADAPTABILITÉ EN FONCTION DES SITUATIONS --- public/type/data.json | 48 +- public/type/quais/data-color.json | 126 +- public/type/quais/sncf-voyageurs-logo.png | Bin 0 -> 9597 bytes src/App.js | 18 + .../afficheurs/AttentifsEnsemble.css | 202 +++ .../afficheurs/AttentifsEnsemble.js | 112 ++ .../afficheurs/DepartEnCoursAFL.css | 90 ++ src/components/afficheurs/DepartEnCoursAFL.js | 63 + .../afficheurs/DepartEnCoursClassique.css | 75 + .../afficheurs/DepartEnCoursClassique.js | 81 + .../afficheurs/GareVideoprotection.css | 78 + .../afficheurs/GareVideoprotection.js | 68 + src/pages/SelectAfficheur.js | 67 + src/pages/afficheurs/afl/AFLArriveesQuais.js | 45 +- src/pages/afficheurs/afl/AFLDepartsQuais.js | 68 +- .../classiques/ClassiquesArrivees.js | 6 +- .../classiques/ClassiquesDeparts.js | 6 +- .../classiques/ClassiquesFlashInfo.css | 215 +++ .../classiques/ClassiquesFlashInfo.js | 117 ++ .../classiques/ClassiquesInformations.css | 133 ++ .../classiques/ClassiquesInformations.js | 120 ++ .../quais/ClassiquesArriveesQuais.css | 420 +++--- .../quais/ClassiquesArriveesQuais.js | 1301 ++++++---------- .../quais/ClassiquesDepartsQuais.css | 421 +++--- .../quais/ClassiquesDepartsQuais.js | 1323 +++++++---------- src/services/sncfApi.js | 27 +- 26 files changed, 3040 insertions(+), 2190 deletions(-) create mode 100644 public/type/quais/sncf-voyageurs-logo.png create mode 100644 src/components/afficheurs/AttentifsEnsemble.css create mode 100644 src/components/afficheurs/AttentifsEnsemble.js create mode 100644 src/components/afficheurs/DepartEnCoursAFL.css create mode 100644 src/components/afficheurs/DepartEnCoursAFL.js create mode 100644 src/components/afficheurs/DepartEnCoursClassique.css create mode 100644 src/components/afficheurs/DepartEnCoursClassique.js create mode 100644 src/components/afficheurs/GareVideoprotection.css create mode 100644 src/components/afficheurs/GareVideoprotection.js create mode 100644 src/pages/afficheurs/classiques/ClassiquesFlashInfo.css create mode 100644 src/pages/afficheurs/classiques/ClassiquesFlashInfo.js create mode 100644 src/pages/afficheurs/classiques/ClassiquesInformations.css create mode 100644 src/pages/afficheurs/classiques/ClassiquesInformations.js 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 0000000000000000000000000000000000000000..88d4d343249ac8425b9085e83e263ec2b5cd4ae3 GIT binary patch literal 9597 zcmd6M2UJtr*6yYU2@pUr^aK=83?z_1=)I^kMHCA{NJ266P(nutRf+`_r71QnGz9@E zQbMqxs3<|CgMb3k5tZ_GJV)<2cicPPfA9Fm8;>zU_FikQZ_fFxS=Qc3vA4Ap+$6aP z006<=R%ZJFfU_9@INm^b!I80D#RC8Uwe&JIwKwyk`g;??0KlrP_>ji|hxg(JRoBnH z;uJY88ZLI@&<+!C%(yck~aae(lYn0y>Q};P7WR0waFlZ+XM&#|XaHF0F)ndx8_Es40F8UU z?LW)`n7I^Hl#XHQ(_` zN1|R23mI}A!Vm?5p1}C#%=I`J`(T+*I|^}jEB7Othixg$@an1N0HvF%cM0Bq89)pnQtd(*W2=BY)b zO8pbhE>c}P2G8*p-HqKUE?B;UA#%2^8=h{Uu~)h&0bT;DusYUg^JYa=VGK&(1vz(GEk?zl^V%Oej zX4{k}i|KkRIBZj1s5;s0qMQPCTp)HnAc^oxMjUR4tUm6YV%47hZtybazWrH=9$O1; zJsW@wAG}J>(`#4AvdfStF-JfJ4zV(vVSCQvmfZT@#J%E9pV{kmypAb*Ytw@K`EZqF zn~w=8k}i5hfT|VFP*w$%Y`1?r#baz%k#TpS>68(4Fc#g&xe2e{WPO^8kr#!En!OmV z$T=GeC2=ABUwbC%?|0M?rP*+|4hkpJjSPmj-nqOb&ZKAqTj=QGY;@toL;H_s1Z0hK zg$s>kSBve#c3TwQh)uFrShox^XvYZ)ee5+1_AW|!?YX(7ZdV>*uNxkd1~*Sb=nN~; zV6w4ojTDIVmuj^yTDj9M%XCY+YI9Wc789)@CNv;r>TbA`tNcWwh&&eO5q((0m+x@> zti2v=-qbfYB?s8rD`w#~(u97v^=%;blFD6=wWK|kCsqn4C3RC(cy6Q>((R=imfwcF zS(YU1TV8xFbesA@`jd(WX>4*OS4{^M`*oreRldF3U}{^)X5Dld#t}e3H?Gocyv%fu z-u+bBI|pC;S8zKT)SR|l&7|qsSd*1LmZX=|m&m=N@-)kT{3_z_zGwUi<9#8gxK#AF zgk2?c!w~56zIT0FfR7u=-!+y}Z-0+5%Jx`)b6y%ioYuu+gJ#v77fXSuyWFwi(`tF9 zimT$89D@f0U63jVCjCDq07XW84f~jl(oS4_PMNy1yc`H_dOSZuXxm9FS|;Q$H|qhk zop+(p&7OB>at$|!M zNsIDoXM~j#-`I1#g;tujU2Ra7O1C)o%F9zzM*wB9_chrQcO>(RNzUtQ6NeW!^`sk^ z@}XnB2-fYQ)TT&`ka*G!;^A!Q(b)4uM<@L*dS%miB>yDyd)ZgKh{xcrUY?RcWj%@U zkq5IpwC%^^XBuiy3U21$vCZg zHr#6F$q|ykeO>LiT-yt$_danP%^PhQJvG`kYMiRtdSTcu;$hX?xPl+y8Z60;%ENPKA%-kD3l7yuz2?ow_@`n?~X)N*n0JKhTC=C(teGv zxQ*rUk?_$i&I^fOPPM)D;kD_+r$x^1>fu~#@HN%7JWbPxdzy!iUa7aQJy?7AN8vJ6 zCN-m2=%~jd+j86UFAee*bfO+@cGh*yP_+v^>0A7TqmS~4D;%n?RgbTatlv|g7k+Cg zf8yNY^kT~r&-WYOk1l3@GIVu?Qh1Y^JR)1VpW&M(FbK(RDSyF0=uT*~`UG}D>#f!z z)*7=*{T@z_)ct~y9b&Djky(>lb204vH$v8X3;V}ia-)L-+vTzJ#=CjU*U zO;vWjh74Lm>lF6fD#)TN&UB`+eh>(yEornR9;T_LUx4M5UzjL_l z@Y;o&7xrD)^!&wzybJLMmk&NYxcy*l@fRnD;;7;W_gGG$PG3Jbbz4_l^62!Wc3yCs zq^R6?_K0}1T%1?$e5Wro1QlX8l|EIo+`62;TqQFh)Fkvk`m>CxuBc3u3{^%8f85_B zuP)cjw$gSyZ=Z*sNBIKpcnOA%i6(9%er`KPy;@V|yvbS8`Aa%EBVFRC`a|;6LV)JT zo3>)<(7;}gLuxENR&hyH?_!@~_4(?`n);y7!0}qvBbVhX5oWYqi=Iogp-)pg7JDbc z`c4huUQgX3<)mETU&?%dkV4iqBDL8R5Nhyg%A4o4teUjmsHj%lQnk)5Q z);u{iDpkEE7OB`?;IDUCJP+*jZ4w?)q7U( zDrkyh*SV?2sdw+|3tjh}FS2VFVI{kantk+s7M3G*u~t7+SqEAS$vxHav12;hL)&kzE@I*Rf|i<*I7xNF$+NyJh|p2^#9UY0 zVe7c+dOf3(UuXs)(vl;|ed^<%WQ)s)#_h;Uiycn&z zHLKvZe{s>B6{n}AdJFX*zP=j}Mm;~$Rnr^sjk$%feS%lG&>&fVd=t5o89 zyyb{P!`M=f1jX)9*g>Bomd>}FSk5vIeUJLx*HV|{C}y}rCrl}!tV>;};)q(~vY}z- z>2#Z%c1=g`@3ZkMNs~DWT3W0R?+*DOeRuTEds3ctoiqHt-d*;BbhP!M-G}WTYD2e8 z;rjwrBKBMVJ;fv{kmuvV8nghlHU^pHzJnv0$d* zri*vX?GGK@F8yiP4zDkMB9%c^H!DMAvi+}ILtXLNC?&@9MZcPNokq5pJ{NWnz9=iJ zvpMnjp%AX7mp5NAerfT&tIK=2@R+yyp|thq1<08E z$%5X21(RjB3GzpkkwYI`A{xHHljW*^C~mwM{4gKNoC&G>8Wl5d`Z=jB{(<3HgM%@F zt8bUJzb{N*UA>CjVaIgv;X&hH-P;8m_vMSt=LJSMdkwAk6)YTxE9$tplY3q2U;&Tg zu%MAx-Hxis+z?>7k-#d^ISYI|sK0~zima@Hslxm&;ZWRG!F(EICNc5&B@Uw#T!ZJl zJw5$fUcI_ijD9Z#RCMewP&;oz_lTF%J3k?>;kUpVuZ!Wj92Aq}qId!VK3VYlTOFnX zfcSRyhhz8tZL*IC6DagsjT8HT8qe>w8xsg?AKK`II0O%Rg{0T$~ISAoK_VDu6 zm!GM9ERXOa>B}F&*`RIwP05~KR^fqUhj7~i#Bd5xn*~4n`h@T=s6n%MPwnBuHjXlEDFOZDTR7InRXtX*)OIsDAiN)gayAbMV zb+j5`nF|`vy@#S-d+p`1t%*K@Bnw0m=E3 z=6@+oJrL$kR@+ae`UM9P$>t%TnF{gs9&_rwgT5sk@3}y!fd;K3-NAe?r82>Oz8-v#% zxS??CW?f1S0`x_wbk9!-F~5gdj%#w!TYbMY^z8hA_0rWqP6jO z6wzHBgTi95I23`1A)zn?Ei&324Kj$vveWBlZ0T5eQG@&f!~RTI2XerlSKeNTpW&oK zApQ{%R6+=uB>(52*S}!nf28-%^`V|*Fv$KXGXA>Vf9R(Axd(+30?Ec6AalQplKrnl zLa3{&q7lkA1frKOJGFNFvX1CU@bw^rvMu}ofY!m=TPlw^)HYF1S*cdC~ZOw2H1VTIs1?lGoA;CRe zr65kZfkJ45)K&~A1^Xqmk1b?2r!~x8ll*IqGJ8$oiVHvK=bF>zKi9M-aC`l zACYGMQ&(#tTUWB%K4|G|u?X-=AvNj;x874R0x+@Vmx{t}|He=QNe)1xu-j5;P{0m^ zIS7lH&w?YpA%M|lDIvxA*ASr5QVGspIQSK@KDq+~hB>S?F+L*_%L&jF*~S4QZceby z)JzKi=-{1GlDh=@2Y}%A>Fl9H=H;2T_NhwEK;t{()u#?_`xijFyG2@0XRyag;LG&v z19~F7K!`>8Ee6Pw5sd9OH^zHRqZu$t1eH=5-XkD6r%|c;+buxsIJR$STryiXk>~_d z08`NzdWh|)Q7b5aRFQTG#5P(|UIu-E0uVQlnOFpy)}NP+_kLcdr4x+PeyaMZm5sv9 zb`K==yz{;uNK&AnT?`f$7N=vp| z|9hsTLfIwP=!gcEN`qZbC2s{7#)T*EQKz4lP42pjZa|GQgSXS0hM=ab3JMh7W~l&! z>IX%dko&(D^AYREk8sh^_$y5~_j@N7z>rE}*1NU*NE@Rxis4!C1N{Qd9wzW3e$oLW z8yo5`-MVp#6=1dD3LoZ}7@U@9QF^Rdd=#)esoi9d0^^n1RsGaD0%dWoyvn0y4A$I@ zmEI<(#z!^xK@FY1s1+^dqZmBC7vK_NMj)RWgrte?dHp}P^jaP^()^FD}f#-KK zyh-)VC$>8|jBTQFo4}uWPsOb?Xj5ri2>MmPxYt8A;DK8`pD@0HuS+R|p~^>!=|LtB6x{kWfJ&26HG-JFhnI*lgxqzUsjda)kP%mL25QWL z#^K+aghYh-JEpX6pMBFY(}Oj;XB7qYWZ!C0`&1725@2CU9Q9a+e}%83*WcG2#pC#N=Sa68)eR@ue}ubO@e5PsdBX zIGj)>Vul{S*|VfdziP8Nv_EZRcrMVB6G1nIT}&k)`G1t^8$sd`wt)!C_-`V3*MeX! z1_~?~t0KqVY1Y2mMmu?2tK1%Re|By5HVr725_)TeRgz$e9Bjb}o4`HA7(xeMLt|ly z0mgbC@4n+!b+u#THJ}m|hE{apm(b(BPR48nuoj87;}f7_f_)ahc3G*bnmNSuk?Gx$ zPRFG378$Sr--xH-WF|XDl;r#HVD{tTUWcRy^H)=Kr{t1o2jv$E=kBzM=ck zAdU8h$}07?ltc%;E!;@xW+AD9Ef%9EDO^aXWgKc#okhU64Zh)?Nc3d^truWxlV&Ik z4~<65jS7G*+mowzn`?gp8j+a(>H62f!Ky@dWd@}Nx7ls#n9u611M6u3(V7S7QU#0U za^kBm_C0I30N+jM83-XZC;kmZoEtQfmp3scddoMr?m95Gjj$<%83|bPu~5(46|sub z+r<-*U3Y>rV2Os!a4A#?1wghxDO+lpqAj zHm^jM2v>it>4g)RW8WS%hb6)i)pHWl(^XGS)Gy~KAS4DPIfM>}x1Yg*0bP@F%SIkf z;mGv@&p#`!HmN^h|UFG*( z$dOOEa?Al%8)6J;5#)=De}~*tA3X6WxGxTtm?N|)=^ocxX;%33^J8yfAMhcqADNN` zqXu#%V)(97x|TLU2>azU@6JUKEud{zC?PerI~r}!CUCxDt}P3r;g!4q()yzR;oZmi zX1jy$A1eSiKS{3IokM6|=>kXW%SZ!u(*u10vKu>H!S4O*7;WTWAb#@CKFFT-eAXrp zHtXM~Yw&0Te4qXHpDY1Y|8fq?_vNw`GOO0l?M++|4YaHKMdgd?Hb!ocKc_$4JP1kO8zr}SkO|Ac!taR zS8{Th5V9|L=<=YZihNr0J=5ivHXlbjqL-FPwEBRFN>WZ~M$(X@)YQ(^o(K0nIY#DL zG~t3+KSY=iLVv$g*H!SY&Oe0u;shK zC5xGoG=>mcq-79}Zum*HCyvfshr6!0Z02Nfm}GPEec!npe)sXsok@GD-KEznj*SoL zT;Ytdzc$lX9&Z=3yUQd`Dt(uWDa5x&q}hL;E14!)m%lRLvbH+yII}RC z-oEqU`oTB2jnCaBe9^Xv+M)~wEyo7#w$li4s% ztZT57@+#OSfjX2CHU5>-92^T9qn$RJXVF?M!U7*uz8*iCbgS!1i(JI1dJ$xr&E%7g z-fwNHB^g+|_#gKok_HN;PO}fMM1nO~%m)JxVO@wU=MKkp+^IzHv=Hi3C=)-p`1J?# z%F;;Bv#7jQbIRq(F@;XKuCU<8n5IkyEvQ?+#`CV3JrX7MF(A{iaK8-VdLA3-AwAz8_ z4>W0OT{pj)Gh7iFQdXdu`X#bNDwp1Uy93{zC%eIs?LIkY1et=C@4k|zO&X%eYt=K? zM8WEI-sLQ{k&G|8r&hG(_N^Fk2!Y8k%YNQPU71pQLfb=HI~yU}l{voQ|I+aX*XnQt zT||s@#B|?keM8`t(zp?X8?|Hi=IFJTG7rI{5#Kc_u7&e{J}yhw(=Pp(*|~oFUIh3Y zx|G0@WU|uZzj{aI42aL{22Bn?>+Fh+V+ADY?=y>oF^!etiutr`xcUnwCTrbapiHvg z<~-lCawOqy{5#3f&owDylU{=fOyZN3s?6XFpP*wz2r{wi#b@!MfqH613 zL)WfuXUUqtlh(%sUo%yDG-RH+4fZbSkf#(rb+>GfjFNQGx+vr#EvL0!)oS#Tb(TEc zU&Cu|l&_89W9F-#!ZvH=zUsfbQOBpmx)SVZhhApNva1|xK>~bp@?Y1ve_1KP!^HoW e#d)xFox=<(9~WB5pTYh$W4F1jS&4~j-2VX@G*F2E literal 0 HcmV?d00001 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) */} +
+
+
ATTENTIFS
+
ENSEMBLE
+
+ + {/* 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 */} +
+
+
DÉPARTS
+
EN COURS
+
+ +
+

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 ? ( + Train au départ 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. +
+
+
+ + +
+ + {/* 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}. +
+
+
+ + +
+ + )}
{/* 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) */} +
+
+ information +
+
+ + {/* 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 */} - SNCF + 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.label} + {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) */} +
- {/* 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 ( -
-
Aucun Départ Prévu
-
-
-
- -
-
-
- ); - } + const candidates = []; - return ( -
- {/* LEFT PANEL: White Background */} -
- {/* SNCF Logo */} - SNCF + 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.label} + {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) */} +
- {/* 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 };