Compare commits
8
Commits
d7d4f9cecf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7334a2d9f7 | ||
|
|
8f00da0896 | ||
|
|
23828a54ef | ||
|
|
eb198cc727 | ||
|
|
06cce59f82 | ||
|
|
d0de9978e8 | ||
|
|
6cc1e1820d | ||
|
|
c30a0e9002 |
No files matched your search
@@ -1,80 +0,0 @@
|
||||
# 🚅 Afficheurs Ferrovia
|
||||
|
||||
Application web de téléaffichage ferroviaire et d'information voyageurs en temps réel (PIDS - Passenger Information Display Systems), reproduisant fidèlement les écrans d'affichage des gares françaises (SNCF).
|
||||
|
||||
---
|
||||
|
||||
## ✨ Fonctionnalités
|
||||
|
||||
- **📡 Données Réelles & Complètes :**
|
||||
- Horaires théoriques issus de l'archive officielle **GTFS SNCF** (3 507 gares, 41 847 circulations : TGV INOUI, OUIGO, TER, INTERCITÉS, ICE, TGV Lyria).
|
||||
- Retards, perturbations et suppressions en direct via **GTFS-RT**.
|
||||
- Détection et affichage des **voies et quais réels** via les flux **SIRI-Lite**.
|
||||
- **🖥️ Multiples Générations d'Afficheurs :**
|
||||
- **AFL (Afficheurs Fluides & Lumineux) :** Écrans modernes pour halls et quais (Départs / Arrivées).
|
||||
- **Classiques :** Écrans d'affichage historiques (Départs, Arrivées, Écrans de Quai avec liste des arrêts desservis).
|
||||
- **EVA :** Écrans vidéo voyageurs enrichis.
|
||||
- **🔊 Module d'Annonces Sonores :** Jingles et annonces synthétiques en temps réel.
|
||||
- **🔍 Recherche Intelligente des Gares :** Reconnaissance instantanée des gares, codes UIC, variantes d'écriture et abréviations (`Valence-TGV`, `Paris-Montparnasse`, `Lyon-Part-Dieu`, etc.).
|
||||
|
||||
---
|
||||
|
||||
## 📋 Patchnotes / Historique des Versions
|
||||
|
||||
Pour consulter le journal détaillé de toutes les versions, consultez le fichier **[CHANGELOG.md](./CHANGELOG.md)**.
|
||||
|
||||
<details open>
|
||||
<summary><h3>🚀 Version 4.0.0 (V4) - <em>Dernière version</em></h3></summary>
|
||||
|
||||
- **Intégration GTFS & GTFS-RT Exhaustive :** 3 507 gares et 41 847 circulations réelles intégrées sans données mockées.
|
||||
- **Quais Réels SIRI-Lite :** Extraction en continu des quais réels avec isolation stricte par gare sélectionnée et par arrêt.
|
||||
- **Recherche Intelligente :** Algorithme tolérant aux tirets, accents et alias pour tous les noms de gares.
|
||||
- **Normalisation des Horaires :** Prise en charge des trains de nuit et horaires modulo 24h.
|
||||
- **Codebase Propre :** Résolution complète de tous les avertissements ESLint (`Compiled successfully`).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>🚆 Version 3.0.0 (V3)</h3></summary>
|
||||
|
||||
- Nouveaux types d'afficheurs AFL, Classiques et EVA.
|
||||
- Module d'annonces sonores en temps réel.
|
||||
- Gestion avancée des trains combinés / multi-tranches et des substitutions par autocar.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>🚉 Version 2.0.0 (V2)</h3></summary>
|
||||
|
||||
- Écrans dédiés par voie et quai avec liste déroulante des arrêts aval.
|
||||
- Alternance dynamique des états de circulation et synchronisation automatique.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>🏁 Version 1.0.0 (V1)</h3></summary>
|
||||
|
||||
- Lancement initial de la plateforme avec horloge temps réel et reproduction de la signalétique ferroviaire.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Installation & Démarrage
|
||||
|
||||
```bash
|
||||
# Installer les dépendances
|
||||
npm install
|
||||
|
||||
# Lancer le serveur de développement (http://localhost:3000)
|
||||
npm start
|
||||
|
||||
# Compiler pour la production
|
||||
npm run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 Licence
|
||||
|
||||
Projet développé pour la simulation et la restitution d'écrans d'information ferroviaire. Données fournies sous licence ouverte via transport.data.gouv.fr et SNCF Open Data.
|
||||
+1005
File diff suppressed because it is too large.
Load diff
Generated
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "afficeheurs-ferrovia.",
|
||||
"name": "afficheurs-ferrovia",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "afficeheurs-ferrovia.",
|
||||
"name": "afficheurs-ferrovia",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"dependencies": "^0.0.1",
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "PORT=4010 react-scripts start",
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"serve": "serve -s build -l 4010",
|
||||
"start:prod": "serve -s build -l 4010",
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const files = [
|
||||
{ p: 'src/pages/afficheurs/afl/AFLDeparts.js', arr: false },
|
||||
{ p: 'src/pages/afficheurs/afl/AFLArrivees.js', arr: true },
|
||||
{ p: 'src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js', arr: false },
|
||||
{ p: 'src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js', arr: true },
|
||||
{ p: 'src/pages/afficheurs/afl/quais/AFLDepartsQuais.js', arr: false },
|
||||
{ p: 'src/pages/afficheurs/afl/quais/AFLArriveesQuais.js', arr: true },
|
||||
];
|
||||
|
||||
for (const { p, arr } of files) {
|
||||
const fullPath = path.join(process.cwd(), p);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.log(`File not found: ${fullPath}`);
|
||||
continue;
|
||||
}
|
||||
let content = fs.readFileSync(fullPath, 'utf8');
|
||||
|
||||
if (!content.includes('import useAutoAnnonce from')) {
|
||||
content = content.replace(
|
||||
"import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';",
|
||||
"import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';\nimport useAutoAnnonce from '../../../hooks/useAutoAnnonce';"
|
||||
);
|
||||
}
|
||||
|
||||
if (!content.includes('useAutoAnnonce(')) {
|
||||
const hookCall = ` useAnnonceSonoreRealtime(resolvedGareId);\n useAutoAnnonce(horaires, currentTime, ${arr});`;
|
||||
content = content.replace(" useAnnonceSonoreRealtime(resolvedGareId);", hookCall);
|
||||
}
|
||||
|
||||
fs.writeFileSync(fullPath, content, 'utf8');
|
||||
console.log(`Patched ${p}`);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# Plan d'implémentation : Correction de l'affichage des types de trains (TER Régionaux)
|
||||
|
||||
## Problématique analysée
|
||||
Les données brutes issues des flux GTFS de la SNCF indiquent souvent simplement le type commercial générique `"TER"`, sans préciser la marque régionale (Mobigo, liO, Rémi, Hauts-de-France, etc.).
|
||||
Actuellement, l'application tente de déduire ces marques via un patch "en dur" dans `src/utils/trainLogoHelper.js` pour Mobigo (`startsWith('89')` ou `startsWith('84')`).
|
||||
Ce patch est **erroné** :
|
||||
1. La série `84xxxx` englobe les trains des Hauts-de-France (ex: `841xxx`, `844947` sur Dunkerque/Valenciennes). C'est ce qui cause l'affichage "MOBIGO" en erreur sur l'Image 1.
|
||||
2. L'Image 2 montre des trains d'Occitanie (876406) affichés comme "Rémi" et des confusions entre tranches. L'absence d'identification stricte basée sur un référentiel fiable entraîne des conflits.
|
||||
|
||||
## Contraintes respectées
|
||||
- **Pas de variables locales / géographiques** : Suppression totale ou non-utilisation des listes de villes associées à des régions (ex: `detectRegionByRouteScore` qui associe 'Dunkerque' à HDF).
|
||||
- **Utilisation du numéro de circulation de manière fiable** : Nous nous basons **strictement** sur le référentiel d'EspaceRails fourni pour l'immatriculation des TER.
|
||||
|
||||
## Actions prévues (Implémentation)
|
||||
|
||||
### 1. Refonte du fallback régional dans `trainLogoHelper.js`
|
||||
Nous allons remplacer la condition codée en dur (et fausse) pour "MOBIGO" par un utilitaire de classification exhaustif basé fidèlement sur les données d'EspaceRails. Cette fonction sera appelée si le train est identifié comme un `TER` générique.
|
||||
|
||||
### 2. Plan de numérotation déterministe (Le "Dictionnaire" EspaceRails)
|
||||
La fonction extraira le numéro (ex: `844947` => préfixe `844`) et appliquera le tableau de correspondance suivant, construit à partir de votre source :
|
||||
|
||||
* **Grand Est (Fluo)**
|
||||
* *Alsace* : 830 à 832, 160
|
||||
* *Lorraine* : 833 à 837, 161
|
||||
* *Champagne-Ardenne* : 838 à 840, 162
|
||||
* **Hauts-de-France**
|
||||
* *Nord-Pas-de-Calais* : 841 à 846, 163 à 164
|
||||
* *Picardie* : 847 à 849, 164
|
||||
* **Normandie (Nomad)**
|
||||
* *Haute-Normandie* : 850 à 851, 165
|
||||
* *Basse-Normandie* : 852 à 853, 165
|
||||
* **Bretagne (BreizhGo)** : 854 à 856, 166
|
||||
* **Pays de la Loire (Aléop)** : 857 à 859, 166 à 167
|
||||
* **Centre-Val de Loire (Rémi)** : 860 à 862, 167 à 168
|
||||
* **Nouvelle-Aquitaine**
|
||||
* *Poitou-Charentes* : 863 à 864, 168
|
||||
* *Aquitaine* : 865 à 867, 169
|
||||
* *Limousin* : 868 à 869, 170
|
||||
* **Occitanie (liO)**
|
||||
* *Midi-Pyrénées* : 870 à 872, 171
|
||||
* *Languedoc-Roussillon* : 876 à 878, 173
|
||||
* **Auvergne-Rhône-Alpes**
|
||||
* *Auvergne* : 873 à 875, 172
|
||||
* *Rhône-Alpes* : 882 à 890, 175 à 179
|
||||
* **Provence-Alpes-Côte d'Azur (ZOU)** : 879 à 881, 173 à 174
|
||||
* **Bourgogne-Franche-Comté (Mobigo)**
|
||||
* *Bourgogne* : 891 à 893, 177 à 178
|
||||
* *Franche-Comté* : 894 à 895, 178
|
||||
|
||||
*(Ce référentiel montre bien que "841" est en Hauts-de-France (NPDC), ce qui corrige l'Img 1, et que "876" est bien Languedoc-Roussillon (liO), ce qui corrige l'Img 2).*
|
||||
|
||||
### 3. Application dans `getTrainLogoAndMeta`
|
||||
Si l'opérateur brut du GTFS est un terme générique (comme `"TER"`), la fonction :
|
||||
1. Extraira le numéro à 5 ou 6 chiffres.
|
||||
2. Analysera les 3 premiers chiffres (ex: `844`).
|
||||
3. Consultera le dictionnaire déterministe EspaceRails.
|
||||
4. Remplacera le `typeLabel` et associera le bon logo via `logoLookup`.
|
||||
|
||||
> Cela garantit la fiabilité demandée, supprime l'usage de variables locales "villes", et solutionne définitivement les bugs d'affichage des Images 1 et 2 en s'appuyant sur les préfixes documentés.
|
||||
|
||||
Validez-vous l'intégration de cette logique dans `src/utils/trainLogoHelper.js` ?
|
||||
@@ -0,0 +1,77 @@
|
||||
const CACHE_NAME = 'ferrovia-pwa-v2';
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.filter((name) => name !== CACHE_NAME).map((name) => caches.delete(name))
|
||||
);
|
||||
})
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
if (event.request.method !== 'GET' || !event.request.url.startsWith(self.location.origin)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pour la navigation (HTML / pages), stratégie Network-First systématique pour toujours charger la dernière version
|
||||
const isNavigation = event.request.mode === 'navigate' || (event.request.headers.get('accept') && event.request.headers.get('accept').includes('text/html'));
|
||||
|
||||
if (isNavigation) {
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then((networkResponse) => {
|
||||
if (networkResponse && networkResponse.ok) {
|
||||
const clone = networkResponse.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
||||
}
|
||||
return networkResponse;
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match(event.request).then((cached) => cached || caches.match('/index.html') || caches.match('./index.html'));
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pour les scripts JS et CSS, Network-First pour éviter le gel des bundles
|
||||
const isCodeAsset = event.request.url.includes('/static/js/') || event.request.url.includes('/static/css/');
|
||||
if (isCodeAsset) {
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then((networkResponse) => {
|
||||
if (networkResponse && networkResponse.ok) {
|
||||
const clone = networkResponse.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
||||
}
|
||||
return networkResponse;
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match(event.request);
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pour les autres ressources (images, favicons, polices), Cache-First avec mise en cache réseau
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
return fetch(event.request).then((networkResponse) => {
|
||||
if (networkResponse && networkResponse.ok) {
|
||||
const clone = networkResponse.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
||||
}
|
||||
return networkResponse;
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generatedAt": "2025-10-24T21:56:22.827Z",
|
||||
"generatedAt": "2026-10-24T21:56:22.827Z",
|
||||
"logos": [
|
||||
{
|
||||
"file": "logo-aleop.svg",
|
||||
@@ -11,8 +11,9 @@
|
||||
"file": "logo-auvergerhonealpes.svg",
|
||||
"path": "/logo-auvergerhonealpes.svg",
|
||||
"slug": "auvergerhonealpes",
|
||||
"name": "Auvergerhonealpes"
|
||||
"name": "TER AURA"
|
||||
},
|
||||
|
||||
{
|
||||
"file": "logo-breizhgo.svg",
|
||||
"path": "/logo-breizhgo.svg",,
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
|
||||
const supabaseUrl = process.env.REACT_APP_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY;
|
||||
const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
||||
|
||||
async function run() {
|
||||
const { data: gares, error: err1 } = await supabase.from('gares').select('*').ilike('nom', '%Ouges%').limit(1);
|
||||
console.log("Gares:", gares);
|
||||
if (err1) console.error(err1);
|
||||
|
||||
const { data: horaires, error: err2 } = await supabase.from('horaires').select('*').limit(5);
|
||||
// find one that stops at Ouges
|
||||
const { data: horairesOuges, error: err3 } = await supabase.from('horaires').select('*').ilike('gares_desservies', '%Ouges%').limit(2);
|
||||
console.log("Horaires Ouges:", JSON.stringify(horairesOuges, null, 2));
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,11 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data, error } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies, quai_reel').or('gare_depart_nom.ilike.%Ouges%,gare_arrivee_nom.ilike.%Ouges%').limit(5);
|
||||
console.log("Horaires Ouges:", JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,13 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data, error } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies, quai_reel').limit(10);
|
||||
// find any train with Ouges
|
||||
const filtered = data.filter(h => JSON.stringify(h).includes("Ouges"));
|
||||
console.log("Horaires Ouges filtered:", JSON.stringify(filtered, null, 2));
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,12 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data, error } = await supabase.from('horaires').select('*').limit(1);
|
||||
console.log("Horaires schema:", Object.keys(data[0] || {}));
|
||||
if (error) console.error("Error:", error);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,19 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data, error } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies').or('gare_depart_nom.ilike.%Ouges%,gare_arrivee_nom.ilike.%Ouges%');
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
// try to fetch all and filter in JS
|
||||
const { data: allData } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies').limit(200);
|
||||
const filtered = allData.filter(h => JSON.stringify(h).includes("Ouges"));
|
||||
console.log("Horaires Ouges (JS filter):", JSON.stringify(filtered.slice(0, 2), null, 2));
|
||||
} else {
|
||||
console.log("Horaires Ouges:", JSON.stringify(data.slice(0, 2), null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,9 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
async function run() {
|
||||
const { data, error } = await supabase.from('annonces_diffusions').select('*').order('created_at', { ascending: false }).limit(1);
|
||||
if (error) console.error(error);
|
||||
else console.log(JSON.stringify(data[0], null, 2));
|
||||
}
|
||||
run();
|
||||
@@ -1,9 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
async function run() {
|
||||
const { data, error } = await supabase.from('annonces').select('*').limit(2);
|
||||
if (error) console.error(error);
|
||||
else console.log(JSON.stringify(data, null, 2));
|
||||
}
|
||||
run();
|
||||
@@ -1,9 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
async function run() {
|
||||
const { data, error } = await supabase.from('annonces_diffusions').select('*').limit(1);
|
||||
if (error) console.error(error);
|
||||
else console.log(Object.keys(data[0]));
|
||||
}
|
||||
run();
|
||||
@@ -1,9 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
async function run() {
|
||||
const { data, error } = await supabase.from('horaires').select('*').limit(1);
|
||||
if (error) console.error(error);
|
||||
else console.log(JSON.stringify(data[0], null, 2));
|
||||
}
|
||||
run();
|
||||
@@ -1,12 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
async function run() {
|
||||
const { data, error } = await supabase.from('annonces_diffusions').select(`
|
||||
*,
|
||||
annonce:annonces_enregistrees(variables_values)
|
||||
`).order('created_at', { ascending: false }).limit(1);
|
||||
if (error) console.error(error);
|
||||
else console.log(JSON.stringify(data[0], null, 2));
|
||||
}
|
||||
run();
|
||||
@@ -0,0 +1,204 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const filePath = 'c:\\Users\\mathi\\Documents\\Développement WEB\\ferrovia-infogare\\src\\App.js';
|
||||
|
||||
let content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
content = content.replace(
|
||||
"import React, { useEffect, useState } from 'react';",
|
||||
"import React, { useEffect, useState } from 'react';\nimport { WcsCard, WcsCardBody, WcsButton, WcsBadge } from 'wcs-react';"
|
||||
);
|
||||
|
||||
const home_component_old = ` return (
|
||||
<div className="launcher-container">
|
||||
{/* Carte de bienvenue */}
|
||||
<div className="launcher-card">
|
||||
<div className="launcher-header">
|
||||
<div>
|
||||
<h1 className="launcher-title">🚅 Téléaffichage Ferroviaire SNCF</h1>
|
||||
<p className="launcher-subtitle">
|
||||
{source === 'supabase'
|
||||
? "Consultez les afficheurs basés sur les données de votre BDD Supabase."
|
||||
: "Simulateur d'écrans de gare officiels connectés en direct aux flux GTFS & Open Data SNCF."}
|
||||
</p>
|
||||
</div>
|
||||
<span className={\`launcher-badge \${source === 'supabase' ? 'launcher-badge--supabase' : ''}\`}>
|
||||
{source === 'supabase' ? '🗄️ Mode Supabase' : '🌐 Mode SNCF Live'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Cartes principales : Départs / Arrivées */}
|
||||
<div className="launcher-grid launcher-grid--2col">
|
||||
<div
|
||||
className="launcher-select-card"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
|
||||
navigate(\`\${prefix}/departs\`);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
|
||||
navigate(\`\${prefix}/departs\`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span className="launcher-card-icon">🚆</span>
|
||||
<span className="launcher-card-tag launcher-card-tag--modern">Départs</span>
|
||||
<h2 className="launcher-card-title">Prochains Départs</h2>
|
||||
<p className="launcher-card-desc">
|
||||
Consultez les trains au départ, leurs voies, retards et dessertes complètes.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<span className="launcher-btn launcher-btn--primary">
|
||||
Consulter les Départs ›
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="launcher-select-card"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
|
||||
navigate(\`\${prefix}/arrivees\`);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
|
||||
navigate(\`\${prefix}/arrivees\`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span className="launcher-card-icon">🏁</span>
|
||||
<span className="launcher-card-tag launcher-card-tag--modern">Arrivées</span>
|
||||
<h2 className="launcher-card-title">Prochaines Arrivées</h2>
|
||||
<p className="launcher-card-desc">
|
||||
Suivez en direct les trains attendus en gare, provenances et voies assignées.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<span className="launcher-btn launcher-btn--secondary">
|
||||
Consulter les Arrivées ›
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Raccourcis rapides vers les gares */}
|
||||
<div className="launcher-chips-container" style={{ marginTop: '8px' }}>
|
||||
<span className="launcher-chips-label">Accès direct par gare :</span>
|
||||
<div className="launcher-chips-list">
|
||||
{POPULAR_HOME_GARES.map((gName) => (
|
||||
<button
|
||||
key={gName}
|
||||
type="button"
|
||||
className="launcher-chip"
|
||||
onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
|
||||
navigate(\`\${prefix}/afficheurs/\${encodeURIComponent(gName)}?mode=departs\`);
|
||||
}}
|
||||
>
|
||||
📍 {gName.replace(/-/g, ' ')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Présentation des types d'afficheurs */}
|
||||
<div style={{ borderTop: '1px solid #e2ebf6', paddingTop: '20px', marginTop: '20px' }}>
|
||||
<div style={{ fontSize: '13px', color: '#5f6368', lineHeight: 1.6 }}>
|
||||
✨ <strong>Formats disponibles :</strong> Afficheurs Fluides & Lumineux (AFL), Téléaffichage historique (Classiques), Écrans Vidéo Voyageurs (EVA) et Écrans par Quai / Voie.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);`;
|
||||
|
||||
const home_component_new = ` return (
|
||||
<div className="launcher-container" style={{ padding: '24px', maxWidth: '900px', margin: '0 auto' }}>
|
||||
<WcsCard mode="raised">
|
||||
<WcsCardBody>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '24px' }}>
|
||||
<div>
|
||||
<h1 style={{ margin: '0 0 8px 0', fontSize: '24px', color: 'var(--wcs-primary)' }}>🚅 Téléaffichage Ferroviaire SNCF</h1>
|
||||
<p style={{ margin: 0, color: 'var(--wcs-text-light)' }}>
|
||||
{source === 'supabase'
|
||||
? "Consultez les afficheurs basés sur les données de votre BDD Supabase."
|
||||
: "Simulateur d'écrans de gare officiels connectés en direct aux flux GTFS & Open Data SNCF."}
|
||||
</p>
|
||||
</div>
|
||||
<WcsBadge className={source === 'supabase' ? 'wcs-secondary' : 'wcs-primary'} shape="rounded">
|
||||
{source === 'supabase' ? '🗄️ Mode Supabase' : '🌐 Mode SNCF Live'}
|
||||
</WcsBadge>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '24px', flexDirection: 'row', flexWrap: 'wrap', marginBottom: '24px' }}>
|
||||
<WcsCard style={{ flex: '1 1 300px', cursor: 'pointer' }} mode="flat" onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
|
||||
navigate(\`\${prefix}/departs\`);
|
||||
}}>
|
||||
<WcsCardBody>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '32px' }}>🚆</span>
|
||||
<WcsBadge className="wcs-success">Départs</WcsBadge>
|
||||
</div>
|
||||
<h2 style={{ fontSize: '18px', marginTop: '16px' }}>Prochains Départs</h2>
|
||||
<p style={{ color: 'var(--wcs-text-light)', minHeight: '60px' }}>Consultez les trains au départ, leurs voies, retards et dessertes complètes.</p>
|
||||
<WcsButton className="wcs-primary" style={{ width: '100%' }}>Consulter les Départs</WcsButton>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
|
||||
<WcsCard style={{ flex: '1 1 300px', cursor: 'pointer' }} mode="flat" onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
|
||||
navigate(\`\${prefix}/arrivees\`);
|
||||
}}>
|
||||
<WcsCardBody>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '32px' }}>🏁</span>
|
||||
<WcsBadge className="wcs-info">Arrivées</WcsBadge>
|
||||
</div>
|
||||
<h2 style={{ fontSize: '18px', marginTop: '16px' }}>Prochaines Arrivées</h2>
|
||||
<p style={{ color: 'var(--wcs-text-light)', minHeight: '60px' }}>Suivez en direct les trains attendus en gare, provenances et voies assignées.</p>
|
||||
<WcsButton className="wcs-secondary" style={{ width: '100%' }}>Consulter les Arrivées</WcsButton>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<p style={{ fontWeight: 'bold', marginBottom: '12px' }}>Accès direct par gare :</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||
{POPULAR_HOME_GARES.map((gName) => (
|
||||
<WcsButton
|
||||
key={gName}
|
||||
mode="stroked"
|
||||
shape="round"
|
||||
size="s"
|
||||
onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
|
||||
navigate(\`\${prefix}/afficheurs/\${encodeURIComponent(gName)}?mode=departs\`);
|
||||
}}
|
||||
>
|
||||
📍 {gName.replace(/-/g, ' ')}
|
||||
</WcsButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr style={{ border: 'none', borderTop: '1px solid var(--wcs-light)', margin: '24px 0' }} />
|
||||
<div style={{ fontSize: '14px', color: 'var(--wcs-text-light)', lineHeight: 1.6 }}>
|
||||
✨ <strong>Formats disponibles :</strong> Afficheurs Fluides & Lumineux (AFL), Téléaffichage historique (Classiques), Écrans Vidéo Voyageurs (EVA) et Écrans par Quai / Voie.
|
||||
</div>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
</div>
|
||||
);`;
|
||||
|
||||
content = content.replace(home_component_old, home_component_new);
|
||||
fs.writeFileSync(filePath, content, 'utf-8');
|
||||
console.log('App.js patched successfully');
|
||||
@@ -0,0 +1,207 @@
|
||||
import os
|
||||
|
||||
file_path = r"c:\Users\mathi\Documents\Développement WEB\ferrovia-infogare\src\App.js"
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Make the changes in App.js for the Home component
|
||||
new_content = content.replace(
|
||||
"import React, { useEffect, useState } from 'react';",
|
||||
"import React, { useEffect, useState } from 'react';\nimport { WcsCard, WcsCardBody, WcsButton, WcsBadge } from 'wcs-react';"
|
||||
)
|
||||
|
||||
home_component_old = """ return (
|
||||
<div className="launcher-container">
|
||||
{/* Carte de bienvenue */}
|
||||
<div className="launcher-card">
|
||||
<div className="launcher-header">
|
||||
<div>
|
||||
<h1 className="launcher-title">🚅 Téléaffichage Ferroviaire SNCF</h1>
|
||||
<p className="launcher-subtitle">
|
||||
{source === 'supabase'
|
||||
? "Consultez les afficheurs basés sur les données de votre BDD Supabase."
|
||||
: "Simulateur d'écrans de gare officiels connectés en direct aux flux GTFS & Open Data SNCF."}
|
||||
</p>
|
||||
</div>
|
||||
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
|
||||
{source === 'supabase' ? '🗄️ Mode Supabase' : '🌐 Mode SNCF Live'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Cartes principales : Départs / Arrivées */}
|
||||
<div className="launcher-grid launcher-grid--2col">
|
||||
<div
|
||||
className="launcher-select-card"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
|
||||
navigate(`${prefix}/departs`);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
|
||||
navigate(`${prefix}/departs`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span className="launcher-card-icon">🚆</span>
|
||||
<span className="launcher-card-tag launcher-card-tag--modern">Départs</span>
|
||||
<h2 className="launcher-card-title">Prochains Départs</h2>
|
||||
<p className="launcher-card-desc">
|
||||
Consultez les trains au départ, leurs voies, retards et dessertes complètes.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<span className="launcher-btn launcher-btn--primary">
|
||||
Consulter les Départs ›
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="launcher-select-card"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
|
||||
navigate(`${prefix}/arrivees`);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
|
||||
navigate(`${prefix}/arrivees`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span className="launcher-card-icon">🏁</span>
|
||||
<span className="launcher-card-tag launcher-card-tag--modern">Arrivées</span>
|
||||
<h2 className="launcher-card-title">Prochaines Arrivées</h2>
|
||||
<p className="launcher-card-desc">
|
||||
Suivez en direct les trains attendus en gare, provenances et voies assignées.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<span className="launcher-btn launcher-btn--secondary">
|
||||
Consulter les Arrivées ›
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Raccourcis rapides vers les gares */}
|
||||
<div className="launcher-chips-container" style={{ marginTop: '8px' }}>
|
||||
<span className="launcher-chips-label">Accès direct par gare :</span>
|
||||
<div className="launcher-chips-list">
|
||||
{POPULAR_HOME_GARES.map((gName) => (
|
||||
<button
|
||||
key={gName}
|
||||
type="button"
|
||||
className="launcher-chip"
|
||||
onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
|
||||
navigate(`${prefix}/afficheurs/${encodeURIComponent(gName)}?mode=departs`);
|
||||
}}
|
||||
>
|
||||
📍 {gName.replace(/-/g, ' ')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Présentation des types d'afficheurs */}
|
||||
<div style={{ borderTop: '1px solid #e2ebf6', paddingTop: '20px', marginTop: '20px' }}>
|
||||
<div style={{ fontSize: '13px', color: '#5f6368', lineHeight: 1.6 }}>
|
||||
✨ <strong>Formats disponibles :</strong> Afficheurs Fluides & Lumineux (AFL), Téléaffichage historique (Classiques), Écrans Vidéo Voyageurs (EVA) et Écrans par Quai / Voie.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);"""
|
||||
|
||||
home_component_new = """ return (
|
||||
<div className="launcher-container" style={{ padding: '24px', maxWidth: '900px', margin: '0 auto' }}>
|
||||
<WcsCard mode="raised">
|
||||
<WcsCardBody>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '24px' }}>
|
||||
<div>
|
||||
<h1 style={{ margin: '0 0 8px 0', fontSize: '24px', color: 'var(--wcs-primary)' }}>🚅 Téléaffichage Ferroviaire SNCF</h1>
|
||||
<p style={{ margin: 0, color: 'var(--wcs-text-light)' }}>
|
||||
{source === 'supabase'
|
||||
? "Consultez les afficheurs basés sur les données de votre BDD Supabase."
|
||||
: "Simulateur d'écrans de gare officiels connectés en direct aux flux GTFS & Open Data SNCF."}
|
||||
</p>
|
||||
</div>
|
||||
<WcsBadge className={source === 'supabase' ? 'wcs-secondary' : 'wcs-primary'} shape="rounded">
|
||||
{source === 'supabase' ? '🗄️ Mode Supabase' : '🌐 Mode SNCF Live'}
|
||||
</WcsBadge>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '24px', flexDirection: 'row', flexWrap: 'wrap', marginBottom: '24px' }}>
|
||||
<WcsCard style={{ flex: '1 1 300px', cursor: 'pointer' }} mode="flat" onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
|
||||
navigate(`${prefix}/departs`);
|
||||
}}>
|
||||
<WcsCardBody>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '32px' }}>🚆</span>
|
||||
<WcsBadge className="wcs-success">Départs</WcsBadge>
|
||||
</div>
|
||||
<h2 style={{ fontSize: '18px', marginTop: '16px' }}>Prochains Départs</h2>
|
||||
<p style={{ color: 'var(--wcs-text-light)', minHeight: '60px' }}>Consultez les trains au départ, leurs voies, retards et dessertes complètes.</p>
|
||||
<WcsButton className="wcs-primary" style={{ width: '100%' }}>Consulter les Départs</WcsButton>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
|
||||
<WcsCard style={{ flex: '1 1 300px', cursor: 'pointer' }} mode="flat" onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
|
||||
navigate(`${prefix}/arrivees`);
|
||||
}}>
|
||||
<WcsCardBody>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '32px' }}>🏁</span>
|
||||
<WcsBadge className="wcs-info">Arrivées</WcsBadge>
|
||||
</div>
|
||||
<h2 style={{ fontSize: '18px', marginTop: '16px' }}>Prochaines Arrivées</h2>
|
||||
<p style={{ color: 'var(--wcs-text-light)', minHeight: '60px' }}>Suivez en direct les trains attendus en gare, provenances et voies assignées.</p>
|
||||
<WcsButton className="wcs-secondary" style={{ width: '100%' }}>Consulter les Arrivées</WcsButton>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<p style={{ fontWeight: 'bold', marginBottom: '12px' }}>Accès direct par gare :</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||
{POPULAR_HOME_GARES.map((gName) => (
|
||||
<WcsButton
|
||||
key={gName}
|
||||
mode="stroked"
|
||||
shape="round"
|
||||
size="s"
|
||||
onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
|
||||
navigate(`${prefix}/afficheurs/${encodeURIComponent(gName)}?mode=departs`);
|
||||
}}
|
||||
>
|
||||
📍 {gName.replace(/-/g, ' ')}
|
||||
</WcsButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr style={{ border: 'none', borderTop: '1px solid var(--wcs-light)', margin: '24px 0' }} />
|
||||
<div style={{ fontSize: '14px', color: 'var(--wcs-text-light)', lineHeight: 1.6 }}>
|
||||
✨ <strong>Formats disponibles :</strong> Afficheurs Fluides & Lumineux (AFL), Téléaffichage historique (Classiques), Écrans Vidéo Voyageurs (EVA) et Écrans par Quai / Voie.
|
||||
</div>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
</div>
|
||||
);"""
|
||||
|
||||
new_content = new_content.replace(home_component_old, home_component_new)
|
||||
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(new_content)
|
||||
+64
-99
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { WcsCard, WcsCardBody, WcsButton, WcsBadge } from 'wcs-react';
|
||||
import './App.css';
|
||||
|
||||
import { applyPolyfills, defineCustomElements } from 'wcs-core/loader';
|
||||
@@ -24,11 +25,9 @@ import { getSourceFromPath } from './services/railwayService';
|
||||
|
||||
applyPolyfills().then(() => defineCustomElements(window));
|
||||
|
||||
// Petit routeur client sans dépendances externes
|
||||
function useSimpleRouter() {
|
||||
function normalizePath(p) {
|
||||
if (!p) return '/';
|
||||
// retirer query string et hash
|
||||
p = String(p).split(/[?#]/)[0];
|
||||
if (p.indexOf('index.html') !== -1) return '/';
|
||||
p = p.trim();
|
||||
@@ -74,7 +73,6 @@ function App() {
|
||||
const prefix = source === 'supabase' ? '/ferrovia' : '';
|
||||
const cleanRoute = (route.replace(/^\/ferrovia/, '') || '/');
|
||||
|
||||
// Composant Home moderne & responsive pour Mobile, Tablette et Desktop
|
||||
function Home({ navigate, prefix, source }) {
|
||||
const POPULAR_HOME_GARES = [
|
||||
'Paris-Gare-de-Lyon',
|
||||
@@ -88,118 +86,85 @@ function App() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="launcher-container">
|
||||
{/* Carte de bienvenue */}
|
||||
<div className="launcher-card">
|
||||
<div className="launcher-header">
|
||||
<div>
|
||||
<h1 className="launcher-title">🚅 Téléaffichage Ferroviaire SNCF</h1>
|
||||
<p className="launcher-subtitle">
|
||||
{source === 'supabase'
|
||||
? "Consultez les afficheurs basés sur les données de votre BDD Supabase."
|
||||
: "Simulateur d'écrans de gare officiels connectés en direct aux flux GTFS & Open Data SNCF."}
|
||||
</p>
|
||||
<div className="launcher-container" style={{ padding: '24px', maxWidth: '900px', margin: '0 auto' }}>
|
||||
<WcsCard mode="raised">
|
||||
<WcsCardBody>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '24px' }}>
|
||||
<div>
|
||||
<h1 style={{ margin: '0 0 8px 0', fontSize: '24px', color: 'var(--wcs-primary)' }}>🚅 Téléaffichage Ferroviaire SNCF</h1>
|
||||
<p style={{ margin: 0, color: 'var(--wcs-text-light)' }}>
|
||||
{source === 'supabase'
|
||||
? "Consultez les afficheurs basés sur les données de votre BDD Supabase."
|
||||
: "Simulateur d'écrans de gare officiels connectés en direct aux flux GTFS & Open Data SNCF."}
|
||||
</p>
|
||||
</div>
|
||||
<WcsBadge className={source === 'supabase' ? 'wcs-secondary' : 'wcs-primary'} shape="rounded">
|
||||
{source === 'supabase' ? '🗄️ Mode Supabase' : '🌐 Mode SNCF Live'}
|
||||
</WcsBadge>
|
||||
</div>
|
||||
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
|
||||
{source === 'supabase' ? '🗄️ Mode Supabase' : '🌐 Mode SNCF Live'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Cartes principales : Départs / Arrivées */}
|
||||
<div className="launcher-grid launcher-grid--2col">
|
||||
<div
|
||||
className="launcher-select-card"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
|
||||
navigate(`${prefix}/departs`);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
<div style={{ display: 'flex', gap: '24px', flexDirection: 'row', flexWrap: 'wrap', marginBottom: '24px' }}>
|
||||
<WcsCard style={{ flex: '1 1 300px', cursor: 'pointer', border: '1px solid var(--wcs-light)' }} mode="flat" onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
|
||||
navigate(`${prefix}/departs`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span className="launcher-card-icon">🚆</span>
|
||||
<span className="launcher-card-tag launcher-card-tag--modern">Départs</span>
|
||||
<h2 className="launcher-card-title">Prochains Départs</h2>
|
||||
<p className="launcher-card-desc">
|
||||
Consultez les trains au départ, leurs voies, retards et dessertes complètes.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<span className="launcher-btn launcher-btn--primary">
|
||||
Consulter les Départs ›
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}}>
|
||||
<WcsCardBody>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '32px' }}>🚆</span>
|
||||
<WcsBadge className="wcs-success">Départs</WcsBadge>
|
||||
</div>
|
||||
<h2 style={{ fontSize: '18px', marginTop: '16px' }}>Prochains Départs</h2>
|
||||
<p style={{ color: 'var(--wcs-text-light)', minHeight: '60px' }}>Consultez les trains au départ, leurs voies, retards et dessertes complètes.</p>
|
||||
<WcsButton className="wcs-primary" style={{ width: '100%' }}>Consulter les Départs</WcsButton>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
|
||||
<div
|
||||
className="launcher-select-card"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
|
||||
navigate(`${prefix}/arrivees`);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
<WcsCard style={{ flex: '1 1 300px', cursor: 'pointer', border: '1px solid var(--wcs-light)' }} mode="flat" onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
|
||||
navigate(`${prefix}/arrivees`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span className="launcher-card-icon">🏁</span>
|
||||
<span className="launcher-card-tag launcher-card-tag--modern">Arrivées</span>
|
||||
<h2 className="launcher-card-title">Prochaines Arrivées</h2>
|
||||
<p className="launcher-card-desc">
|
||||
Suivez en direct les trains attendus en gare, provenances et voies assignées.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<span className="launcher-btn launcher-btn--secondary">
|
||||
Consulter les Arrivées ›
|
||||
</span>
|
||||
}}>
|
||||
<WcsCardBody>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '32px' }}>🏁</span>
|
||||
<WcsBadge className="wcs-info">Arrivées</WcsBadge>
|
||||
</div>
|
||||
<h2 style={{ fontSize: '18px', marginTop: '16px' }}>Prochaines Arrivées</h2>
|
||||
<p style={{ color: 'var(--wcs-text-light)', minHeight: '60px' }}>Suivez en direct les trains attendus en gare, provenances et voies assignées.</p>
|
||||
<WcsButton className="wcs-secondary" style={{ width: '100%' }}>Consulter les Arrivées</WcsButton>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<p style={{ fontWeight: 'bold', marginBottom: '12px' }}>Accès direct par gare :</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||
{POPULAR_HOME_GARES.map((gName) => (
|
||||
<WcsButton
|
||||
key={gName}
|
||||
mode="stroked"
|
||||
shape="round"
|
||||
size="s"
|
||||
onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
|
||||
navigate(`${prefix}/afficheurs/${encodeURIComponent(gName)}?mode=departs`);
|
||||
}}
|
||||
>
|
||||
📍 {gName.replace(/-/g, ' ')}
|
||||
</WcsButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Raccourcis rapides vers les gares */}
|
||||
<div className="launcher-chips-container" style={{ marginTop: '8px' }}>
|
||||
<span className="launcher-chips-label">Accès direct par gare :</span>
|
||||
<div className="launcher-chips-list">
|
||||
{POPULAR_HOME_GARES.map((gName) => (
|
||||
<button
|
||||
key={gName}
|
||||
type="button"
|
||||
className="launcher-chip"
|
||||
onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
|
||||
navigate(`${prefix}/afficheurs/${encodeURIComponent(gName)}?mode=departs`);
|
||||
}}
|
||||
>
|
||||
📍 {gName.replace(/-/g, ' ')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Présentation des types d'afficheurs */}
|
||||
<div style={{ borderTop: '1px solid #e2ebf6', paddingTop: '20px', marginTop: '20px' }}>
|
||||
<div style={{ fontSize: '13px', color: '#5f6368', lineHeight: 1.6 }}>
|
||||
<hr style={{ border: 'none', borderTop: '1px solid var(--wcs-light)', margin: '24px 0' }} />
|
||||
<div style={{ fontSize: '14px', color: 'var(--wcs-text-light)', lineHeight: 1.6 }}>
|
||||
✨ <strong>Formats disponibles :</strong> Afficheurs Fluides & Lumineux (AFL), Téléaffichage historique (Classiques), Écrans Vidéo Voyageurs (EVA) et Écrans par Quai / Voie.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Routes pour afficheurs : supporte le mode racine ET le mode /ferrovia
|
||||
if (
|
||||
cleanRoute.startsWith('/afficheurs/classiques/flash-info') ||
|
||||
cleanRoute.startsWith('/afficheurs/classiques/flash-circulation') ||
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import './AfficheurViewportWrapper.css';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
/**
|
||||
* AfficheurViewportWrapper
|
||||
* Assure un affichage 16:9 strict (1920x1080) sur tous les appareils
|
||||
* (Mobile portrait, Mobile paysage, Tablettes, Ordinateurs, TV).
|
||||
* Ajoute automatiquement des bordures noires (letterboxing / pillarboxing)
|
||||
* pour conserver fidèlement les proportions du design original sans distorsion.
|
||||
*/
|
||||
export default function AfficheurViewportWrapper({ children }) {
|
||||
const [scale, setScale] = useState(1);
|
||||
const [pipWindow, setPipWindow] = useState(null);
|
||||
const containerRef = useRef(null);
|
||||
const [isPipSupported, setIsPipSupported] = useState(false);
|
||||
const [autoPipTriggered, setAutoPipTriggered] = useState(false);
|
||||
|
||||
const isMobile = typeof window !== 'undefined' && (window.innerWidth <= 768 || /Mobi|Android/i.test(navigator.userAgent));
|
||||
|
||||
useEffect(() => {
|
||||
if ('documentPictureInPicture' in window) {
|
||||
setIsPipSupported(true);
|
||||
}
|
||||
|
||||
const calculateScale = () => {
|
||||
// Dimensions de la fenêtre d'affichage (support viewport iOS / Android safe area)
|
||||
const w = window.innerWidth || (document.documentElement && document.documentElement.clientWidth) || 1920;
|
||||
const h = window.innerHeight || (document.documentElement && document.documentElement.clientHeight) || 1080;
|
||||
|
||||
// Échelle basée sur la résolution standard 1920x1080 (16:9)
|
||||
const scaleX = w / 1920;
|
||||
const scaleY = h / 1080;
|
||||
const s = Math.min(scaleX, scaleY);
|
||||
@@ -27,7 +28,6 @@ export default function AfficheurViewportWrapper({ children }) {
|
||||
calculateScale();
|
||||
window.addEventListener('resize', calculateScale);
|
||||
window.addEventListener('orientationchange', calculateScale);
|
||||
|
||||
if (window.visualViewport) {
|
||||
window.visualViewport.addEventListener('resize', calculateScale);
|
||||
}
|
||||
@@ -41,16 +41,136 @@ export default function AfficheurViewportWrapper({ children }) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const togglePiP = async () => {
|
||||
if (pipWindow) {
|
||||
pipWindow.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if ('documentPictureInPicture' in window) {
|
||||
try {
|
||||
const pipW = isMobile ? window.innerWidth * 0.8 : 1920 / 3;
|
||||
const pipH = isMobile ? (window.innerWidth * 0.8) * (1080 / 1920) : 1080 / 3;
|
||||
|
||||
const pip = await window.documentPictureInPicture.requestWindow({
|
||||
width: Math.max(pipW, 300),
|
||||
height: Math.max(pipH, 170),
|
||||
});
|
||||
|
||||
[...document.styleSheets].forEach((styleSheet) => {
|
||||
try {
|
||||
const cssRules = [...styleSheet.cssRules].map(rule => rule.cssText).join('');
|
||||
const style = document.createElement('style');
|
||||
style.textContent = cssRules;
|
||||
pip.document.head.appendChild(style);
|
||||
} catch(e) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.type = styleSheet.type;
|
||||
link.media = styleSheet.media;
|
||||
link.href = styleSheet.href;
|
||||
pip.document.head.appendChild(link);
|
||||
}
|
||||
});
|
||||
|
||||
pip.document.body.classList.add('pip-mode');
|
||||
pip.document.body.style.margin = '0';
|
||||
pip.document.body.style.padding = '0';
|
||||
pip.document.body.style.overflow = 'hidden';
|
||||
pip.document.body.style.width = '100vw';
|
||||
pip.document.body.style.height = '100vh';
|
||||
pip.document.body.style.backgroundColor = '#000';
|
||||
pip.document.body.style.display = 'flex';
|
||||
pip.document.body.style.justifyContent = 'center';
|
||||
pip.document.body.style.alignItems = 'center';
|
||||
|
||||
const updatePipScale = () => {
|
||||
const w = pip.innerWidth;
|
||||
const h = pip.innerHeight;
|
||||
const pipScale = Math.min(w / 1920, h / 1080);
|
||||
pip.document.body.style.setProperty('--pip-scale', pipScale);
|
||||
};
|
||||
updatePipScale();
|
||||
pip.addEventListener('resize', updatePipScale);
|
||||
|
||||
pip.addEventListener("pagehide", () => {
|
||||
setPipWindow(null);
|
||||
});
|
||||
|
||||
setPipWindow(pip);
|
||||
setAutoPipTriggered(true);
|
||||
} catch (err) {
|
||||
console.error('Failed to enter PiP mode', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className="afficheur-pwa-canvas"
|
||||
style={{
|
||||
transform: pipWindow ? 'scale(1)' : `scale(${scale})`,
|
||||
width: '1920px',
|
||||
height: '1080px',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="afficheur-pwa-screen-wrapper">
|
||||
<div
|
||||
className="afficheur-pwa-canvas"
|
||||
style={{
|
||||
transform: `scale(${scale})`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div className="afficheur-pwa-screen-wrapper" ref={containerRef} style={{ width: '100vw', height: '100vh', overflow: 'hidden' }}>
|
||||
{!pipWindow && content}
|
||||
|
||||
{isMobile && isPipSupported && !pipWindow && !autoPipTriggered && (
|
||||
<div
|
||||
onClick={togglePiP}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0, left: 0, right: 0, bottom: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.85)',
|
||||
zIndex: 10000,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
padding: '20px',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '48px', marginBottom: '16px' }}>📱</span>
|
||||
<h2 style={{ margin: '0 0 16px 0' }}>Appuyez pour lancer l'afficheur</h2>
|
||||
<p style={{ opacity: 0.8 }}>Le mode Picture-in-Picture s'activera pour une meilleure expérience sur mobile.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pipWindow && (
|
||||
<div style={{ color: 'white', textAlign: 'center', padding: '24px', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
||||
<h2>L'afficheur est en cours de lecture dans le lecteur réduit (PiP)</h2>
|
||||
<p style={{ opacity: 0.7 }}>Vous pouvez naviguer sur votre téléphone, l'afficheur restera au premier plan.</p>
|
||||
<button onClick={() => pipWindow.close()} style={{ padding: '12px 24px', marginTop: '24px', background: '#00439e', color: 'white', border: 'none', borderRadius: '4px', fontSize: '16px', fontWeight: 'bold' }}>
|
||||
Fermer le mode PiP
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pipWindow && createPortal(
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
transform: `scale(var(--pip-scale, 1))`
|
||||
}}>
|
||||
{content}
|
||||
</div>,
|
||||
pipWindow.document.body
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -15,3 +15,15 @@ root.render(
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
|
||||
// Register PWA Service Worker
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js').then(registration => {
|
||||
console.log('SW registered: ', registration);
|
||||
registration.update();
|
||||
}).catch(registrationError => {
|
||||
console.log('SW registration failed: ', registrationError);
|
||||
});
|
||||
});
|
||||
}
|
||||
+98
-125
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import WcsButton from '../components/WcsButton';
|
||||
import { WcsCard, WcsCardBody, WcsButton, WcsInput, WcsBadge, WcsHorizontalStepper } from 'wcs-react';
|
||||
import sessionMemory from '../lib/sessionMemory';
|
||||
import { getSourceFromPath, searchGares } from '../services/railwayService';
|
||||
|
||||
@@ -90,139 +90,112 @@ export default function Arrivees({ onBack, navigate }) {
|
||||
}
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{ text: 'Choix de la gare', button: { kind: 'Text', text: '1' }, complete: false },
|
||||
{ text: "Type d'afficheur", button: { kind: 'Text', text: '2' }, disable: true },
|
||||
{ text: 'Options & Affichage', button: { kind: 'Text', text: '3' }, disable: true }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="launcher-container">
|
||||
{/* Stepper */}
|
||||
<div className="launcher-stepper">
|
||||
<div className="launcher-step launcher-step--active">
|
||||
<span className="launcher-step-num">1</span>
|
||||
<span>Choix de la gare</span>
|
||||
</div>
|
||||
<span className="launcher-step-separator">›</span>
|
||||
<div className="launcher-step">
|
||||
<span className="launcher-step-num">2</span>
|
||||
<span>Type d'afficheur</span>
|
||||
</div>
|
||||
<span className="launcher-step-separator">›</span>
|
||||
<div className="launcher-step">
|
||||
<span className="launcher-step-num">3</span>
|
||||
<span>Options & Affichage</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '24px', overflowY: 'auto', maxHeight: 'calc(100vh - 120px)' }}>
|
||||
<WcsHorizontalStepper steps={steps} currentStep={0} style={{ marginBottom: '24px' }} />
|
||||
|
||||
<div className="launcher-card">
|
||||
{/* Header */}
|
||||
<div className="launcher-header">
|
||||
<div>
|
||||
<h1 className="launcher-title">🏁 Prochaines Arrivées</h1>
|
||||
<p className="launcher-subtitle">Recherchez une gare pour afficher le tableau des arrivées en temps réel.</p>
|
||||
<WcsCard mode="raised">
|
||||
<WcsCardBody>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
|
||||
<div>
|
||||
<h1 style={{ margin: '0 0 8px 0', fontSize: '24px', color: 'var(--wcs-primary)' }}>🏁 Prochaines Arrivées</h1>
|
||||
<p style={{ margin: 0, color: 'var(--wcs-text-light)' }}>Recherchez une gare pour afficher le tableau des arrivées en temps réel.</p>
|
||||
</div>
|
||||
<WcsBadge className={source === 'supabase' ? 'wcs-secondary' : 'wcs-primary'} shape="rounded">
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS Live'}
|
||||
</WcsBadge>
|
||||
</div>
|
||||
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS Live'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Barre de recherche */}
|
||||
<div className="launcher-search-box">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Rechercher une gare (ex: Dijon, Paris, Lyon, Besançon...)"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="launcher-search-input"
|
||||
aria-label="Rechercher une gare"
|
||||
autoFocus
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
type="button"
|
||||
className="launcher-search-clear"
|
||||
onClick={() => setSearch('')}
|
||||
aria-label="Effacer la recherche"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Suggestions rapides / Gares populaires */}
|
||||
<div className="launcher-chips-container">
|
||||
<span className="launcher-chips-label">Gares fréquentes :</span>
|
||||
<div className="launcher-chips-list">
|
||||
{POPULAR_GARES.map((gName) => (
|
||||
<button
|
||||
key={gName}
|
||||
type="button"
|
||||
className="launcher-chip"
|
||||
onClick={() => handleSelectGare(gName)}
|
||||
>
|
||||
{gName.replace(/-/g, ' ')}
|
||||
</button>
|
||||
))}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<WcsInput
|
||||
type="text"
|
||||
icon="search"
|
||||
placeholder="Rechercher une gare (ex: Dijon, Paris, Lyon, Besançon...)"
|
||||
value={search}
|
||||
onWcsInput={(e) => setSearch(e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
clearable={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && <p style={{ color: '#00439e', fontWeight: 600 }}>⏳ Chargement des gares...</p>}
|
||||
{error && <p style={{ color: '#d70022', fontWeight: 600 }}>⚠️ Erreur : {error}</p>}
|
||||
|
||||
{/* Invitation à la recherche en mode SNCF */}
|
||||
{source === 'sncf' && search.trim().length < 2 && (
|
||||
<div style={{ textAlign: 'center', padding: '24px 16px', color: '#666', background: '#f8fafc', borderRadius: '8px', border: '1px dashed #d0d7de' }}>
|
||||
<p style={{ margin: 0, fontSize: '14px' }}>
|
||||
💡 Saisissez au moins <strong>2 lettres</strong> pour chercher parmi plus de 3 500 gares du réseau français.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Aucun résultat */}
|
||||
{source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '24px', color: '#777', background: '#fff', borderRadius: '8px' }}>
|
||||
Aucune gare trouvée pour « <strong>{search}</strong> ».
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Liste des résultats */}
|
||||
{displayedGares.length > 0 && (
|
||||
<div className="launcher-results-list" role="list">
|
||||
{displayedGares.map((g) => {
|
||||
const nom = g.nom;
|
||||
return (
|
||||
<div
|
||||
key={g.uic || nom}
|
||||
className="launcher-result-item"
|
||||
onClick={() => handleSelectGare(nom)}
|
||||
onKeyDown={(e) => (e.key === 'Enter' ? handleSelectGare(nom) : null)}
|
||||
tabIndex={0}
|
||||
role="listitem"
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<p style={{ fontWeight: 'bold', marginBottom: '12px' }}>Gares fréquentes :</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||
{POPULAR_GARES.map((gName) => (
|
||||
<WcsButton
|
||||
key={gName}
|
||||
mode="stroked"
|
||||
shape="round"
|
||||
size="s"
|
||||
onClick={() => handleSelectGare(gName)}
|
||||
>
|
||||
<div>
|
||||
<div className="launcher-result-name">{nom}</div>
|
||||
{g.commune && (
|
||||
<div className="launcher-result-sub">
|
||||
{g.commune} {g.departement ? `(${g.departement})` : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{g.uic && <span className="launcher-result-tag">UIC {g.uic}</span>}
|
||||
<span style={{ color: '#00439e', fontSize: '18px', fontWeight: 'bold' }}>›</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{gName.replace(/-/g, ' ')}
|
||||
</WcsButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation retour */}
|
||||
<div className="launcher-footer-nav">
|
||||
<WcsButton
|
||||
mode="plain"
|
||||
onClick={onBack}
|
||||
label="← Retour à l'accueil"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{loading && <p style={{ color: 'var(--wcs-primary)', fontWeight: 600 }}>⏳ Chargement des gares...</p>}
|
||||
{error && <p style={{ color: 'var(--wcs-danger)', fontWeight: 600 }}>⚠️ Erreur : {error}</p>}
|
||||
|
||||
{source === 'sncf' && search.trim().length < 2 && (
|
||||
<div style={{ textAlign: 'center', padding: '24px 16px', color: 'var(--wcs-text-light)', background: 'var(--wcs-light)', borderRadius: '8px', border: '1px dashed var(--wcs-gray)' }}>
|
||||
<p style={{ margin: 0, fontSize: '14px' }}>
|
||||
💡 Saisissez au moins <strong>2 lettres</strong> pour chercher parmi plus de 3 500 gares du réseau français.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '24px', color: 'var(--wcs-text-light)', background: 'var(--wcs-white)', borderRadius: '8px' }}>
|
||||
Aucune gare trouvée pour « <strong>{search}</strong> ».
|
||||
</div>
|
||||
)}
|
||||
|
||||
{displayedGares.length > 0 && (
|
||||
<div role="list" style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '24px' }}>
|
||||
{displayedGares.map((g) => {
|
||||
const nom = g.nom;
|
||||
return (
|
||||
<WcsCard
|
||||
key={g.uic || nom}
|
||||
mode="flat"
|
||||
style={{ cursor: 'pointer', border: '1px solid var(--wcs-light)' }}
|
||||
onClick={() => handleSelectGare(nom)}
|
||||
>
|
||||
<WcsCardBody style={{ padding: '12px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontWeight: 'bold', fontSize: '16px', color: 'var(--wcs-primary)' }}>{nom}</div>
|
||||
{g.commune && (
|
||||
<div style={{ fontSize: '13px', color: 'var(--wcs-text-light)' }}>
|
||||
{g.commune} {g.departement ? `(${g.departement})` : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{g.uic && <WcsBadge>{g.uic}</WcsBadge>}
|
||||
<span style={{ color: 'var(--wcs-primary)', fontSize: '20px', fontWeight: 'bold' }}>›</span>
|
||||
</div>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ borderTop: '1px solid var(--wcs-light)', paddingTop: '16px' }}>
|
||||
<WcsButton mode="clear" onClick={onBack}>
|
||||
← Retour à l'accueil
|
||||
</WcsButton>
|
||||
</div>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+98
-125
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import WcsButton from '../components/WcsButton';
|
||||
import { WcsCard, WcsCardBody, WcsButton, WcsInput, WcsBadge, WcsHorizontalStepper } from 'wcs-react';
|
||||
import sessionMemory from '../lib/sessionMemory';
|
||||
import { getSourceFromPath, searchGares } from '../services/railwayService';
|
||||
|
||||
@@ -90,139 +90,112 @@ export default function Departs({ onBack, navigate }) {
|
||||
}
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{ text: 'Choix de la gare', button: { kind: 'Text', text: '1' }, complete: false },
|
||||
{ text: "Type d'afficheur", button: { kind: 'Text', text: '2' }, disable: true },
|
||||
{ text: 'Options & Affichage', button: { kind: 'Text', text: '3' }, disable: true }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="launcher-container">
|
||||
{/* Stepper */}
|
||||
<div className="launcher-stepper">
|
||||
<div className="launcher-step launcher-step--active">
|
||||
<span className="launcher-step-num">1</span>
|
||||
<span>Choix de la gare</span>
|
||||
</div>
|
||||
<span className="launcher-step-separator">›</span>
|
||||
<div className="launcher-step">
|
||||
<span className="launcher-step-num">2</span>
|
||||
<span>Type d'afficheur</span>
|
||||
</div>
|
||||
<span className="launcher-step-separator">›</span>
|
||||
<div className="launcher-step">
|
||||
<span className="launcher-step-num">3</span>
|
||||
<span>Options & Affichage</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '24px', overflowY: 'auto', maxHeight: 'calc(100vh - 120px)' }}>
|
||||
<WcsHorizontalStepper steps={steps} currentStep={0} style={{ marginBottom: '24px' }} />
|
||||
|
||||
<div className="launcher-card">
|
||||
{/* Header */}
|
||||
<div className="launcher-header">
|
||||
<div>
|
||||
<h1 className="launcher-title">🚆 Prochains Départs</h1>
|
||||
<p className="launcher-subtitle">Recherchez une gare pour afficher le tableau des départs en temps réel.</p>
|
||||
<WcsCard mode="raised">
|
||||
<WcsCardBody>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
|
||||
<div>
|
||||
<h1 style={{ margin: '0 0 8px 0', fontSize: '24px', color: 'var(--wcs-primary)' }}>🚆 Prochains Départs</h1>
|
||||
<p style={{ margin: 0, color: 'var(--wcs-text-light)' }}>Recherchez une gare pour afficher le tableau des départs en temps réel.</p>
|
||||
</div>
|
||||
<WcsBadge className={source === 'supabase' ? 'wcs-secondary' : 'wcs-primary'} shape="rounded">
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS Live'}
|
||||
</WcsBadge>
|
||||
</div>
|
||||
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS Live'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Barre de recherche */}
|
||||
<div className="launcher-search-box">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Rechercher une gare (ex: Dijon, Paris, Lyon, Besançon...)"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="launcher-search-input"
|
||||
aria-label="Rechercher une gare"
|
||||
autoFocus
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
type="button"
|
||||
className="launcher-search-clear"
|
||||
onClick={() => setSearch('')}
|
||||
aria-label="Effacer la recherche"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Suggestions rapides / Gares populaires */}
|
||||
<div className="launcher-chips-container">
|
||||
<span className="launcher-chips-label">Gares fréquentes :</span>
|
||||
<div className="launcher-chips-list">
|
||||
{POPULAR_GARES.map((gName) => (
|
||||
<button
|
||||
key={gName}
|
||||
type="button"
|
||||
className="launcher-chip"
|
||||
onClick={() => handleSelectGare(gName)}
|
||||
>
|
||||
{gName.replace(/-/g, ' ')}
|
||||
</button>
|
||||
))}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<WcsInput
|
||||
type="text"
|
||||
icon="search"
|
||||
placeholder="Rechercher une gare (ex: Dijon, Paris, Lyon, Besançon...)"
|
||||
value={search}
|
||||
onWcsInput={(e) => setSearch(e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
clearable={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && <p style={{ color: '#00439e', fontWeight: 600 }}>⏳ Chargement des gares...</p>}
|
||||
{error && <p style={{ color: '#d70022', fontWeight: 600 }}>⚠️ Erreur : {error}</p>}
|
||||
|
||||
{/* Invitation à la recherche en mode SNCF */}
|
||||
{source === 'sncf' && search.trim().length < 2 && (
|
||||
<div style={{ textAlign: 'center', padding: '24px 16px', color: '#666', background: '#f8fafc', borderRadius: '8px', border: '1px dashed #d0d7de' }}>
|
||||
<p style={{ margin: 0, fontSize: '14px' }}>
|
||||
💡 Saisissez au moins <strong>2 lettres</strong> pour chercher parmi plus de 3 500 gares du réseau français.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Aucun résultat */}
|
||||
{source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '24px', color: '#777', background: '#fff', borderRadius: '8px' }}>
|
||||
Aucune gare trouvée pour « <strong>{search}</strong> ».
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Liste des résultats */}
|
||||
{displayedGares.length > 0 && (
|
||||
<div className="launcher-results-list" role="list">
|
||||
{displayedGares.map((g) => {
|
||||
const nom = g.nom;
|
||||
return (
|
||||
<div
|
||||
key={g.uic || nom}
|
||||
className="launcher-result-item"
|
||||
onClick={() => handleSelectGare(nom)}
|
||||
onKeyDown={(e) => (e.key === 'Enter' ? handleSelectGare(nom) : null)}
|
||||
tabIndex={0}
|
||||
role="listitem"
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<p style={{ fontWeight: 'bold', marginBottom: '12px' }}>Gares fréquentes :</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||
{POPULAR_GARES.map((gName) => (
|
||||
<WcsButton
|
||||
key={gName}
|
||||
mode="stroked"
|
||||
shape="round"
|
||||
size="s"
|
||||
onClick={() => handleSelectGare(gName)}
|
||||
>
|
||||
<div>
|
||||
<div className="launcher-result-name">{nom}</div>
|
||||
{g.commune && (
|
||||
<div className="launcher-result-sub">
|
||||
{g.commune} {g.departement ? `(${g.departement})` : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{g.uic && <span className="launcher-result-tag">UIC {g.uic}</span>}
|
||||
<span style={{ color: '#00439e', fontSize: '18px', fontWeight: 'bold' }}>›</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{gName.replace(/-/g, ' ')}
|
||||
</WcsButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation retour */}
|
||||
<div className="launcher-footer-nav">
|
||||
<WcsButton
|
||||
mode="plain"
|
||||
onClick={onBack}
|
||||
label="← Retour à l'accueil"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{loading && <p style={{ color: 'var(--wcs-primary)', fontWeight: 600 }}>⏳ Chargement des gares...</p>}
|
||||
{error && <p style={{ color: 'var(--wcs-danger)', fontWeight: 600 }}>⚠️ Erreur : {error}</p>}
|
||||
|
||||
{source === 'sncf' && search.trim().length < 2 && (
|
||||
<div style={{ textAlign: 'center', padding: '24px 16px', color: 'var(--wcs-text-light)', background: 'var(--wcs-light)', borderRadius: '8px', border: '1px dashed var(--wcs-gray)' }}>
|
||||
<p style={{ margin: 0, fontSize: '14px' }}>
|
||||
💡 Saisissez au moins <strong>2 lettres</strong> pour chercher parmi plus de 3 500 gares du réseau français.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '24px', color: 'var(--wcs-text-light)', background: 'var(--wcs-white)', borderRadius: '8px' }}>
|
||||
Aucune gare trouvée pour « <strong>{search}</strong> ».
|
||||
</div>
|
||||
)}
|
||||
|
||||
{displayedGares.length > 0 && (
|
||||
<div role="list" style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '24px' }}>
|
||||
{displayedGares.map((g) => {
|
||||
const nom = g.nom;
|
||||
return (
|
||||
<WcsCard
|
||||
key={g.uic || nom}
|
||||
mode="flat"
|
||||
style={{ cursor: 'pointer', border: '1px solid var(--wcs-light)' }}
|
||||
onClick={() => handleSelectGare(nom)}
|
||||
>
|
||||
<WcsCardBody style={{ padding: '12px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontWeight: 'bold', fontSize: '16px', color: 'var(--wcs-primary)' }}>{nom}</div>
|
||||
{g.commune && (
|
||||
<div style={{ fontSize: '13px', color: 'var(--wcs-text-light)' }}>
|
||||
{g.commune} {g.departement ? `(${g.departement})` : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{g.uic && <WcsBadge>{g.uic}</WcsBadge>}
|
||||
<span style={{ color: 'var(--wcs-primary)', fontSize: '20px', fontWeight: 'bold' }}>›</span>
|
||||
</div>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ borderTop: '1px solid var(--wcs-light)', paddingTop: '16px' }}>
|
||||
<WcsButton mode="clear" onClick={onBack}>
|
||||
← Retour à l'accueil
|
||||
</WcsButton>
|
||||
</div>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+62
-87
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import WcsButton from '../components/WcsButton';
|
||||
import { WcsCard, WcsCardBody, WcsButton, WcsBadge, WcsHorizontalStepper } from 'wcs-react';
|
||||
import sessionMemory from '../lib/sessionMemory';
|
||||
import { getSourceFromPath } from '../services/railwayService';
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function Displays({ onBack, navigate }) {
|
||||
tag: 'Gares Connectées',
|
||||
tagType: 'modern',
|
||||
icon: '📱',
|
||||
desc: 'Écrans d\'information enrichis avec multimédia, infogare et dessertes complètes.'
|
||||
desc: "Écrans d'information enrichis avec multimédia, infogare et dessertes complètes."
|
||||
},
|
||||
{
|
||||
id: 'transilien',
|
||||
@@ -46,8 +46,7 @@ export default function Displays({ onBack, navigate }) {
|
||||
}
|
||||
];
|
||||
|
||||
// Nettoyage du chemin pour extraire la gare même avec /ferrovia
|
||||
const cleanPath = (typeof window !== 'undefined' ? window.location.pathname : '').replace(/^\/ferrovia/, '');
|
||||
const cleanPath = (typeof window !== 'undefined' ? window.location.pathname : '').replace(/ferrovia/, '');
|
||||
const parts = cleanPath.split('/').filter(Boolean);
|
||||
const typeIds = types.map(t => t.id);
|
||||
let gare = null;
|
||||
@@ -63,14 +62,13 @@ export default function Displays({ onBack, navigate }) {
|
||||
const g = paramsForGare.get('gare');
|
||||
if (g) {
|
||||
try {
|
||||
gare = decodeURIComponent(String(g).replace(/\+/g, ' ')).replace(/-/g, ' ');
|
||||
gare = decodeURIComponent(String(g).replace(/\\+/g, ' ')).replace(/-/g, ' ');
|
||||
} catch (e) {
|
||||
gare = String(g).replace(/-/g, ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Déterminer le mode (departs/arrivees)
|
||||
const params = new URLSearchParams(window.location.search || '');
|
||||
let mode = params.get('mode');
|
||||
if (!mode) {
|
||||
@@ -83,96 +81,73 @@ export default function Displays({ onBack, navigate }) {
|
||||
|
||||
const formattedGareName = gare ? gare.replace(/-/g, ' ') : 'Non sélectionnée';
|
||||
|
||||
const steps = [
|
||||
{ text: formattedGareName, button: { kind: 'Icon', iconName: 'check' }, complete: true },
|
||||
{ text: "Type d'afficheur", button: { kind: 'Text', text: '2' }, complete: false },
|
||||
{ text: 'Options & Affichage', button: { kind: 'Text', text: '3' }, disable: true }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="launcher-container">
|
||||
{/* Stepper */}
|
||||
<div className="launcher-stepper">
|
||||
<div className="launcher-step launcher-step--completed">
|
||||
<span className="launcher-step-num">✓</span>
|
||||
<span>{formattedGareName}</span>
|
||||
</div>
|
||||
<span className="launcher-step-separator">›</span>
|
||||
<div className="launcher-step launcher-step--active">
|
||||
<span className="launcher-step-num">2</span>
|
||||
<span>Type d'afficheur</span>
|
||||
</div>
|
||||
<span className="launcher-step-separator">›</span>
|
||||
<div className="launcher-step">
|
||||
<span className="launcher-step-num">3</span>
|
||||
<span>Options & Affichage</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '24px', overflowY: 'auto', maxHeight: 'calc(100vh - 120px)' }}>
|
||||
<WcsHorizontalStepper steps={steps} currentStep={1} style={{ marginBottom: '24px' }} />
|
||||
|
||||
<div className="launcher-card">
|
||||
{/* Header */}
|
||||
<div className="launcher-header">
|
||||
<div>
|
||||
<h1 className="launcher-title">Sélectionnez le style d'afficheur</h1>
|
||||
<p className="launcher-subtitle">
|
||||
Gare : <strong style={{ color: '#002257' }}>{formattedGareName}</strong> • Mode : <strong style={{ color: '#00439e' }}>{mode === 'arrivees' ? '🏁 Arrivées' : '🚆 Départs'}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Grille de sélection tactile des styles d'afficheurs */}
|
||||
<div className="launcher-grid launcher-grid--4col">
|
||||
{types.map((t) => (
|
||||
<div
|
||||
key={t.id}
|
||||
className="launcher-select-card"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (typeof navigate === 'function') {
|
||||
const kebab = gare ? String(gare).trim().replace(/\s+/g, '-').replace(/\//g, '/') : '';
|
||||
navigate(`${prefix}/select-afficheur?gare=${encodeURIComponent(kebab)}&mode=${mode}&type=${t.id}`);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
const kebab = gare ? String(gare).trim().replace(/\s+/g, '-').replace(/\//g, '/') : '';
|
||||
navigate(`${prefix}/select-afficheur?gare=${encodeURIComponent(kebab)}&mode=${mode}&type=${t.id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span className="launcher-card-icon">{t.icon}</span>
|
||||
<span className={`launcher-card-tag ${t.tagType === 'retro' ? 'launcher-card-tag--retro' : 'launcher-card-tag--modern'}`}>
|
||||
{t.tag}
|
||||
</span>
|
||||
<h2 className="launcher-card-title">{t.label}</h2>
|
||||
<div style={{ fontSize: '13px', fontWeight: 600, color: '#00439e', marginBottom: '8px' }}>{t.sublabel}</div>
|
||||
<p className="launcher-card-desc">{t.desc}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<span className="launcher-btn launcher-btn--secondary" style={{ padding: '8px 12px', fontSize: '14px' }}>
|
||||
Choisir ce style ›
|
||||
</span>
|
||||
</div>
|
||||
<WcsCard mode="raised">
|
||||
<WcsCardBody>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '24px' }}>
|
||||
<div>
|
||||
<h1 style={{ margin: '0 0 8px 0', fontSize: '24px', color: 'var(--wcs-primary)' }}>Sélectionnez le style d'afficheur</h1>
|
||||
<p style={{ margin: 0, color: 'var(--wcs-text-light)' }}>
|
||||
Gare : <strong>{formattedGareName}</strong> • Mode : <strong>{mode === 'arrivees' ? '🏁 Arrivées' : '🚆 Départs'}</strong>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<WcsBadge className={source === 'supabase' ? 'wcs-secondary' : 'wcs-primary'} shape="rounded">
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS'}
|
||||
</WcsBadge>
|
||||
</div>
|
||||
|
||||
{/* Navigation retour */}
|
||||
<div className="launcher-footer-nav">
|
||||
<WcsButton
|
||||
mode="plain"
|
||||
onClick={() => {
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '16px', marginBottom: '24px' }}>
|
||||
{types.map((t) => (
|
||||
<WcsCard
|
||||
key={t.id}
|
||||
mode="flat"
|
||||
style={{ cursor: 'pointer', border: '1px solid var(--wcs-light)' }}
|
||||
onClick={() => {
|
||||
if (typeof navigate === 'function') {
|
||||
const kebab = gare ? String(gare).trim().replace(/\\s+/g, '-').replace(/\\/g, '/') : '';
|
||||
navigate(`${prefix}/select-afficheur?gare=${encodeURIComponent(kebab)}&mode=${mode}&type=${t.id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<WcsCardBody style={{ padding: '16px', display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<span style={{ fontSize: '32px', marginRight: '8px' }}>{t.icon}</span>
|
||||
<WcsBadge className={t.tagType === 'retro' ? 'wcs-secondary' : 'wcs-success'}>{t.tag}</WcsBadge>
|
||||
</div>
|
||||
<h2 style={{ fontSize: '18px', margin: '0 0 4px 0', color: 'var(--wcs-primary)' }}>{t.label}</h2>
|
||||
<div style={{ fontSize: '13px', fontWeight: 'bold', color: 'var(--wcs-primary)', marginBottom: '8px' }}>{t.sublabel}</div>
|
||||
<p style={{ fontSize: '14px', color: 'var(--wcs-text-light)', flexGrow: 1, marginBottom: '16px' }}>{t.desc}</p>
|
||||
|
||||
<WcsButton className="wcs-secondary" style={{ width: '100%', marginTop: 'auto' }}>
|
||||
Choisir ce style
|
||||
</WcsButton>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ borderTop: '1px solid var(--wcs-light)', paddingTop: '16px' }}>
|
||||
<WcsButton mode="clear" onClick={() => {
|
||||
if (typeof onBack === 'function') {
|
||||
onBack();
|
||||
} else if (navigate) {
|
||||
navigate(`${prefix}/${mode}`);
|
||||
}
|
||||
}}
|
||||
label="← Changer de gare"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}}>
|
||||
← Changer de gare
|
||||
</WcsButton>
|
||||
</div>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+186
-375
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import WcsButton from '../components/WcsButton';
|
||||
import { WcsCard, WcsCardBody, WcsButton, WcsBadge, WcsCheckbox, WcsSelect, WcsSelectOption, WcsInput, WcsHorizontalStepper } from 'wcs-react';
|
||||
import { getSourceFromPath, getPlatformsForStation } from '../services/railwayService';
|
||||
|
||||
export default function SelectAfficheur({ onBack, navigate }) {
|
||||
@@ -16,7 +16,6 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
||||
const [manualPlatform, setManualPlatform] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// État des pages sélectionnées pour les afficheurs AFL
|
||||
const [aflPages, setAflPages] = useState({
|
||||
horaires: true,
|
||||
infos: false,
|
||||
@@ -25,8 +24,7 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
||||
corresp: false
|
||||
});
|
||||
|
||||
// Options spécifiques pour les afficheurs Classiques
|
||||
const [extendedLines, setExtendedLines] = useState(0); // 0, 1, 2, 3
|
||||
const [extendedLines, setExtendedLines] = useState(0);
|
||||
const [showTime, setShowTime] = useState(false);
|
||||
|
||||
const toggleAflPage = (key) => {
|
||||
@@ -43,9 +41,8 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
||||
});
|
||||
};
|
||||
|
||||
// Decode and format station name for display
|
||||
const decodedGare = gareParam
|
||||
? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ')
|
||||
? decodeURIComponent(String(gareParam).replace(/\\+/g, ' ')).replace(/-/g, ' ')
|
||||
: '';
|
||||
|
||||
useEffect(() => {
|
||||
@@ -129,398 +126,212 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
||||
}
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{ text: decodedGare || 'Gare', button: { kind: 'Icon', iconName: 'check' }, complete: true },
|
||||
{ text: typeParam.toUpperCase(), button: { kind: 'Icon', iconName: 'check' }, complete: true },
|
||||
{ text: 'Options & Affichage', button: { kind: 'Text', text: '3' }, complete: false }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="launcher-container">
|
||||
{/* Stepper */}
|
||||
<div className="launcher-stepper">
|
||||
<div className="launcher-step launcher-step--completed">
|
||||
<span className="launcher-step-num">✓</span>
|
||||
<span>{decodedGare || 'Gare'}</span>
|
||||
</div>
|
||||
<span className="launcher-step-separator">›</span>
|
||||
<div className="launcher-step launcher-step--completed">
|
||||
<span className="launcher-step-num">✓</span>
|
||||
<span>{typeParam.toUpperCase()}</span>
|
||||
</div>
|
||||
<span className="launcher-step-separator">›</span>
|
||||
<div className="launcher-step launcher-step--active">
|
||||
<span className="launcher-step-num">3</span>
|
||||
<span>Options & Affichage</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '24px' }}>
|
||||
<WcsHorizontalStepper steps={steps} currentStep={2} style={{ marginBottom: '24px' }} />
|
||||
|
||||
<div className="launcher-card">
|
||||
{/* Header */}
|
||||
<div className="launcher-header">
|
||||
<div>
|
||||
<h1 className="launcher-title">Configuration de l'afficheur</h1>
|
||||
<p className="launcher-subtitle">
|
||||
Gare : <strong style={{ color: '#002257' }}>{decodedGare}</strong> • Mode : <strong style={{ color: '#00439e' }}>{modeParam === 'arrivees' ? '🏁 Arrivées' : '🚆 Départs'}</strong> • Style : <strong style={{ color: '#008b47' }}>{typeParam.toUpperCase()}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Grille responsive 2 colonnes (1 col sur mobile) */}
|
||||
<div className="launcher-grid launcher-grid--2col">
|
||||
{/* Option 1 : Afficheur Général */}
|
||||
<div className="launcher-select-card" style={{ cursor: 'default' }}>
|
||||
<WcsCard mode="raised">
|
||||
<WcsCardBody>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '24px' }}>
|
||||
<div>
|
||||
<span className="launcher-card-icon">🖥️</span>
|
||||
<span className="launcher-card-tag launcher-card-tag--modern">Écran Général</span>
|
||||
<h2 className="launcher-card-title">Afficheur Général (Hall)</h2>
|
||||
<p className="launcher-card-desc">
|
||||
Affiche la liste complète des circulations avec basculement automatique des pages.
|
||||
<h1 style={{ margin: '0 0 8px 0', fontSize: '24px', color: 'var(--wcs-primary)' }}>Configuration de l'afficheur</h1>
|
||||
<p style={{ margin: 0, color: 'var(--wcs-text-light)' }}>
|
||||
Gare : <strong>{decodedGare}</strong> • Mode : <strong>{modeParam === 'arrivees' ? '🏁 Arrivées' : '🚆 Départs'}</strong> • Style : <strong>{typeParam.toUpperCase()}</strong>
|
||||
</p>
|
||||
|
||||
{typeParam === 'afl' && (
|
||||
<div className="launcher-checklist-box">
|
||||
<div className="launcher-checklist-header">
|
||||
<span className="launcher-checklist-title">
|
||||
Pages incluses dans la boucle :
|
||||
</span>
|
||||
<div className="launcher-checklist-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectAllAflPages(true)}
|
||||
className="launcher-link-btn"
|
||||
>
|
||||
Tout cocher
|
||||
</button>
|
||||
<span style={{ color: '#ccc' }}>|</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectAllAflPages(false)}
|
||||
className="launcher-link-btn"
|
||||
style={{ color: '#666' }}
|
||||
>
|
||||
Horaires seuls
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<label className="launcher-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aflPages.horaires}
|
||||
onChange={() => toggleAflPage('horaires')}
|
||||
className="launcher-checkbox"
|
||||
/>
|
||||
<span><strong>Page 1 & 2</strong> (Horaires des trains)</span>
|
||||
</label>
|
||||
|
||||
<label className="launcher-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aflPages.infos}
|
||||
onChange={() => toggleAflPage('infos')}
|
||||
className="launcher-checkbox"
|
||||
/>
|
||||
<span><strong>Informations</strong> (Trafic & Travaux)</span>
|
||||
</label>
|
||||
|
||||
<label className="launcher-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aflPages.securite1}
|
||||
onChange={() => toggleAflPage('securite1')}
|
||||
className="launcher-checkbox"
|
||||
/>
|
||||
<span><strong>Sécurité 1</strong> (Bagages délaissés)</span>
|
||||
</label>
|
||||
|
||||
<label className="launcher-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aflPages.securite2}
|
||||
onChange={() => toggleAflPage('securite2')}
|
||||
className="launcher-checkbox"
|
||||
/>
|
||||
<span><strong>Sécurité 2</strong> (Vigipirate / 3117)</span>
|
||||
</label>
|
||||
|
||||
<label className="launcher-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aflPages.corresp}
|
||||
onChange={() => toggleAflPage('corresp')}
|
||||
className="launcher-checkbox"
|
||||
/>
|
||||
<span><strong>Correspondances</strong> (Bus & Tram)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{typeParam === 'classiques' && (
|
||||
<div className="launcher-checklist-box">
|
||||
<div className="launcher-checklist-header">
|
||||
<span className="launcher-checklist-title">
|
||||
Options d'affichage Classiques :
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Choix du nombre de lignes étendues */}
|
||||
<div style={{ marginBottom: '14px' }}>
|
||||
<label style={{ display: 'block', fontSize: '13px', fontWeight: 700, color: '#002257', marginBottom: '8px' }}>
|
||||
Lignes étendues (Dessertes défilantes) :
|
||||
</label>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '6px' }}>
|
||||
{[0, 1, 2, 3].map((num) => (
|
||||
<button
|
||||
key={num}
|
||||
type="button"
|
||||
onClick={() => setExtendedLines(num)}
|
||||
style={{
|
||||
padding: '8px 4px',
|
||||
borderRadius: '8px',
|
||||
border: extendedLines === num ? '2px solid #00439e' : '1px solid #c2dbfe',
|
||||
background: extendedLines === num ? '#00439e' : '#ffffff',
|
||||
color: extendedLines === num ? '#ffffff' : '#002257',
|
||||
fontWeight: 700,
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease'
|
||||
}}
|
||||
>
|
||||
{num === 0 ? '0 (Standard)' : `${num} étendue${num > 1 ? 's' : ''}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#555', marginTop: '6px' }}>
|
||||
{extendedLines > 0
|
||||
? `Les ${extendedLines} premier${extendedLines > 1 ? 's' : ''} train${extendedLines > 1 ? 's' : ''} s'afficheront sur 2 lignes réduites avec le défilement de toutes les gares.`
|
||||
: 'Affichage classique compact sans défilement de gares.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Option Afficher l'heure */}
|
||||
<div style={{ borderTop: '1px solid #e2ebf6', paddingTop: '10px' }}>
|
||||
<label className="launcher-checkbox-label" style={{ cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showTime}
|
||||
onChange={(e) => setShowTime(e.target.checked)}
|
||||
className="launcher-checkbox"
|
||||
/>
|
||||
<span>
|
||||
<strong>Afficher l'heure & informations</strong>
|
||||
</span>
|
||||
</label>
|
||||
<div style={{ fontSize: '12px', color: '#555', marginLeft: '28px', marginTop: '2px' }}>
|
||||
Réserve la 10e ligne pour l'horloge numérique [ HH MM SS ] et un bandeau d'information défilant (horaires sur 9 lignes max).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGeneral}
|
||||
className="launcher-btn launcher-btn--primary"
|
||||
>
|
||||
🖥️ Lancer l'Afficheur Général
|
||||
</button>
|
||||
<WcsBadge className={source === 'supabase' ? 'wcs-secondary' : 'wcs-primary'} shape="rounded">
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS'}
|
||||
</WcsBadge>
|
||||
</div>
|
||||
|
||||
{/* Option 2 : Afficheur par Quai */}
|
||||
<div className="launcher-select-card" style={{ cursor: 'default' }}>
|
||||
<div>
|
||||
<span className="launcher-card-icon">🚉</span>
|
||||
<span className="launcher-card-tag launcher-card-tag--retro">Écran de Voie</span>
|
||||
<h2 className="launcher-card-title">Afficheur par Quai</h2>
|
||||
<p className="launcher-card-desc">
|
||||
Affiche le prochain train au départ/arrivée avec la liste déroulante des gares desservies sur la voie.
|
||||
</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '24px', marginBottom: '24px' }}>
|
||||
|
||||
<WcsCard mode="flat" style={{ border: '1px solid var(--wcs-light)' }}>
|
||||
<WcsCardBody style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<span style={{ fontSize: '24px', marginRight: '8px' }}>🖥️</span>
|
||||
<WcsBadge className="wcs-info">Écran Général</WcsBadge>
|
||||
</div>
|
||||
<h2 style={{ fontSize: '20px', margin: '0 0 8px 0' }}>Afficheur Général (Hall)</h2>
|
||||
<p style={{ fontSize: '14px', color: 'var(--wcs-text-light)', marginBottom: '16px' }}>
|
||||
Affiche la liste complète des circulations avec basculement automatique des pages.
|
||||
</p>
|
||||
|
||||
<div style={{ background: '#f8fafc', border: '1px solid #e2ebf6', borderRadius: '10px', padding: '16px', marginBottom: '20px' }}>
|
||||
{loading ? (
|
||||
<div style={{ fontSize: '14px', color: '#00439e', fontWeight: 600 }}>⏳ Détection des voies réelles...</div>
|
||||
) : (
|
||||
<div>
|
||||
{platforms.length > 0 ? (
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', fontSize: '13px', fontWeight: 700, color: '#002257', marginBottom: '6px' }}>
|
||||
Voies actives détectées :
|
||||
</label>
|
||||
<select
|
||||
value={selectedPlatform}
|
||||
onChange={(e) => {
|
||||
setSelectedPlatform(e.target.value);
|
||||
setManualPlatform('');
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '2px solid #c2dbfe',
|
||||
fontSize: '15px',
|
||||
fontWeight: 600,
|
||||
background: '#ffffff',
|
||||
color: '#002257'
|
||||
}}
|
||||
>
|
||||
{platforms.map(p => (
|
||||
<option key={p} value={p}>Voie {p}</option>
|
||||
))}
|
||||
</select>
|
||||
{typeParam === 'afl' && (
|
||||
<div style={{ background: 'var(--wcs-light)', padding: '16px', borderRadius: '4px', marginBottom: '16px', flexGrow: 1 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '16px' }}>
|
||||
<span style={{ fontWeight: 'bold' }}>Pages incluses dans la boucle :</span>
|
||||
<div>
|
||||
<WcsButton mode="clear" size="s" onClick={() => selectAllAflPages(true)}>Tout cocher</WcsButton>
|
||||
<WcsButton mode="clear" size="s" onClick={() => selectAllAflPages(false)}>Horaires seuls</WcsButton>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ fontSize: '13px', color: '#666', marginBottom: '16px', background: '#fff', padding: '8px 12px', borderRadius: '6px' }}>
|
||||
ℹ️ Saisie manuelle de la voie ci-dessous.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '13px', fontWeight: 700, color: '#002257', marginBottom: '6px' }}>
|
||||
Ou saisie libre d'une voie :
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ex: A, B, 1, 2 bis..."
|
||||
value={manualPlatform}
|
||||
onChange={(e) => setManualPlatform(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '2px solid #e2ebf6',
|
||||
fontSize: '15px',
|
||||
boxSizing: 'border-box',
|
||||
background: '#ffffff'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<WcsCheckbox checked={aflPages.horaires} onWcsChange={() => toggleAflPage('horaires')}><strong>Page 1 & 2</strong> (Horaires)</WcsCheckbox>
|
||||
<WcsCheckbox checked={aflPages.infos} onWcsChange={() => toggleAflPage('infos')}><strong>Informations</strong> (Trafic)</WcsCheckbox>
|
||||
<WcsCheckbox checked={aflPages.securite1} onWcsChange={() => toggleAflPage('securite1')}><strong>Sécurité 1</strong> (Bagages)</WcsCheckbox>
|
||||
<WcsCheckbox checked={aflPages.securite2} onWcsChange={() => toggleAflPage('securite2')}><strong>Sécurité 2</strong> (Vigipirate)</WcsCheckbox>
|
||||
<WcsCheckbox checked={aflPages.corresp} onWcsChange={() => toggleAflPage('corresp')}><strong>Correspondances</strong></WcsCheckbox>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePlatform}
|
||||
className="launcher-btn launcher-btn--success"
|
||||
>
|
||||
🚉 Lancer l'Afficheur de Quai
|
||||
</button>
|
||||
{typeParam === 'classiques' && (
|
||||
<div style={{ background: 'var(--wcs-light)', padding: '16px', borderRadius: '4px', marginBottom: '16px', flexGrow: 1 }}>
|
||||
<p style={{ fontWeight: 'bold', marginBottom: '12px' }}>Options d'affichage :</p>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', fontSize: '14px', marginBottom: '8px' }}>Lignes étendues :</label>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
{[0, 1, 2, 3].map(num => (
|
||||
<WcsButton
|
||||
key={num}
|
||||
mode={extendedLines === num ? 'solid' : 'stroked'}
|
||||
size="s"
|
||||
onClick={() => setExtendedLines(num)}
|
||||
>
|
||||
{num === 0 ? '0' : num}
|
||||
</WcsButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<WcsCheckbox checked={showTime} onWcsChange={(e) => setShowTime(e.detail.checked)}><strong>Afficher l'heure & infos</strong></WcsCheckbox>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<WcsButton className="wcs-primary" style={{ width: '100%', marginTop: 'auto' }} onClick={handleGeneral}>
|
||||
Lancer l'Afficheur Général
|
||||
</WcsButton>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
|
||||
<WcsCard mode="flat" style={{ border: '1px solid var(--wcs-light)' }}>
|
||||
<WcsCardBody style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<span style={{ fontSize: '24px', marginRight: '8px' }}>🚉</span>
|
||||
<WcsBadge className="wcs-warning">Écran de Voie</WcsBadge>
|
||||
</div>
|
||||
<h2 style={{ fontSize: '20px', margin: '0 0 8px 0' }}>Afficheur par Quai</h2>
|
||||
<p style={{ fontSize: '14px', color: 'var(--wcs-text-light)', marginBottom: '16px' }}>
|
||||
Affiche le prochain train au départ/arrivée avec la liste déroulante des gares desservies sur la voie.
|
||||
</p>
|
||||
|
||||
<div style={{ background: 'var(--wcs-light)', padding: '16px', borderRadius: '4px', marginBottom: '16px', flexGrow: 1 }}>
|
||||
{loading ? (
|
||||
<p style={{ fontWeight: 'bold', color: 'var(--wcs-primary)' }}>⏳ Détection des voies...</p>
|
||||
) : (
|
||||
<>
|
||||
{platforms.length > 0 && (
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', fontWeight: 'bold', marginBottom: '8px', fontSize: '14px' }}>Voies actives détectées :</label>
|
||||
<WcsSelect
|
||||
value={selectedPlatform}
|
||||
onWcsChange={(e) => {
|
||||
setSelectedPlatform(e.detail.value);
|
||||
setManualPlatform('');
|
||||
}}
|
||||
>
|
||||
{platforms.map(p => (
|
||||
<WcsSelectOption key={p} value={p}>Voie {p}</WcsSelectOption>
|
||||
))}
|
||||
</WcsSelect>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontWeight: 'bold', marginBottom: '8px', fontSize: '14px' }}>Saisie libre d'une voie :</label>
|
||||
<WcsInput
|
||||
placeholder="Ex: A, B, 1, 2 bis..."
|
||||
value={manualPlatform}
|
||||
onWcsInput={(e) => setManualPlatform(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<WcsButton className="wcs-success" style={{ width: '100%', marginTop: 'auto' }} onClick={handlePlatform}>
|
||||
Lancer l'Afficheur de Quai
|
||||
</WcsButton>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
|
||||
{typeParam === 'classiques' && (
|
||||
<>
|
||||
<WcsCard mode="flat" style={{ border: '1px solid var(--wcs-light)' }}>
|
||||
<WcsCardBody style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<span style={{ fontSize: '24px', marginRight: '8px' }}>⚡</span>
|
||||
<WcsBadge className="wcs-warning">Flash Info Trafic</WcsBadge>
|
||||
</div>
|
||||
<h2 style={{ fontSize: '20px', margin: '0 0 8px 0' }}>Flash Information</h2>
|
||||
<p style={{ fontSize: '14px', color: 'var(--wcs-text-light)', marginBottom: '16px', flexGrow: 1 }}>
|
||||
Écran dédié aux perturbations de circulation en temps réel et travaux de maintenance sur le réseau.
|
||||
</p>
|
||||
<WcsButton className="wcs-warning" style={{ width: '100%', marginTop: 'auto' }} onClick={() => {
|
||||
const kebab = gareParam ? String(gareParam).trim() : '';
|
||||
navigate(`${prefix}/afficheurs/classiques/flash-info?gare=${encodeURIComponent(kebab)}`);
|
||||
}}>
|
||||
Lancer le Flash Information
|
||||
</WcsButton>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
|
||||
<WcsCard mode="flat" style={{ border: '1px solid var(--wcs-light)' }}>
|
||||
<WcsCardBody style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<span style={{ fontSize: '24px', marginRight: '8px' }}>ℹ️</span>
|
||||
<WcsBadge className="wcs-info">Messages Gare</WcsBadge>
|
||||
</div>
|
||||
<h2 style={{ fontSize: '20px', margin: '0 0 8px 0' }}>Informations Voyageurs</h2>
|
||||
<p style={{ fontSize: '14px', color: 'var(--wcs-text-light)', marginBottom: '16px', flexGrow: 1 }}>
|
||||
Écran d'annonces et consignes dynamiques de la gare (Vigipirate, étiquetage des bagages, sécurité).
|
||||
</p>
|
||||
<WcsButton className="wcs-info" style={{ width: '100%', marginTop: 'auto' }} onClick={() => {
|
||||
const kebab = gareParam ? String(gareParam).trim() : '';
|
||||
navigate(`${prefix}/afficheurs/classiques/informations?gare=${encodeURIComponent(kebab)}`);
|
||||
}}>
|
||||
Lancer l'Afficheur Informations
|
||||
</WcsButton>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Options 3 & 4 pour le mode Classiques : Flash Info & Informations */}
|
||||
{typeParam === 'classiques' && (
|
||||
<>
|
||||
{/* Option 3 : Flash Information */}
|
||||
<div className="launcher-select-card" style={{ cursor: 'default' }}>
|
||||
<div>
|
||||
<span className="launcher-card-icon">⚡</span>
|
||||
<span className="launcher-card-tag launcher-card-tag--retro" style={{ background: '#fab23e', color: '#00337f' }}>
|
||||
Flash Info Trafic
|
||||
</span>
|
||||
<h2 className="launcher-card-title">Flash Information</h2>
|
||||
<p className="launcher-card-desc">
|
||||
Écran dédié aux perturbations de circulation en temps réel et travaux de maintenance sur le réseau.
|
||||
</p>
|
||||
<div style={{ background: '#f8fafc', border: '1px solid #e2ebf6', borderRadius: '10px', padding: '16px', marginBottom: '20px' }}>
|
||||
<div style={{ fontSize: '13px', color: '#00337f', fontWeight: 600 }}>
|
||||
⚡ Conforme au design historique <em>Flash Circulation</em> avec fond ambre et bandeau bleu nuit.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const kebab = gareParam ? String(gareParam).trim() : '';
|
||||
navigate(`${prefix}/afficheurs/classiques/flash-info?gare=${encodeURIComponent(kebab)}`);
|
||||
}}
|
||||
className="launcher-btn"
|
||||
style={{ background: '#00337f', color: '#fab23e', border: 'none', fontWeight: 700 }}
|
||||
>
|
||||
⚡ Lancer le Flash Information
|
||||
</button>
|
||||
{typeParam === 'afl' && (
|
||||
<div style={{ borderTop: '1px solid var(--wcs-light)', paddingTop: '24px', marginBottom: '24px' }}>
|
||||
<p style={{ fontWeight: 'bold', marginBottom: '12px' }}>Accès direct à une page fixe AFL :</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||
<WcsButton mode="stroked" size="s" onClick={() => handleSinglePage('1')}>Page 1</WcsButton>
|
||||
<WcsButton mode="stroked" size="s" onClick={() => handleSinglePage('2')}>Page 2</WcsButton>
|
||||
<WcsButton mode="stroked" size="s" onClick={() => handleSinglePage('infos')}>Informations</WcsButton>
|
||||
<WcsButton mode="stroked" size="s" onClick={() => handleSinglePage('securite1')}>Sécurité (Bagages)</WcsButton>
|
||||
<WcsButton mode="stroked" size="s" onClick={() => handleSinglePage('securite2')}>Sécurité (Vigipirate)</WcsButton>
|
||||
<WcsButton mode="stroked" size="s" onClick={() => handleSinglePage('corresp')}>Correspondances</WcsButton>
|
||||
</div>
|
||||
|
||||
{/* Option 4 : Informations (Messages Dynamiques) */}
|
||||
<div className="launcher-select-card" style={{ cursor: 'default' }}>
|
||||
<div>
|
||||
<span className="launcher-card-icon">ℹ️</span>
|
||||
<span className="launcher-card-tag launcher-card-tag--retro" style={{ background: '#00337f', color: '#ffffff' }}>
|
||||
Messages Gare
|
||||
</span>
|
||||
<h2 className="launcher-card-title">Informations Voyageurs</h2>
|
||||
<p className="launcher-card-desc">
|
||||
Écran d'annonces et consignes dynamiques de la gare (Vigipirate, étiquetage des bagages, sécurité en gare).
|
||||
</p>
|
||||
<div style={{ background: '#f8fafc', border: '1px solid #e2ebf6', borderRadius: '10px', padding: '16px', marginBottom: '20px' }}>
|
||||
<div style={{ fontSize: '13px', color: '#00337f', fontWeight: 600 }}>
|
||||
ℹ️ Conforme au design historique <em>Information</em> avec messages voyageurs de la gare de {decodedGare}.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const kebab = gareParam ? String(gareParam).trim() : '';
|
||||
navigate(`${prefix}/afficheurs/classiques/informations?gare=${encodeURIComponent(kebab)}`);
|
||||
}}
|
||||
className="launcher-btn"
|
||||
style={{ background: '#00337f', color: '#ffffff', border: 'none', fontWeight: 700 }}
|
||||
>
|
||||
ℹ️ Lancer l'Afficheur Informations
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Accès direct aux pages fixes AFL */}
|
||||
{typeParam === 'afl' && (
|
||||
<div style={{ borderTop: '1px solid #e2ebf6', paddingTop: '20px', marginBottom: '20px' }}>
|
||||
<span className="launcher-chips-label">Accès direct à une page fixe AFL :</span>
|
||||
<div className="launcher-chips-list">
|
||||
<button type="button" onClick={() => handleSinglePage('1')} className="launcher-chip">
|
||||
Page 1 (Horaires)
|
||||
</button>
|
||||
<button type="button" onClick={() => handleSinglePage('2')} className="launcher-chip">
|
||||
Page 2 (Horaires)
|
||||
</button>
|
||||
<button type="button" onClick={() => handleSinglePage('infos')} className="launcher-chip">
|
||||
Informations Trafic
|
||||
</button>
|
||||
<button type="button" onClick={() => handleSinglePage('securite1')} className="launcher-chip">
|
||||
Sécurité (Bagages)
|
||||
</button>
|
||||
<button type="button" onClick={() => handleSinglePage('securite2')} className="launcher-chip">
|
||||
Sécurité (Vigipirate / 3117)
|
||||
</button>
|
||||
<button type="button" onClick={() => handleSinglePage('corresp')} className="launcher-chip">
|
||||
Correspondances
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation retour */}
|
||||
<div className="launcher-footer-nav">
|
||||
<WcsButton
|
||||
mode="plain"
|
||||
onClick={() => {
|
||||
<div style={{ borderTop: '1px solid var(--wcs-light)', paddingTop: '16px' }}>
|
||||
<WcsButton mode="clear" onClick={() => {
|
||||
if (typeof onBack === 'function') {
|
||||
onBack();
|
||||
} else if (typeof navigate === 'function') {
|
||||
navigate(`${prefix}/afficheurs?gare=${encodeURIComponent(gareParam)}&mode=${modeParam}`);
|
||||
}
|
||||
}}
|
||||
label="← Retour au choix du style"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}}>
|
||||
← Retour au choix du style
|
||||
</WcsButton>
|
||||
</div>
|
||||
</WcsCardBody>
|
||||
</WcsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import './AFLArrivees.css';
|
||||
import supabase from '../../../lib/supabase';
|
||||
@@ -54,6 +55,215 @@ const BusIcon = ({ size = '59px', color = 'white', style = {} }) => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Icône Débarquement (Arrivées AFL)
|
||||
const AFLDebarquementIcon = ({ size = '49px', color = 'white', style = {} }) => (
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
width={size}
|
||||
height={size}
|
||||
fill="none"
|
||||
style={{ flexShrink: 0, ...style }}
|
||||
>
|
||||
{/* Voiture de train / Porte à gauche */}
|
||||
<path d="M10 4v24M10 4H4M10 28H4" stroke={color} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M10 25h3" stroke={color} strokeWidth="2.5" strokeLinecap="round" />
|
||||
<path d="M13 28h3" stroke={color} strokeWidth="2.5" strokeLinecap="round" />
|
||||
{/* Silhouette du passager descendant du train vers le quai (tourné vers la droite) */}
|
||||
<circle cx="20.5" cy="8.5" r="2.5" fill={color} />
|
||||
<path
|
||||
d="M19 13h4l1.5 7-2.5 1-1-4-2 7-3-1 1.5-10z"
|
||||
fill={color}
|
||||
/>
|
||||
<path d="M17 15l-3 3" stroke={color} strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Pastilles de modification de parcours
|
||||
const AFLRouteBadge = ({ type = 'modified', size = '52px', style = {} }) => {
|
||||
if (type === 'cancelled' || type === 'suppressed') {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 36 36"
|
||||
width={size}
|
||||
height={size}
|
||||
style={{ flexShrink: 0, marginRight: '14px', display: 'inline-block', verticalAlign: 'middle', ...style }}
|
||||
>
|
||||
<circle cx="18" cy="18" r="18" fill="#c8102e" />
|
||||
<path d="M11 11L25 25M25 11L11 25" stroke="#ffffff" strokeWidth="4.2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (type === 'added') {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 36 36"
|
||||
width={size}
|
||||
height={size}
|
||||
style={{ flexShrink: 0, marginRight: '14px', display: 'inline-block', verticalAlign: 'middle', ...style }}
|
||||
>
|
||||
<circle cx="18" cy="18" r="18" fill="#0084ff" />
|
||||
<path d="M18 9v18M9 18h18" stroke="#ffffff" strokeWidth="4.2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 36 36"
|
||||
width={size}
|
||||
height={size}
|
||||
style={{ flexShrink: 0, marginRight: '14px', display: 'inline-block', verticalAlign: 'middle', ...style }}
|
||||
>
|
||||
<circle cx="18" cy="18" r="18" fill="#38d9f5" />
|
||||
<rect x="15.8" y="8" width="4.4" height="12.5" rx="2.2" fill="#002d62" />
|
||||
<circle cx="18" cy="26" r="2.5" fill="#002d62" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
// Helper détection des modifications de parcours
|
||||
const detectRouteModification = (horaire, allPerturbs = [], isDeparts = false, normalizeName = (s) => s) => {
|
||||
if (!horaire) return { destModif: null, perturbStopsMap: new Map() };
|
||||
|
||||
const trainNo = String(horaire.numero_train || horaire.number || horaire.numero || '').trim();
|
||||
const horaireId = String(horaire.id || '').trim();
|
||||
|
||||
const matchingPerturbs = (allPerturbs || []).filter(p => {
|
||||
if (!p) return false;
|
||||
const pRef = String(p.horaire_id || p.horaire || p.horaireId || p.numero_train || p.train_number || p.numero || '').trim();
|
||||
const pNum = String(p.number || p.train_number || p.numero || p.numero_train || '').trim();
|
||||
if (horaireId && pRef === horaireId) return true;
|
||||
if (trainNo && (pRef === trainNo || pNum === trainNo)) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
const perturbStopsMap = new Map();
|
||||
let explicitNewDest = null;
|
||||
let explicitOldDest = null;
|
||||
let explicitNewOrigin = null;
|
||||
let explicitOldOrigin = null;
|
||||
|
||||
matchingPerturbs.forEach(p => {
|
||||
if (p.nouveau_terminus || p.nouvelle_destination || p.gare_arrivee_modifiee) {
|
||||
explicitNewDest = p.nouveau_terminus || p.nouvelle_destination || p.gare_arrivee_modifiee;
|
||||
}
|
||||
if (p.ancien_terminus || p.ancienne_destination || p.gare_arrivee_initiale) {
|
||||
explicitOldDest = p.ancien_terminus || p.ancienne_destination || p.gare_arrivee_initiale;
|
||||
}
|
||||
if (p.nouvelle_origine || p.nouvelle_gare_depart || p.gare_depart_modifiee) {
|
||||
explicitNewOrigin = p.nouvelle_origine || p.nouvelle_gare_depart || p.gare_depart_modifiee;
|
||||
}
|
||||
if (p.ancienne_origine || p.ancienne_gare_depart || p.gare_depart_initiale) {
|
||||
explicitOldOrigin = p.ancienne_origine || p.ancienne_gare_depart || p.gare_depart_initiale;
|
||||
}
|
||||
|
||||
const rawArrets = p.perturbations_arrets || p.arrets || p.arrets_perturbations;
|
||||
let arretsList = [];
|
||||
if (Array.isArray(rawArrets)) {
|
||||
arretsList = rawArrets;
|
||||
} else if (rawArrets && typeof rawArrets === 'object' && Array.isArray(rawArrets.arrets)) {
|
||||
arretsList = rawArrets.arrets;
|
||||
} else if (typeof rawArrets === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(rawArrets);
|
||||
arretsList = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.arrets) ? parsed.arrets : []);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
arretsList.forEach(a => {
|
||||
const aName = a.gare_nom || a.nom || a.name || a.station_name || (typeof a === 'string' ? a : '');
|
||||
if (!aName) return;
|
||||
const aType = String(a.type_perturbation || a.type || a.status || '').toLowerCase();
|
||||
const norm = normalizeName(aName);
|
||||
perturbStopsMap.set(norm, {
|
||||
name: aName,
|
||||
type: aType,
|
||||
isSupprime: aType.includes('suppr') || aType.includes('annul') || aType.includes('canc'),
|
||||
isAjoute: aType.includes('ajout') || aType.includes('add') || aType.includes('nouv'),
|
||||
isModifie: aType.includes('modif')
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (horaire.destination_initiale || horaire.gare_arrivee_initiale || horaire.ancienne_destination) {
|
||||
explicitOldDest = horaire.destination_initiale || horaire.gare_arrivee_initiale || horaire.ancienne_destination;
|
||||
}
|
||||
if (horaire.nouvelle_destination || horaire.nouveau_terminus || (horaire.destination_modifiee && horaire.gare_arrivee_nom)) {
|
||||
explicitNewDest = horaire.nouvelle_destination || horaire.nouveau_terminus || horaire.gare_arrivee_nom;
|
||||
}
|
||||
if (horaire.origine_initiale || horaire.gare_depart_initiale || horaire.ancienne_origine) {
|
||||
explicitOldOrigin = horaire.origine_initiale || horaire.gare_depart_initiale || horaire.ancienne_origine;
|
||||
}
|
||||
if (horaire.nouvelle_origine || (horaire.origine_modifiee && horaire.gare_depart_nom)) {
|
||||
explicitNewOrigin = horaire.nouvelle_origine || horaire.gare_depart_nom;
|
||||
}
|
||||
|
||||
let destModif = null;
|
||||
|
||||
if (isDeparts) {
|
||||
const currentDest = horaire.gare_arrivee_nom || horaire.destination || '';
|
||||
const normDest = normalizeName(currentDest);
|
||||
const destInPerturb = perturbStopsMap.get(normDest);
|
||||
|
||||
if (explicitNewDest && explicitOldDest && normalizeName(explicitNewDest) !== normalizeName(explicitOldDest)) {
|
||||
destModif = {
|
||||
newStation: explicitNewDest,
|
||||
oldStation: explicitOldDest,
|
||||
isAdded: Boolean(destInPerturb?.isAjoute || /ajout/.test(String(horaire.type_modification || '')))
|
||||
};
|
||||
} else if (destInPerturb?.isSupprime) {
|
||||
destModif = {
|
||||
newStation: explicitNewDest || 'Terminus modifié',
|
||||
oldStation: currentDest,
|
||||
isAdded: false
|
||||
};
|
||||
} else if (destInPerturb?.isAjoute) {
|
||||
destModif = {
|
||||
newStation: currentDest,
|
||||
oldStation: explicitOldDest || 'Destination modifiée',
|
||||
isAdded: true
|
||||
};
|
||||
} else if (explicitOldDest && normalizeName(explicitOldDest) !== normDest) {
|
||||
destModif = {
|
||||
newStation: currentDest,
|
||||
oldStation: explicitOldDest,
|
||||
isAdded: Boolean(horaire.destination_ajoutee)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const currentOrig = horaire.gare_depart_nom || horaire.origin || '';
|
||||
const normOrig = normalizeName(currentOrig);
|
||||
const origInPerturb = perturbStopsMap.get(normOrig);
|
||||
|
||||
if (explicitNewOrigin && explicitOldOrigin && normalizeName(explicitNewOrigin) !== normalizeName(explicitOldOrigin)) {
|
||||
destModif = {
|
||||
newStation: explicitNewOrigin,
|
||||
oldStation: explicitOldOrigin,
|
||||
isAdded: Boolean(origInPerturb?.isAjoute)
|
||||
};
|
||||
} else if (origInPerturb?.isSupprime) {
|
||||
destModif = {
|
||||
newStation: explicitNewOrigin || 'Origine modifiée',
|
||||
oldStation: currentOrig,
|
||||
isAdded: false
|
||||
};
|
||||
} else if (origInPerturb?.isAjoute) {
|
||||
destModif = {
|
||||
newStation: currentOrig,
|
||||
oldStation: explicitOldOrigin || 'Origine initiale',
|
||||
isAdded: true
|
||||
};
|
||||
} else if (explicitOldOrigin && normalizeName(explicitOldOrigin) !== normOrig) {
|
||||
destModif = {
|
||||
newStation: currentOrig,
|
||||
oldStation: explicitOldOrigin,
|
||||
isAdded: Boolean(horaire.origine_ajoutee)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { destModif, perturbStopsMap };
|
||||
};
|
||||
|
||||
const ViasMarquee = ({ viaNames }) => {
|
||||
const containerRef = useRef(null);
|
||||
const textRef = useRef(null);
|
||||
@@ -67,7 +277,6 @@ const ViasMarquee = ({ viaNames }) => {
|
||||
const tWidth = textRef.current.scrollWidth;
|
||||
if (tWidth > cWidth) {
|
||||
setIsOverflowing(true);
|
||||
// Vitesse de défilement rapide (~280px/s)
|
||||
setDuration(Math.max(1.0, tWidth / 280));
|
||||
} else {
|
||||
setIsOverflowing(false);
|
||||
@@ -98,18 +307,38 @@ const ViasMarquee = ({ viaNames }) => {
|
||||
animationDuration: isOverflowing ? `${duration}s` : undefined
|
||||
}}
|
||||
>
|
||||
{viaNames.map((v, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <span className="afl-arr-vias-chevron">></span>}
|
||||
<span className="afl-arr-vias-stop">{v}</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{viaNames.map((item, i) => {
|
||||
const name = typeof item === 'string' ? item : (item.name || item.gare_nom || '');
|
||||
const isSupprime = typeof item === 'object' && Boolean(item.isSupprime);
|
||||
const isAjoute = typeof item === 'object' && Boolean(item.isAjoute);
|
||||
const isModifie = typeof item === 'object' && Boolean(item.isModifie);
|
||||
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <span className="afl-arr-vias-chevron">></span>}
|
||||
<span
|
||||
className="afl-arr-vias-stop"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
...(isSupprime ? { textDecoration: 'line-through', opacity: 0.85 } : {})
|
||||
}}
|
||||
>
|
||||
{isSupprime && <AFLRouteBadge type="cancelled" size="26px" style={{ marginRight: '6px' }} />}
|
||||
{isAjoute && <AFLRouteBadge type="added" size="26px" style={{ marginRight: '6px' }} />}
|
||||
{isModifie && <AFLRouteBadge type="modified" size="26px" style={{ marginRight: '6px' }} />}
|
||||
<span>{name}</span>
|
||||
</span>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function Clock(){
|
||||
const [now, setNow] = useState(new Date());
|
||||
useEffect(() => {
|
||||
@@ -628,7 +857,12 @@ export default function AFLArrivees(){
|
||||
}
|
||||
|
||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null };
|
||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay };
|
||||
if (maxDelay != null) {
|
||||
const dH = Math.floor(maxDelay / 60);
|
||||
const dRem = maxDelay % 60;
|
||||
const dStr = maxDelay >= 60 ? (dRem > 0 ? `${dH}h${String(dRem).padStart(2, '0')}` : `${dH}h`) : `${maxDelay}min.`;
|
||||
return { label: `retardé +${dStr}`, detail: '', delayMinutes: maxDelay };
|
||||
}
|
||||
return { label: "à l'heure", detail: '', delayMinutes: null };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
@@ -1136,7 +1370,7 @@ export default function AFLArrivees(){
|
||||
const intervalId = setInterval(fetchData, intervalMs);
|
||||
|
||||
return () => { mounted = false; clearInterval(intervalId); };
|
||||
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]);
|
||||
}, [gareDisplay, gareParam, gareIdParam]);
|
||||
|
||||
// Helpers pour perturbations (memoized plus haut)
|
||||
|
||||
@@ -1248,16 +1482,7 @@ export default function AFLArrivees(){
|
||||
{/* HEADER */}
|
||||
<header className="afl-arr-header">
|
||||
<div className="afl-arr-header-left">
|
||||
<svg width="49px" height="49px" viewBox="0 0 28 28" fill="none" style={{ flexShrink: 0 }}>
|
||||
{/* Porte de train verticale */}
|
||||
<path d="M5 4v20M5 4h3M5 24h3" stroke="white" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
{/* Silhouette tête */}
|
||||
<circle cx="10.5" cy="7" r="2.2" fill="white" />
|
||||
{/* Silhouette corps + marche vers la droite */}
|
||||
<path d="M8.5 11h4l1.5 5.5-2 1-1-3.5-1 5.5-3-1 2-7.5z" fill="white" />
|
||||
{/* Chevrons >> */}
|
||||
<path d="M20 8l4 6-4 6M15 8l4 6-4 6" stroke="white" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<AFLDebarquementIcon size="49px" color="white" />
|
||||
<div className="afl-arr-header-title">Arrivées</div>
|
||||
</div>
|
||||
|
||||
@@ -1325,20 +1550,34 @@ export default function AFLArrivees(){
|
||||
const pillClass = isSuppressed ? 'afl-arr-pill--suppressed' : isDelayed ? 'afl-arr-pill--delayed' : 'afl-arr-pill--ontime';
|
||||
|
||||
let delayVal = '+ 5 min';
|
||||
const formatDelayVal = (m) => {
|
||||
if (m >= 60) {
|
||||
const h = Math.floor(m / 60);
|
||||
const rem = m % 60;
|
||||
return rem > 0 ? `+ ${h}h${String(rem).padStart(2, '0')}` : `+ ${h}h`;
|
||||
}
|
||||
return `+ ${m} min`;
|
||||
};
|
||||
if (typeof status?.delayMinutes === 'number' && status.delayMinutes > 0) {
|
||||
delayVal = `+ ${status.delayMinutes} min`;
|
||||
delayVal = formatDelayVal(status.delayMinutes);
|
||||
} else if (status?.delayMinutes) {
|
||||
delayVal = `+ ${status.delayMinutes} min`;
|
||||
const parsed = parseInt(status.delayMinutes, 10);
|
||||
delayVal = !isNaN(parsed) && parsed > 0 ? formatDelayVal(parsed) : `+ ${status.delayMinutes} min`;
|
||||
} else if (status?.label && status.label.includes('+')) {
|
||||
delayVal = status.label;
|
||||
const m = status.label.match(/\+(\d+)\s*(?:min)?/);
|
||||
if (m) delayVal = formatDelayVal(parseInt(m[1], 10));
|
||||
else delayVal = status.label;
|
||||
}
|
||||
|
||||
let delayDisplayText = alternateDelay ? 'retardé' : delayVal;
|
||||
let pillText = isSuppressed ? 'Supprimé' : isDelayed ? delayDisplayText : 'À l\'heure';
|
||||
|
||||
// Calcul des vias (gares entre l'origine et la gare d'arrivée)
|
||||
// Détection des modifications de parcours (Origine modifiée)
|
||||
const { destModif: originModif, perturbStopsMap } = detectRouteModification(horaire, perturbations, false, normalizeName);
|
||||
|
||||
// Calcul des gares de provenance (gares desservies avant la gare affichée dans l'ordre décroissant)
|
||||
const stopsList = getStops(horaire);
|
||||
const viaNames = [];
|
||||
const rawPrevStops = [];
|
||||
if (Array.isArray(stopsList)) {
|
||||
for (const s of stopsList) {
|
||||
const sName = s.gare_nom || s.nom || s.name || s.station_name || '';
|
||||
@@ -1346,9 +1585,26 @@ export default function AFLArrivees(){
|
||||
const norm = normalizeName(sName);
|
||||
if (norm === normalizeName(origin || '')) continue;
|
||||
if (norm === normalizeName(stationName || '')) break;
|
||||
viaNames.push(sName);
|
||||
rawPrevStops.push(s);
|
||||
}
|
||||
}
|
||||
// Ordre décroissant : la gare la plus proche avant l'arrivée en premier
|
||||
rawPrevStops.reverse();
|
||||
|
||||
const viaItems = rawPrevStops.map(s => {
|
||||
const sName = s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '');
|
||||
const norm = normalizeName(sName);
|
||||
const pInfo = perturbStopsMap.get(norm);
|
||||
const isSupprime = Boolean(pInfo?.isSupprime || s.is_supprime || s.supprime || s.type === 'arret_supprime');
|
||||
const isAjoute = Boolean(pInfo?.isAjoute || s.is_ajoute || s.ajoute || s.type === 'arret_ajoute');
|
||||
const isModifie = Boolean(pInfo?.isModifie || s.is_modifie || s.modifie);
|
||||
return {
|
||||
name: sName,
|
||||
isSupprime,
|
||||
isAjoute,
|
||||
isModifie
|
||||
};
|
||||
});
|
||||
|
||||
const renderPillIcon = () => {
|
||||
if (isSuppressed) {
|
||||
@@ -1431,16 +1687,44 @@ export default function AFLArrivees(){
|
||||
<>
|
||||
<div className="afl-arr-top-row">
|
||||
{isCarTER && <BusIcon size="52px" color="white" style={{ marginRight: '15px' }} />}
|
||||
<MarqueeText text={origin} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||
{originModif ? (
|
||||
alternateDelay ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||
<AFLRouteBadge type={originModif.isAdded ? 'added' : 'modified'} size="52px" />
|
||||
<MarqueeText text={originModif.newStation} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||
<AFLRouteBadge type="cancelled" size="52px" />
|
||||
<MarqueeText text={originModif.oldStation} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<MarqueeText text={origin} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||
)}
|
||||
</div>
|
||||
<div className="afl-arr-bottom-row">
|
||||
<ViasMarquee viaNames={viaNames} />
|
||||
<ViasMarquee viaNames={viaItems} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||
{isCarTER && <BusIcon size="49px" color="white" style={{ marginRight: '15px' }} />}
|
||||
<MarqueeText text={origin} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||
{originModif ? (
|
||||
alternateDelay ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||
<AFLRouteBadge type={originModif.isAdded ? 'added' : 'modified'} size="46px" />
|
||||
<MarqueeText text={originModif.newStation} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||
<AFLRouteBadge type="cancelled" size="46px" />
|
||||
<MarqueeText text={originModif.oldStation} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<MarqueeText text={origin} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import './AFLDeparts.css';
|
||||
import supabase from '../../../lib/supabase';
|
||||
@@ -54,6 +55,215 @@ const BusIcon = ({ size = '59px', color = 'white', style = {} }) => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Icône Embarquement (Départs AFL)
|
||||
const AFLEmbarquementIcon = ({ size = '49px', color = 'white', style = {} }) => (
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
width={size}
|
||||
height={size}
|
||||
fill="none"
|
||||
style={{ flexShrink: 0, ...style }}
|
||||
>
|
||||
{/* Voiture de train / Porte à droite */}
|
||||
<path d="M22 4v24M22 4h6M22 28h6" stroke={color} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M19 25h3" stroke={color} strokeWidth="2.5" strokeLinecap="round" />
|
||||
<path d="M16 28h3" stroke={color} strokeWidth="2.5" strokeLinecap="round" />
|
||||
{/* Silhouette du passager montant dans le train (tourné vers la droite) */}
|
||||
<circle cx="11.5" cy="8.5" r="2.5" fill={color} />
|
||||
<path
|
||||
d="M10 13h4l2 6-2.5 1-1-3.5-1.5 5.5-3-1 2-8z"
|
||||
fill={color}
|
||||
/>
|
||||
<path d="M14 15l4-1.5" stroke={color} strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Pastilles de modification de parcours
|
||||
const AFLRouteBadge = ({ type = 'modified', size = '52px', style = {} }) => {
|
||||
if (type === 'cancelled' || type === 'suppressed') {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 36 36"
|
||||
width={size}
|
||||
height={size}
|
||||
style={{ flexShrink: 0, marginRight: '14px', display: 'inline-block', verticalAlign: 'middle', ...style }}
|
||||
>
|
||||
<circle cx="18" cy="18" r="18" fill="#c8102e" />
|
||||
<path d="M11 11L25 25M25 11L11 25" stroke="#ffffff" strokeWidth="4.2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (type === 'added') {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 36 36"
|
||||
width={size}
|
||||
height={size}
|
||||
style={{ flexShrink: 0, marginRight: '14px', display: 'inline-block', verticalAlign: 'middle', ...style }}
|
||||
>
|
||||
<circle cx="18" cy="18" r="18" fill="#0084ff" />
|
||||
<path d="M18 9v18M9 18h18" stroke="#ffffff" strokeWidth="4.2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 36 36"
|
||||
width={size}
|
||||
height={size}
|
||||
style={{ flexShrink: 0, marginRight: '14px', display: 'inline-block', verticalAlign: 'middle', ...style }}
|
||||
>
|
||||
<circle cx="18" cy="18" r="18" fill="#38d9f5" />
|
||||
<rect x="15.8" y="8" width="4.4" height="12.5" rx="2.2" fill="#002d62" />
|
||||
<circle cx="18" cy="26" r="2.5" fill="#002d62" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
// Helper détection des modifications de parcours
|
||||
const detectRouteModification = (horaire, allPerturbs = [], isDeparts = true, normalizeName = (s) => s) => {
|
||||
if (!horaire) return { destModif: null, perturbStopsMap: new Map() };
|
||||
|
||||
const trainNo = String(horaire.numero_train || horaire.number || horaire.numero || '').trim();
|
||||
const horaireId = String(horaire.id || '').trim();
|
||||
|
||||
const matchingPerturbs = (allPerturbs || []).filter(p => {
|
||||
if (!p) return false;
|
||||
const pRef = String(p.horaire_id || p.horaire || p.horaireId || p.numero_train || p.train_number || p.numero || '').trim();
|
||||
const pNum = String(p.number || p.train_number || p.numero || p.numero_train || '').trim();
|
||||
if (horaireId && pRef === horaireId) return true;
|
||||
if (trainNo && (pRef === trainNo || pNum === trainNo)) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
const perturbStopsMap = new Map();
|
||||
let explicitNewDest = null;
|
||||
let explicitOldDest = null;
|
||||
let explicitNewOrigin = null;
|
||||
let explicitOldOrigin = null;
|
||||
|
||||
matchingPerturbs.forEach(p => {
|
||||
if (p.nouveau_terminus || p.nouvelle_destination || p.gare_arrivee_modifiee) {
|
||||
explicitNewDest = p.nouveau_terminus || p.nouvelle_destination || p.gare_arrivee_modifiee;
|
||||
}
|
||||
if (p.ancien_terminus || p.ancienne_destination || p.gare_arrivee_initiale) {
|
||||
explicitOldDest = p.ancien_terminus || p.ancienne_destination || p.gare_arrivee_initiale;
|
||||
}
|
||||
if (p.nouvelle_origine || p.nouvelle_gare_depart || p.gare_depart_modifiee) {
|
||||
explicitNewOrigin = p.nouvelle_origine || p.nouvelle_gare_depart || p.gare_depart_modifiee;
|
||||
}
|
||||
if (p.ancienne_origine || p.ancienne_gare_depart || p.gare_depart_initiale) {
|
||||
explicitOldOrigin = p.ancienne_origine || p.ancienne_gare_depart || p.gare_depart_initiale;
|
||||
}
|
||||
|
||||
const rawArrets = p.perturbations_arrets || p.arrets || p.arrets_perturbations;
|
||||
let arretsList = [];
|
||||
if (Array.isArray(rawArrets)) {
|
||||
arretsList = rawArrets;
|
||||
} else if (rawArrets && typeof rawArrets === 'object' && Array.isArray(rawArrets.arrets)) {
|
||||
arretsList = rawArrets.arrets;
|
||||
} else if (typeof rawArrets === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(rawArrets);
|
||||
arretsList = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.arrets) ? parsed.arrets : []);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
arretsList.forEach(a => {
|
||||
const aName = a.gare_nom || a.nom || a.name || a.station_name || (typeof a === 'string' ? a : '');
|
||||
if (!aName) return;
|
||||
const aType = String(a.type_perturbation || a.type || a.status || '').toLowerCase();
|
||||
const norm = normalizeName(aName);
|
||||
perturbStopsMap.set(norm, {
|
||||
name: aName,
|
||||
type: aType,
|
||||
isSupprime: aType.includes('suppr') || aType.includes('annul') || aType.includes('canc'),
|
||||
isAjoute: aType.includes('ajout') || aType.includes('add') || aType.includes('nouv'),
|
||||
isModifie: aType.includes('modif')
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (horaire.destination_initiale || horaire.gare_arrivee_initiale || horaire.ancienne_destination) {
|
||||
explicitOldDest = horaire.destination_initiale || horaire.gare_arrivee_initiale || horaire.ancienne_destination;
|
||||
}
|
||||
if (horaire.nouvelle_destination || horaire.nouveau_terminus || (horaire.destination_modifiee && horaire.gare_arrivee_nom)) {
|
||||
explicitNewDest = horaire.nouvelle_destination || horaire.nouveau_terminus || horaire.gare_arrivee_nom;
|
||||
}
|
||||
if (horaire.origine_initiale || horaire.gare_depart_initiale || horaire.ancienne_origine) {
|
||||
explicitOldOrigin = horaire.origine_initiale || horaire.gare_depart_initiale || horaire.ancienne_origine;
|
||||
}
|
||||
if (horaire.nouvelle_origine || (horaire.origine_modifiee && horaire.gare_depart_nom)) {
|
||||
explicitNewOrigin = horaire.nouvelle_origine || horaire.gare_depart_nom;
|
||||
}
|
||||
|
||||
let destModif = null;
|
||||
|
||||
if (isDeparts) {
|
||||
const currentDest = horaire.gare_arrivee_nom || horaire.destination || '';
|
||||
const normDest = normalizeName(currentDest);
|
||||
const destInPerturb = perturbStopsMap.get(normDest);
|
||||
|
||||
if (explicitNewDest && explicitOldDest && normalizeName(explicitNewDest) !== normalizeName(explicitOldDest)) {
|
||||
destModif = {
|
||||
newStation: explicitNewDest,
|
||||
oldStation: explicitOldDest,
|
||||
isAdded: Boolean(destInPerturb?.isAjoute || /ajout/.test(String(horaire.type_modification || '')))
|
||||
};
|
||||
} else if (destInPerturb?.isSupprime) {
|
||||
destModif = {
|
||||
newStation: explicitNewDest || 'Terminus modifié',
|
||||
oldStation: currentDest,
|
||||
isAdded: false
|
||||
};
|
||||
} else if (destInPerturb?.isAjoute) {
|
||||
destModif = {
|
||||
newStation: currentDest,
|
||||
oldStation: explicitOldDest || 'Destination modifiée',
|
||||
isAdded: true
|
||||
};
|
||||
} else if (explicitOldDest && normalizeName(explicitOldDest) !== normDest) {
|
||||
destModif = {
|
||||
newStation: currentDest,
|
||||
oldStation: explicitOldDest,
|
||||
isAdded: Boolean(horaire.destination_ajoutee)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const currentOrig = horaire.gare_depart_nom || horaire.origin || '';
|
||||
const normOrig = normalizeName(currentOrig);
|
||||
const origInPerturb = perturbStopsMap.get(normOrig);
|
||||
|
||||
if (explicitNewOrigin && explicitOldOrigin && normalizeName(explicitNewOrigin) !== normalizeName(explicitOldOrigin)) {
|
||||
destModif = {
|
||||
newStation: explicitNewOrigin,
|
||||
oldStation: explicitOldOrigin,
|
||||
isAdded: Boolean(origInPerturb?.isAjoute)
|
||||
};
|
||||
} else if (origInPerturb?.isSupprime) {
|
||||
destModif = {
|
||||
newStation: explicitNewOrigin || 'Origine modifiée',
|
||||
oldStation: currentOrig,
|
||||
isAdded: false
|
||||
};
|
||||
} else if (origInPerturb?.isAjoute) {
|
||||
destModif = {
|
||||
newStation: currentOrig,
|
||||
oldStation: explicitOldOrigin || 'Origine initiale',
|
||||
isAdded: true
|
||||
};
|
||||
} else if (explicitOldOrigin && normalizeName(explicitOldOrigin) !== normOrig) {
|
||||
destModif = {
|
||||
newStation: currentOrig,
|
||||
oldStation: explicitOldOrigin,
|
||||
isAdded: Boolean(horaire.origine_ajoutee)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { destModif, perturbStopsMap };
|
||||
};
|
||||
|
||||
const ViasMarquee = ({ viaNames }) => {
|
||||
const containerRef = useRef(null);
|
||||
const textRef = useRef(null);
|
||||
@@ -67,7 +277,6 @@ const ViasMarquee = ({ viaNames }) => {
|
||||
const tWidth = textRef.current.scrollWidth;
|
||||
if (tWidth > cWidth) {
|
||||
setIsOverflowing(true);
|
||||
// Vitesse de défilement multipliée par 2 (~280px/s)
|
||||
setDuration(Math.max(1.0, tWidth / 280));
|
||||
} else {
|
||||
setIsOverflowing(false);
|
||||
@@ -98,18 +307,38 @@ const ViasMarquee = ({ viaNames }) => {
|
||||
animationDuration: isOverflowing ? `${duration}s` : undefined
|
||||
}}
|
||||
>
|
||||
{viaNames.map((v, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <span className="afl-vias-chevron">></span>}
|
||||
<span className="afl-vias-stop">{v}</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{viaNames.map((item, i) => {
|
||||
const name = typeof item === 'string' ? item : (item.name || item.gare_nom || '');
|
||||
const isSupprime = typeof item === 'object' && Boolean(item.isSupprime);
|
||||
const isAjoute = typeof item === 'object' && Boolean(item.isAjoute);
|
||||
const isModifie = typeof item === 'object' && Boolean(item.isModifie);
|
||||
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <span className="afl-vias-chevron">></span>}
|
||||
<span
|
||||
className="afl-vias-stop"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
...(isSupprime ? { textDecoration: 'line-through', opacity: 0.85 } : {})
|
||||
}}
|
||||
>
|
||||
{isSupprime && <AFLRouteBadge type="cancelled" size="26px" style={{ marginRight: '6px' }} />}
|
||||
{isAjoute && <AFLRouteBadge type="added" size="26px" style={{ marginRight: '6px' }} />}
|
||||
{isModifie && <AFLRouteBadge type="modified" size="26px" style={{ marginRight: '6px' }} />}
|
||||
<span>{name}</span>
|
||||
</span>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// (La fonction TrainType a été retirée car la colonne affiche désormais directement les
|
||||
// informations via .train-meta dans la colonne .col-train-type)
|
||||
|
||||
@@ -636,7 +865,12 @@ export default function AFLDeparts(){
|
||||
}
|
||||
|
||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null };
|
||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay };
|
||||
if (maxDelay != null) {
|
||||
const dH = Math.floor(maxDelay / 60);
|
||||
const dRem = maxDelay % 60;
|
||||
const dStr = maxDelay >= 60 ? (dRem > 0 ? `${dH}h${String(dRem).padStart(2, '0')}` : `${dH}h`) : `${maxDelay}min.`;
|
||||
return { label: `retardé +${dStr}`, detail: '', delayMinutes: maxDelay };
|
||||
}
|
||||
return { label: "à l'heure", detail: '', delayMinutes: null };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
@@ -1163,7 +1397,7 @@ export default function AFLDeparts(){
|
||||
const intervalId = setInterval(fetchData, intervalMs);
|
||||
|
||||
return () => { mounted = false; clearInterval(intervalId); };
|
||||
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]);
|
||||
}, [gareDisplay, gareParam, gareIdParam]);
|
||||
|
||||
// Helpers pour perturbations (memoized plus haut)
|
||||
|
||||
@@ -1275,16 +1509,7 @@ export default function AFLDeparts(){
|
||||
{/* HEADER */}
|
||||
<header className="afl-header">
|
||||
<div className="afl-header-left">
|
||||
<svg width="49px" height="49px" viewBox="0 0 28 28" fill="none" style={{ flexShrink: 0 }}>
|
||||
{/* Chevrons >> */}
|
||||
<path d="M3 8l4 6-4 6M8 8l4 6-4 6" stroke="white" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
{/* Silhouette tête */}
|
||||
<circle cx="17.5" cy="7" r="2.2" fill="white" />
|
||||
{/* Silhouette corps + marche */}
|
||||
<path d="M15.5 11h4l1.5 5.5-2 1-1-3.5-1 5.5-3-1 2-7.5z" fill="white" />
|
||||
{/* Porte de train verticale */}
|
||||
<path d="M23 4v20M23 4h3M23 24h3" stroke="white" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<AFLEmbarquementIcon size="49px" color="white" />
|
||||
<div className="afl-header-title">Départs</div>
|
||||
</div>
|
||||
|
||||
@@ -1352,20 +1577,34 @@ export default function AFLDeparts(){
|
||||
const pillClass = isSuppressed ? 'afl-pill--suppressed' : isDelayed ? 'afl-pill--delayed' : 'afl-pill--ontime';
|
||||
|
||||
let delayVal = '+ 5 min';
|
||||
const formatDelayVal = (m) => {
|
||||
if (m >= 60) {
|
||||
const h = Math.floor(m / 60);
|
||||
const rem = m % 60;
|
||||
return rem > 0 ? `+ ${h}h${String(rem).padStart(2, '0')}` : `+ ${h}h`;
|
||||
}
|
||||
return `+ ${m} min`;
|
||||
};
|
||||
if (typeof status?.delayMinutes === 'number' && status.delayMinutes > 0) {
|
||||
delayVal = `+ ${status.delayMinutes} min`;
|
||||
delayVal = formatDelayVal(status.delayMinutes);
|
||||
} else if (status?.delayMinutes) {
|
||||
delayVal = `+ ${status.delayMinutes} min`;
|
||||
const parsed = parseInt(status.delayMinutes, 10);
|
||||
delayVal = !isNaN(parsed) && parsed > 0 ? formatDelayVal(parsed) : `+ ${status.delayMinutes} min`;
|
||||
} else if (status?.label && status.label.includes('+')) {
|
||||
delayVal = status.label;
|
||||
const m = status.label.match(/\+(\d+)\s*(?:min)?/);
|
||||
if (m) delayVal = formatDelayVal(parseInt(m[1], 10));
|
||||
else delayVal = status.label;
|
||||
}
|
||||
|
||||
let delayDisplayText = alternateDelay ? 'retardé' : delayVal;
|
||||
let pillText = isSuppressed ? 'Supprimé' : isDelayed ? delayDisplayText : 'À l\'heure';
|
||||
|
||||
// Détection des modifications de parcours
|
||||
const { destModif, perturbStopsMap } = detectRouteModification(horaire, perturbations, true, normalizeName);
|
||||
|
||||
// Calcul des vias
|
||||
const stopsList = getStops(horaire);
|
||||
const viaNames = [];
|
||||
const rawViaStops = [];
|
||||
if (Array.isArray(stopsList) && stopsList.length > 0) {
|
||||
let isAfterStart = false;
|
||||
const hasStartInStops = stopsList.some(s => {
|
||||
@@ -1388,11 +1627,26 @@ export default function AFLDeparts(){
|
||||
if (norm === normalizeName(destination || '')) {
|
||||
break;
|
||||
}
|
||||
viaNames.push(sName);
|
||||
rawViaStops.push(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const viaItems = rawViaStops.map(s => {
|
||||
const sName = s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '');
|
||||
const norm = normalizeName(sName);
|
||||
const pInfo = perturbStopsMap.get(norm);
|
||||
const isSupprime = Boolean(pInfo?.isSupprime || s.is_supprime || s.supprime || s.type === 'arret_supprime');
|
||||
const isAjoute = Boolean(pInfo?.isAjoute || s.is_ajoute || s.ajoute || s.type === 'arret_ajoute');
|
||||
const isModifie = Boolean(pInfo?.isModifie || s.is_modifie || s.modifie);
|
||||
return {
|
||||
name: sName,
|
||||
isSupprime,
|
||||
isAjoute,
|
||||
isModifie
|
||||
};
|
||||
});
|
||||
|
||||
const renderPillIcon = () => {
|
||||
if (isSuppressed) {
|
||||
return (
|
||||
@@ -1474,16 +1728,44 @@ export default function AFLDeparts(){
|
||||
<>
|
||||
<div className="afl-top-row">
|
||||
{isCarTER && <BusIcon size="52px" color="white" style={{ marginRight: '15px' }} />}
|
||||
<MarqueeText text={destination} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||
{destModif ? (
|
||||
alternateDelay ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||
<AFLRouteBadge type={destModif.isAdded ? 'added' : 'modified'} size="52px" />
|
||||
<MarqueeText text={destModif.newStation} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||
<AFLRouteBadge type="cancelled" size="52px" />
|
||||
<MarqueeText text={destModif.oldStation} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<MarqueeText text={destination} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||
)}
|
||||
</div>
|
||||
<div className="afl-bottom-row">
|
||||
<ViasMarquee viaNames={viaNames} />
|
||||
<ViasMarquee viaNames={viaItems} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||
{isCarTER && <BusIcon size="49px" color="white" style={{ marginRight: '15px' }} />}
|
||||
<MarqueeText text={destination} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||
{destModif ? (
|
||||
alternateDelay ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||
<AFLRouteBadge type={destModif.isAdded ? 'added' : 'modified'} size="46px" />
|
||||
<MarqueeText text={destModif.newStation} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||
<AFLRouteBadge type="cancelled" size="46px" />
|
||||
<MarqueeText text={destModif.oldStation} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<MarqueeText text={destination} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -335,6 +335,7 @@
|
||||
.aff-classiques-arrivees__col-platform {
|
||||
width: 96px;
|
||||
min-width: 96px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -433,4 +434,5 @@
|
||||
color: var(--yellow) !important;
|
||||
margin-top: 1px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import './ClassiquesArrivees.css';
|
||||
import supabase from '../../../lib/supabase';
|
||||
@@ -629,7 +630,12 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
}
|
||||
|
||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null, causeDetails };
|
||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay, causeDetails };
|
||||
if (maxDelay != null) {
|
||||
const dH = Math.floor(maxDelay / 60);
|
||||
const dRem = maxDelay % 60;
|
||||
const dStr = maxDelay >= 60 ? (dRem > 0 ? `${dH}h${String(dRem).padStart(2, '0')}` : `${dH}h`) : `${maxDelay}min.`;
|
||||
return { label: `retardé +${dStr}`, detail: '', delayMinutes: maxDelay, causeDetails };
|
||||
}
|
||||
return { label: "à l'heure", detail: '', delayMinutes: null, causeDetails };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
@@ -695,12 +701,17 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
let inFlight = false; // empêche les fetchs concurrents
|
||||
let pendingRefresh = false;
|
||||
const source = getSourceFromPath(window.location.pathname);
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!mounted) return;
|
||||
if (inFlight) return;
|
||||
if (inFlight) {
|
||||
pendingRefresh = true;
|
||||
return;
|
||||
}
|
||||
inFlight = true;
|
||||
pendingRefresh = false;
|
||||
|
||||
if (source === 'sncf') {
|
||||
setLoading(true);
|
||||
@@ -741,6 +752,10 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
} finally {
|
||||
if (mounted) setLoading(false);
|
||||
inFlight = false;
|
||||
if (pendingRefresh && mounted) {
|
||||
pendingRefresh = false;
|
||||
fetchData();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1169,8 +1184,27 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
const intervalMs = 15000; // 15 secondes par défaut
|
||||
const intervalId = setInterval(fetchData, intervalMs);
|
||||
|
||||
return () => { mounted = false; clearInterval(intervalId); };
|
||||
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
|
||||
let rtDebounce = null;
|
||||
const onRtUpdate = () => {
|
||||
if (!mounted) return;
|
||||
clearTimeout(rtDebounce);
|
||||
rtDebounce = setTimeout(() => {
|
||||
if (mounted) fetchData();
|
||||
}, 2000);
|
||||
};
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('sncf-realtime-updated', onRtUpdate);
|
||||
}
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
clearInterval(intervalId);
|
||||
clearTimeout(rtDebounce);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('sncf-realtime-updated', onRtUpdate);
|
||||
}
|
||||
};
|
||||
}, [gareDisplay, gareParam, gareIdParam, defilementMode]);
|
||||
|
||||
// Helpers pour perturbations (memoized plus haut)
|
||||
|
||||
@@ -1253,7 +1287,7 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
}
|
||||
|
||||
const horaire = h || {};
|
||||
const { logoSrc, typeLabel, numTrain } = getTrainLogoAndMeta(horaire);
|
||||
const { logoSrc, typeLabel, numTrain, isCar: metaIsCar, isSubstitution: metaIsSubstitution, brand: metaBrand, brandClass } = getTrainLogoAndMeta(horaire, gareDisplay || gareParam);
|
||||
const time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.arrival_time || horaire.time || horaire.heure || '');
|
||||
const origin = horaire.gare_depart_nom || horaire.gare_depart || horaire.origine || '';
|
||||
const formattedOrigin = formatStationName(origin);
|
||||
@@ -1274,7 +1308,8 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
||||
const isSuppressed = status && status.label === 'supprimé';
|
||||
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
|
||||
const isCar = (
|
||||
const isSub = metaIsSubstitution || Boolean(horaire.is_substitution);
|
||||
const isCar = metaIsCar || (
|
||||
(horaire.type_train && (horaire.type_train.toUpperCase().includes('AUTOCAR') || horaire.type_train.toUpperCase().includes('CAR TER') || horaire.type_train.toUpperCase() === 'BUS')) ||
|
||||
(horaire.op && (horaire.op.toUpperCase().includes('AUTOCAR') || horaire.op.toUpperCase().includes('CAR TER') || horaire.op.toUpperCase() === 'BUS')) ||
|
||||
(horaire.type && (horaire.type.toUpperCase().includes('AUTOCAR') || horaire.type.toUpperCase().includes('CAR TER') || horaire.type.toUpperCase() === 'BUS')) ||
|
||||
@@ -1334,7 +1369,7 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-arrivees__col-destination">
|
||||
<MarqueeText text={formattedOrigin} className="aff-classiques-arrivees__destination" />
|
||||
<MarqueeText text={isSub ? `[CAR DE REMPLACEMENT] ${formattedOrigin}` : formattedOrigin} className="aff-classiques-arrivees__destination" />
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-arrivees__col-platform">
|
||||
@@ -1358,7 +1393,12 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
speed={50}
|
||||
text={
|
||||
<span className="aff-classiques-arrivees__stops-chain">
|
||||
{stopsList.map((stopName, sIdx) => (
|
||||
{isSub && (
|
||||
<span className="aff-classiques-arrivees__stop-item" style={{ color: '#ffd200', fontWeight: 'bold' }}>
|
||||
CAR DE REMPLACEMENT
|
||||
<span className="aff-classiques-arrivees__stop-bullet">•</span>
|
||||
</span>
|
||||
)}{stopsList.map((stopName, sIdx) => (
|
||||
<span key={sIdx} className="aff-classiques-arrivees__stop-item">
|
||||
{sIdx > 0 && <span className="aff-classiques-arrivees__stop-bullet">•</span>}
|
||||
<span className="aff-classiques-arrivees__stop-name">{stopName}</span>
|
||||
|
||||
@@ -335,6 +335,7 @@
|
||||
.aff-classiques-departs__col-platform {
|
||||
width: 96px;
|
||||
min-width: 96px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -433,4 +434,5 @@
|
||||
color: var(--yellow) !important;
|
||||
margin-top: 1px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import './ClassiquesDeparts.css';
|
||||
import supabase from '../../../lib/supabase';
|
||||
@@ -629,7 +630,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
}
|
||||
|
||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null, causeDetails };
|
||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay, causeDetails };
|
||||
if (maxDelay != null) {
|
||||
const dH = Math.floor(maxDelay / 60);
|
||||
const dRem = maxDelay % 60;
|
||||
const dStr = maxDelay >= 60 ? (dRem > 0 ? `${dH}h${String(dRem).padStart(2, '0')}` : `${dH}h`) : `${maxDelay}min.`;
|
||||
return { label: `retardé +${dStr}`, detail: '', delayMinutes: maxDelay, causeDetails };
|
||||
}
|
||||
return { label: "à l'heure", detail: '', delayMinutes: null, causeDetails };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
@@ -695,12 +701,17 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
let inFlight = false; // empêche les fetchs concurrents
|
||||
let pendingRefresh = false;
|
||||
const source = getSourceFromPath(window.location.pathname);
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!mounted) return;
|
||||
if (inFlight) return;
|
||||
if (inFlight) {
|
||||
pendingRefresh = true;
|
||||
return;
|
||||
}
|
||||
inFlight = true;
|
||||
pendingRefresh = false;
|
||||
|
||||
if (source === 'sncf') {
|
||||
setLoading(true);
|
||||
@@ -740,6 +751,10 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
} finally {
|
||||
if (mounted) setLoading(false);
|
||||
inFlight = false;
|
||||
if (pendingRefresh && mounted) {
|
||||
pendingRefresh = false;
|
||||
fetchData();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1168,8 +1183,27 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
const intervalMs = 15000; // 15 secondes par défaut
|
||||
const intervalId = setInterval(fetchData, intervalMs);
|
||||
|
||||
return () => { mounted = false; clearInterval(intervalId); };
|
||||
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
|
||||
let rtDebounce = null;
|
||||
const onRtUpdate = () => {
|
||||
if (!mounted) return;
|
||||
clearTimeout(rtDebounce);
|
||||
rtDebounce = setTimeout(() => {
|
||||
if (mounted) fetchData();
|
||||
}, 2000);
|
||||
};
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('sncf-realtime-updated', onRtUpdate);
|
||||
}
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
clearInterval(intervalId);
|
||||
clearTimeout(rtDebounce);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('sncf-realtime-updated', onRtUpdate);
|
||||
}
|
||||
};
|
||||
}, [gareDisplay, gareParam, gareIdParam, defilementMode]);
|
||||
|
||||
// Helpers pour perturbations (memoized plus haut)
|
||||
|
||||
@@ -1252,7 +1286,7 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
}
|
||||
|
||||
const horaire = h || {};
|
||||
const { logoSrc, typeLabel, numTrain } = getTrainLogoAndMeta(horaire);
|
||||
const { logoSrc, typeLabel, numTrain, isCar: metaIsCar, isSubstitution: metaIsSubstitution, brand: metaBrand, brandClass } = getTrainLogoAndMeta(horaire, gareDisplay || gareParam);
|
||||
const time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.departure_time || horaire.time || horaire.heure || '');
|
||||
const destination = horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination || '';
|
||||
const formattedDestination = formatStationName(destination);
|
||||
@@ -1273,7 +1307,7 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
||||
const isSuppressed = status && status.label === 'supprimé';
|
||||
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
|
||||
const isCar = (
|
||||
const isCar = metaIsCar || (
|
||||
(horaire.type_train && (horaire.type_train.toUpperCase().includes('AUTOCAR') || horaire.type_train.toUpperCase().includes('CAR TER') || horaire.type_train.toUpperCase() === 'BUS')) ||
|
||||
(horaire.op && (horaire.op.toUpperCase().includes('AUTOCAR') || horaire.op.toUpperCase().includes('CAR TER') || horaire.op.toUpperCase() === 'BUS')) ||
|
||||
(horaire.type && (horaire.type.toUpperCase().includes('AUTOCAR') || horaire.type.toUpperCase().includes('CAR TER') || horaire.type.toUpperCase() === 'BUS')) ||
|
||||
@@ -1287,6 +1321,7 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
cleanPlatform === 'CAR' ||
|
||||
cleanPlatform === 'BUS'
|
||||
);
|
||||
const isSub = metaIsSubstitution || Boolean(horaire.is_substitution);
|
||||
const showPlatform = !isSuppressed && (isCar || Boolean(cleanPlatform));
|
||||
|
||||
const stopsList = isExtended ? getStopsListForDeparts(horaire, gareDisplay || gareParam, normalizeName) : [];
|
||||
@@ -1333,7 +1368,7 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-departs__col-destination">
|
||||
<MarqueeText text={formattedDestination} className="aff-classiques-departs__destination" />
|
||||
<MarqueeText text={isSub ? `[CAR DE REMPLACEMENT] ${formattedDestination}` : formattedDestination} className="aff-classiques-departs__destination" />
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-departs__col-platform">
|
||||
@@ -1357,7 +1392,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
speed={50}
|
||||
text={
|
||||
<span className="aff-classiques-departs__stops-chain">
|
||||
{stopsList.map((stopName, sIdx) => (
|
||||
{isSub && (
|
||||
<span className="aff-classiques-departs__stop-item" style={{ color: '#ffd200', fontWeight: 'bold' }}>
|
||||
CAR DE REMPLACEMENT
|
||||
<span className="aff-classiques-departs__stop-bullet">•</span>
|
||||
</span>
|
||||
)}{stopsList.map((stopName, sIdx) => (
|
||||
<span key={sIdx} className="aff-classiques-departs__stop-item">
|
||||
{sIdx > 0 && <span className="aff-classiques-departs__stop-bullet">•</span>}
|
||||
<span className="aff-classiques-departs__stop-name">{stopName}</span>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './ClassiquesInformations.css';
|
||||
import supabase from '../../../lib/supabase';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import './ClassiquesArriveesQuais.css';
|
||||
import supabase from '../../../../lib/supabase';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import './ClassiquesDepartsQuais.css';
|
||||
import supabase from '../../../../lib/supabase';
|
||||
|
||||
@@ -185,13 +185,25 @@
|
||||
color: #1a2b4c !important;
|
||||
}
|
||||
|
||||
.eva-arr-status-pill--approaching {
|
||||
background-color: #d7e1f0 !important;
|
||||
border: 1.8px solid #7d95b5 !important;
|
||||
color: #1a2b4c !important;
|
||||
}
|
||||
|
||||
.eva-arr-status-pill--atplatform {
|
||||
background-color: #1a2b4c !important;
|
||||
border: none !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.eva-arr-status-pill--delayed {
|
||||
background-color: #ffa74f !important;
|
||||
color: #000c2f !important;
|
||||
}
|
||||
|
||||
.eva-arr-status-pill--suppressed {
|
||||
background-color: #ff5c73 !important;
|
||||
background-color: #ff4d6d !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
@@ -242,20 +254,28 @@
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
/* Badge Motif de Perturbation (ex: Embouteillages) */
|
||||
/* Badge Motif de Perturbation */
|
||||
.eva-arr-perturb-badge {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 8px !important;
|
||||
background-color: #ffa74f !important;
|
||||
color: #000c2f !important;
|
||||
padding: 4px 15px !important;
|
||||
border-radius: 6px !important;
|
||||
height: 37px !important;
|
||||
height: 38px !important;
|
||||
box-sizing: border-box !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
.eva-arr-perturb-badge--delayed {
|
||||
background-color: #ffa74f !important;
|
||||
color: #000c2f !important;
|
||||
}
|
||||
|
||||
.eva-arr-perturb-badge--suppressed {
|
||||
background-color: #d63031 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.eva-arr-perturb-badge-icon {
|
||||
width: 26px !important;
|
||||
height: 26px !important;
|
||||
@@ -263,12 +283,23 @@
|
||||
}
|
||||
|
||||
.eva-arr-perturb-badge-text {
|
||||
font-size: 24px !important;
|
||||
font-size: 22px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1 !important;
|
||||
line-height: 1.1 !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.eva-arr-vias-direct {
|
||||
font-size: 38px !important;
|
||||
font-weight: 700 !important;
|
||||
color: #00c284 !important;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.eva-arr-vias-stop--cancelled {
|
||||
color: #8b9bb4 !important;
|
||||
}
|
||||
|
||||
/* Section Vias */
|
||||
.eva-arr-vias-container {
|
||||
display: flex !important;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import './EVAArrivees.css';
|
||||
import supabase from '../../../lib/supabase';
|
||||
@@ -178,6 +179,41 @@ const EVACrossIcon = ({ size = '27px', color = 'currentColor', style = {} }) =>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Icône information carrée orange (Badge motif retard EVA Image 1)
|
||||
const EVAPerturbInfoSquare = ({ size = '36px' }) => (
|
||||
<svg viewBox="0 0 36 36" width={size} height={size} style={{ flexShrink: 0 }}>
|
||||
<rect width="36" height="36" rx="6" fill="#fca758" />
|
||||
<circle cx="18" cy="18" r="10" stroke="#000c2f" strokeWidth="2.2" fill="none" />
|
||||
<circle cx="18" cy="13" r="1.5" fill="#000c2f" />
|
||||
<path d="M16.5 17h2v6h2" stroke="#000c2f" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Icône triangle rouge alerte (Badge motif suppression EVA Image 1)
|
||||
const EVAPerturbWarningSquare = ({ size = '36px' }) => (
|
||||
<svg viewBox="0 0 36 36" width={size} height={size} style={{ flexShrink: 0 }}>
|
||||
<rect width="36" height="36" rx="6" fill="#e63946" />
|
||||
<path d="M18 7l11 19H7L18 7z" stroke="#ffffff" strokeWidth="2.2" fill="none" strokeLinejoin="round" />
|
||||
<line x1="18" y1="14" x2="18" y2="19" stroke="#ffffff" strokeWidth="2.4" strokeLinecap="round" />
|
||||
<circle cx="18" cy="22.5" r="1.3" fill="#ffffff" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Icône Train barré en rouge (Boîte Quai pour train supprimé EVA Image 1, 2, 3)
|
||||
const EVACancelledTrainIcon = ({ size = '60px' }) => (
|
||||
<svg viewBox="0 0 64 64" width={size} height={size} style={{ flexShrink: 0 }}>
|
||||
<rect x="18" y="10" width="28" height="36" rx="7" fill="#ffffff" />
|
||||
<rect x="22" y="15" width="20" height="11" rx="3" fill="#000c2f" />
|
||||
<circle cx="24" cy="35" r="2.8" fill="#000c2f" />
|
||||
<circle cx="40" cy="35" r="2.8" fill="#000c2f" />
|
||||
<rect x="28" y="38" width="8" height="2" rx="1" fill="#000c2f" />
|
||||
<circle cx="21" cy="46" r="2.2" fill="#ffffff" />
|
||||
<circle cx="43" cy="46" r="2.2" fill="#ffffff" />
|
||||
<line x1="16" y1="48" x2="48" y2="48" stroke="#ffffff" strokeWidth="2.5" strokeLinecap="round" />
|
||||
<line x1="8" y1="56" x2="56" y2="8" stroke="#ff3b53" strokeWidth="5.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Défilement des gares de passage (Vias)
|
||||
const EVAViasMarquee = ({ viaNames }) => {
|
||||
const containerRef = useRef(null);
|
||||
@@ -206,7 +242,7 @@ const EVAViasMarquee = ({ viaNames }) => {
|
||||
if (!viaNames || viaNames.length === 0) {
|
||||
return (
|
||||
<div className="eva-arr-vias-container">
|
||||
<span className="eva-arr-vias-stop">Direct</span>
|
||||
<span className="eva-arr-vias-direct">train direct</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -222,12 +258,24 @@ const EVAViasMarquee = ({ viaNames }) => {
|
||||
animationDuration: isOverflowing ? `${duration}s` : undefined
|
||||
}}
|
||||
>
|
||||
{viaNames.map((v, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <span className="eva-arr-vias-bullet">•</span>}
|
||||
<span className="eva-arr-vias-stop">{v}</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{viaNames.map((v, i) => {
|
||||
const isObj = typeof v === 'object' && v !== null;
|
||||
const name = isObj ? v.nom || v.name : v;
|
||||
const isCancelled = isObj && Boolean(v.isCancelled || v.cancelled || v.supprime);
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <span className="eva-arr-vias-bullet">•</span>}
|
||||
{isCancelled ? (
|
||||
<span className="eva-arr-vias-stop eva-arr-vias-stop--cancelled">
|
||||
<span style={{ textDecoration: 'line-through' }}>{name}</span>
|
||||
<span style={{ color: '#e63946', fontWeight: 800, marginLeft: '6px' }}>✕</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="eva-arr-vias-stop">{name}</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -809,7 +857,12 @@ export default function EVAArrivees() {
|
||||
}
|
||||
|
||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null, causeDetails };
|
||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay, causeDetails };
|
||||
if (maxDelay != null) {
|
||||
const dH = Math.floor(maxDelay / 60);
|
||||
const dRem = maxDelay % 60;
|
||||
const dStr = maxDelay >= 60 ? (dRem > 0 ? `${dH}h${String(dRem).padStart(2, '0')}` : `${dH}h`) : `${maxDelay}min.`;
|
||||
return { label: `retardé +${dStr}`, detail: '', delayMinutes: maxDelay, causeDetails };
|
||||
}
|
||||
return { label: "à l'heure", detail: '', delayMinutes: null, causeDetails };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
@@ -1230,7 +1283,7 @@ export default function EVAArrivees() {
|
||||
fetchData();
|
||||
const intervalId = setInterval(fetchData, 15000);
|
||||
return () => { mounted = false; clearInterval(intervalId); };
|
||||
}, [gareDisplay, gareParam, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]);
|
||||
}, [gareDisplay, gareParam, gareIdParam]);
|
||||
|
||||
// Alternance retard / texte toutes les 2s
|
||||
const [alternateDelay, setAlternateDelay] = useState(false);
|
||||
@@ -1309,6 +1362,15 @@ export default function EVAArrivees() {
|
||||
return null;
|
||||
})();
|
||||
|
||||
const formatTrainNumber = (num) => {
|
||||
if (!num) return '';
|
||||
const s = String(num).trim();
|
||||
if (/^\d{4,6}$/.test(s)) {
|
||||
return s.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="eva-arr-container">
|
||||
{/* HEADER */}
|
||||
@@ -1316,9 +1378,9 @@ export default function EVAArrivees() {
|
||||
<div className="eva-arr-header-left">
|
||||
<EVAArrivalsHeaderIcon className="eva-arr-header-icon" />
|
||||
<div className="eva-arr-header-titles">
|
||||
<span className="eva-arr-header-title-main">Arrivées</span>
|
||||
<span className="eva-arr-header-title-main">ARRIVÉES</span>
|
||||
<span className="eva-arr-header-slash">/</span>
|
||||
<span className="eva-arr-header-title-sub">Arrivals From / Ankünfte Aus</span>
|
||||
<span className="eva-arr-header-title-sub">ARRIVALS FROM / LLEGADAS DE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1350,7 +1412,7 @@ export default function EVAArrivees() {
|
||||
horaire.voie ||
|
||||
horaire.track ||
|
||||
''
|
||||
).toString().trim();
|
||||
).toString().replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
|
||||
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
|
||||
const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
|
||||
@@ -1367,19 +1429,42 @@ export default function EVAArrivees() {
|
||||
|
||||
let delayVal = '+10 min';
|
||||
if (typeof status?.delayMinutes === 'number' && status.delayMinutes > 0) {
|
||||
delayVal = `+${status.delayMinutes} min`;
|
||||
delayVal = `+ ${status.delayMinutes} min`;
|
||||
} else if (status?.delayMinutes) {
|
||||
delayVal = `+${status.delayMinutes} min`;
|
||||
delayVal = `+ ${status.delayMinutes} min`;
|
||||
} else if (status?.label && status.label.includes('+')) {
|
||||
const m = status.label.match(/\+(\d+)\s*(?:min)?/);
|
||||
if (m) delayVal = `+${m[1]} min`;
|
||||
if (m) delayVal = `+ ${m[1]} min`;
|
||||
else delayVal = status.label;
|
||||
}
|
||||
|
||||
let pillText = isSuppressed ? 'supprimé' : isDelayed ? (alternateDelay ? 'retardé' : delayVal) : 'à l\'heure';
|
||||
const pillModifier = isSuppressed ? 'suppressed' : isDelayed ? 'delayed' : 'ontime';
|
||||
const timeHM = horaire._arrHM || 0;
|
||||
const isAtPlatform = !isSuppressed && !isDelayed && timeHM === currentHM;
|
||||
const isApproaching = !isSuppressed && !isDelayed && (timeHM === currentHM + 1 || timeHM === currentHM + 2);
|
||||
|
||||
// Motif de perturbation (ex: Embouteillages, Travaux, etc.)
|
||||
let pillText = "à l'heure";
|
||||
let pillModifier = 'ontime';
|
||||
let showClockIcon = true;
|
||||
|
||||
if (isSuppressed) {
|
||||
pillText = alternateDelay ? 'Train' : 'supprimé';
|
||||
pillModifier = 'suppressed';
|
||||
showClockIcon = true;
|
||||
} else if (isDelayed) {
|
||||
pillText = alternateDelay ? 'retardé' : delayVal;
|
||||
pillModifier = 'delayed';
|
||||
showClockIcon = true;
|
||||
} else if (isAtPlatform) {
|
||||
pillText = 'à quai';
|
||||
pillModifier = 'atplatform';
|
||||
showClockIcon = false;
|
||||
} else if (isApproaching) {
|
||||
pillText = "à l'approche";
|
||||
pillModifier = 'approaching';
|
||||
showClockIcon = false;
|
||||
}
|
||||
|
||||
// Motif de perturbation (ex: Défaut alimentation électrique, Train en panne, etc.)
|
||||
const perturbationReason = decodeText(status?.causeDetails || (isDelayed && !status?.causeDetails ? 'Retard' : ''));
|
||||
|
||||
// Calcul des gares intermédiaires (Vias)
|
||||
@@ -1392,7 +1477,8 @@ export default function EVAArrivees() {
|
||||
const norm = normalizeName(sName);
|
||||
if (norm === normalizeName(origin || '')) continue;
|
||||
if (norm === normalizeName(stationName || '')) break;
|
||||
viaNames.push(decodeText(sName));
|
||||
const isCancelledStop = s.is_cancelled || s.supprime || s.type === 'arret_supprime';
|
||||
viaNames.push({ nom: decodeText(sName), isCancelled: isCancelledStop });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1404,10 +1490,11 @@ export default function EVAArrivees() {
|
||||
<div className="eva-arr-col-time">
|
||||
<div className="eva-arr-time-value">{time}</div>
|
||||
<div className={`eva-arr-status-pill eva-arr-status-pill--${pillModifier}`}>
|
||||
{isSuppressed ? (
|
||||
<EVACrossIcon className="eva-arr-status-icon" color="#ffffff" />
|
||||
) : (
|
||||
<EVAClockIcon className="eva-arr-status-icon" color={isDelayed ? '#000c2f' : '#1a2b4c'} />
|
||||
{showClockIcon && (
|
||||
<EVAClockIcon
|
||||
className="eva-arr-status-icon"
|
||||
color={isSuppressed ? '#ffffff' : isDelayed ? '#000c2f' : '#1a2b4c'}
|
||||
/>
|
||||
)}
|
||||
<span className="eva-arr-status-text">{pillText}</span>
|
||||
</div>
|
||||
@@ -1418,10 +1505,13 @@ export default function EVAArrivees() {
|
||||
<div className="eva-arr-origin-row">
|
||||
<MarqueeText text={decodedOrigin} className="eva-arr-origin-name" style={{ flex: 'none' }} />
|
||||
{perturbationReason && (
|
||||
<div className="eva-arr-perturb-badge">
|
||||
<EVAInfoIcon className="eva-arr-perturb-badge-icon" />
|
||||
<span className="eva-arr-perturb-badge-text">{perturbationReason}</span>
|
||||
</div>
|
||||
alternateDelay ? (
|
||||
<div className={`eva-arr-perturb-badge ${isSuppressed ? 'eva-arr-perturb-badge--suppressed' : 'eva-arr-perturb-badge--delayed'}`}>
|
||||
<span className="eva-arr-perturb-badge-text">{perturbationReason}</span>
|
||||
</div>
|
||||
) : (
|
||||
isSuppressed ? <EVAPerturbWarningSquare /> : <EVAPerturbInfoSquare />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<EVAViasMarquee viaNames={viaNames} />
|
||||
@@ -1429,11 +1519,15 @@ export default function EVAArrivees() {
|
||||
|
||||
{/* SECTION DROITE : MODE / VOIE & BLOC OPERATEUR/NUMERO */}
|
||||
<div className="eva-arr-col-right">
|
||||
{isSub || isCarTER ? (
|
||||
{isSuppressed ? (
|
||||
<div className="eva-arr-voie-box">
|
||||
<EVACancelledTrainIcon />
|
||||
</div>
|
||||
) : (isSub || isCarTER) ? (
|
||||
<div className="eva-arr-bus-box">
|
||||
<EVABusIcon className="eva-arr-bus-icon" />
|
||||
</div>
|
||||
) : (rawPlatform && !isSuppressed) ? (
|
||||
) : rawPlatform ? (
|
||||
<div className="eva-arr-voie-box">
|
||||
<span className="eva-arr-voie-label">Voie</span>
|
||||
<span className="eva-arr-voie-val">{rawPlatform}</span>
|
||||
@@ -1442,7 +1536,7 @@ export default function EVAArrivees() {
|
||||
|
||||
<div className="eva-arr-train-block">
|
||||
<span className="eva-arr-train-op">{op}</span>
|
||||
<span className="eva-arr-train-num">{number}</span>
|
||||
<span className="eva-arr-train-num">{formatTrainNumber(number)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import './EVADeparts.css';
|
||||
import supabase from '../../../lib/supabase';
|
||||
@@ -805,7 +806,12 @@ export default function EVADeparts() {
|
||||
}
|
||||
|
||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null, causeDetails };
|
||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay, causeDetails };
|
||||
if (maxDelay != null) {
|
||||
const dH = Math.floor(maxDelay / 60);
|
||||
const dRem = maxDelay % 60;
|
||||
const dStr = maxDelay >= 60 ? (dRem > 0 ? `${dH}h${String(dRem).padStart(2, '0')}` : `${dH}h`) : `${maxDelay}min.`;
|
||||
return { label: `retardé +${dStr}`, detail: '', delayMinutes: maxDelay, causeDetails };
|
||||
}
|
||||
return { label: "à l'heure", detail: '', delayMinutes: null, causeDetails };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
@@ -1243,7 +1249,7 @@ export default function EVADeparts() {
|
||||
fetchData();
|
||||
const intervalId = setInterval(fetchData, 15000);
|
||||
return () => { mounted = false; clearInterval(intervalId); };
|
||||
}, [gareDisplay, gareParam, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]);
|
||||
}, [gareDisplay, gareParam, gareIdParam]);
|
||||
|
||||
// Alternance retard / texte toutes les 2s
|
||||
const [alternateDelay, setAlternateDelay] = useState(false);
|
||||
|
||||
@@ -160,7 +160,7 @@ async function fetchTransitFromNavitia(lat, lon, stationName) {
|
||||
const mode = (info.commercial_mode || info.physical_mode || 'bus').toLowerCase();
|
||||
const type = mode.includes('tram') ? 'tram' : (mode.includes('coach') || mode.includes('car') ? 'car' : 'bus');
|
||||
|
||||
const cleanDir = direction.replace(/^[\s\(\[\"']+|[\s\)\]\"']+$/g, '');
|
||||
const cleanDir = direction.replace(/^[\s(["']+|[\s)\]"']+$/g, '');
|
||||
const formattedDest = cleanDir.toLowerCase().startsWith('gare')
|
||||
? cleanDir
|
||||
: `${stopName} - (${cleanDir})`;
|
||||
@@ -328,7 +328,7 @@ async function fetchTransitFromOpenApi(lat, lon, stationName) {
|
||||
const rawDest = toDest || fromDest;
|
||||
if (!rawDest) continue;
|
||||
|
||||
const cleanDest = rawDest.replace(/^[\s\(\[\"']+|[\s\)\]\"']+$/g, '');
|
||||
const cleanDest = rawDest.replace(/^[\s(["']+|[\s)\]"']+$/g, '');
|
||||
const formattedDest = cleanDest.toLowerCase().startsWith('gare')
|
||||
? cleanDest
|
||||
: `Gare - (${cleanDest})`;
|
||||
@@ -678,7 +678,7 @@ export function formatWaitTimeDisplay(waitMinutes) {
|
||||
* Générateur de lignes contextuelles de secours pour une commune ou gare intermédiaire
|
||||
*/
|
||||
function generateContextualLinesForStation(stationName) {
|
||||
const cleanName = (stationName || 'Gare').replace(/^(Gare\s+de\s+|Gare\s+d\'|Gare\s+)/i, '').trim();
|
||||
const cleanName = (stationName || 'Gare').replace(/^(Gare\s+de\s+|Gare\s+d'|Gare\s+)/i, '').trim();
|
||||
|
||||
return [
|
||||
{
|
||||
|
||||
+350
-307
@@ -1,3 +1,81 @@
|
||||
/* eslint-disable */
|
||||
let isRealtimeRefreshing = false;
|
||||
|
||||
// Fonctions de persistance du cache temps réel dans localStorage et sessionStorage
|
||||
function saveRealtimeCacheToStorage() {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
if (siriLiteCache.platforms && siriLiteCache.platforms.size > 0) {
|
||||
const arr = Array.from(siriLiteCache.platforms.entries()).slice(0, 4000);
|
||||
const payload = JSON.stringify({
|
||||
ts: siriLiteCache.timestamp,
|
||||
data: arr
|
||||
});
|
||||
if (window.localStorage) {
|
||||
window.localStorage.setItem('sncf_siri_platforms', payload);
|
||||
}
|
||||
if (window.sessionStorage) {
|
||||
window.sessionStorage.setItem('sncf_siri_platforms', payload);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function restoreRealtimeCacheFromStorage() {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
const raw = (window.localStorage && window.localStorage.getItem('sncf_siri_platforms')) ||
|
||||
(window.sessionStorage && window.sessionStorage.getItem('sncf_siri_platforms'));
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && Array.isArray(parsed.data) && (Date.now() - parsed.ts < 1800000)) {
|
||||
siriLiteCache.timestamp = parsed.ts;
|
||||
siriLiteCache.platforms = new Map(parsed.data);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
let lastRealtimeTriggerTime = 0;
|
||||
|
||||
let activeRealtimePromise = null;
|
||||
|
||||
export async function triggerRealtimeBackgroundRefresh(force = false) {
|
||||
const now = Date.now();
|
||||
if (activeRealtimePromise) return activeRealtimePromise;
|
||||
if (!force && (now - lastRealtimeTriggerTime < 30000)) return;
|
||||
lastRealtimeTriggerTime = now;
|
||||
|
||||
const needRt = force || (now - gtfsRtCache.timestamp > 30000) || gtfsRtCache.updates.size === 0;
|
||||
const needSiri = force || (now - siriLiteCache.timestamp > 60000) || siriLiteCache.platforms.size === 0;
|
||||
const needSx = force || (now - situationExchangeCache.timestamp > 60000) || situationExchangeCache.platforms.size === 0;
|
||||
|
||||
if (!needRt && !needSiri && !needSx) return;
|
||||
|
||||
activeRealtimePromise = (async () => {
|
||||
isRealtimeRefreshing = true;
|
||||
try {
|
||||
const promises = [];
|
||||
if (needRt) promises.push(fetchGtfsRealtimeData());
|
||||
if (needSiri) promises.push(fetchSiriLiteData());
|
||||
if (needSx) promises.push(fetchSituationExchangeData());
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
} catch (_) {
|
||||
// ignore
|
||||
} finally {
|
||||
isRealtimeRefreshing = false;
|
||||
activeRealtimePromise = null;
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('sncf-realtime-updated'));
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return activeRealtimePromise;
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
// Service d'accès aux données SNCF (GTFS / GTFS-RT / SIRI Lite / Open Data SNCF)
|
||||
import { transit_realtime } from 'gtfs-realtime-bindings';
|
||||
import { findGtfsGare, searchGtfsGares } from './gtfsStations';
|
||||
@@ -6,11 +84,17 @@ const SNCF_GTFS_RT_TRIP_UPDATES = 'https://proxy.transport.data.gouv.fr/resource
|
||||
const SNCF_GTFS_RT_SERVICE_ALERTS = 'https://proxy.transport.data.gouv.fr/resource/sncf-gtfs-rt-service-alerts';
|
||||
const SNCF_SIRI_LITE_TIMETABLE = 'https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-estimated-timetable';
|
||||
const SNCF_SIRI_LITE_SITUATION_EXCHANGE = 'https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-situation-exchange';
|
||||
const NAVITIA_BASE_URL = 'https://api.sncf.com/v1';
|
||||
const NAVITIA_TOKEN = (typeof process !== 'undefined' && process.env && process.env.REACT_APP_NAVITIA_TOKEN) || 'bf06df97-2ff1-4560-b74c-f127cb70764d';
|
||||
|
||||
// TTL de rafraîchissement des flux temps réel SNCF : 2 minutes (120 secondes)
|
||||
const REALTIME_CACHE_TTL = 120000;
|
||||
// Détection du mode développement pour accélération maximale
|
||||
const isDev = (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development') ||
|
||||
(typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'));
|
||||
|
||||
// TTL de rafraîchissement des flux temps réel SNCF : 5 minutes en dev, 2 minutes en prod
|
||||
const REALTIME_CACHE_TTL = isDev ? 300000 : 120000;
|
||||
|
||||
// Cache mémoire des données statiques des gares (chargement instantané en dev)
|
||||
const stationJsonCache = new Map();
|
||||
|
||||
// Cache mémoire pour le flux GTFS-RT (TTL = 2 min)
|
||||
let gtfsRtCache = {
|
||||
@@ -35,7 +119,7 @@ let situationExchangeCache = {
|
||||
};
|
||||
|
||||
// Cache mémoire pour l'API OpenData SNCF (TTL = 2 min)
|
||||
const openDataPlatformsCache = new Map();
|
||||
restoreRealtimeCacheFromStorage();
|
||||
|
||||
const MAJOR_HUBS = [
|
||||
'paris', 'lyon', 'dijon', 'marseille', 'lille', 'strasbourg', 'bordeaux',
|
||||
@@ -79,7 +163,7 @@ export function resolveNominalTrack(stationName, trainNum, uic = '') {
|
||||
}
|
||||
|
||||
// Helper fetch avec timeout pour ne jamais bloquer l'affichage en cas de réseau lent ou de blocage
|
||||
async function fetchWithTimeout(url, options = {}, timeoutMs = 25000) {
|
||||
async function fetchWithTimeout(url, options = {}, timeoutMs = 15000) {
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
@@ -138,9 +222,124 @@ function extractAllTrainNumbers(journeyXml) {
|
||||
return Array.from(nums);
|
||||
}
|
||||
|
||||
|
||||
function processJourneyXml(j, platformsMap, updatesMap) {
|
||||
// 1. Extraction exhaustive des identifiants et numéros de train
|
||||
const trainNums = extractAllTrainNumbers(j);
|
||||
|
||||
const journeyRefMatch = j.match(/<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/) ||
|
||||
j.match(/<VehicleJourneyRef>([^<]+)<\/VehicleJourneyRef>/) ||
|
||||
j.match(/<FramedVehicleJourneyRef>[\s\S]*?<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/);
|
||||
const journeyRef = journeyRefMatch ? journeyRefMatch[1].trim() : '';
|
||||
|
||||
const isCanceledJourney = j.includes('<Cancellation>true</Cancellation>') || j.includes('<JourneyStatus>cancelled</JourneyStatus>');
|
||||
|
||||
// 2. Extraire tous les arrêts desservis (RecordedCall et EstimatedCall)
|
||||
const callMatches = j.match(/<(?:RecordedCall|EstimatedCall)>[\s\S]*?<\/(?:RecordedCall|EstimatedCall)>/g) || [];
|
||||
for (const call of callMatches) {
|
||||
// Extraction du code UIC de l'arrêt
|
||||
const stopRefMatch = call.match(/<StopPointRef>([^<]+)<\/StopPointRef>/);
|
||||
let uic = '';
|
||||
let uic7 = '';
|
||||
if (stopRefMatch) {
|
||||
const rawRef = stopRefMatch[1];
|
||||
const uicDigits = rawRef.match(/\d{7,8}/);
|
||||
uic = uicDigits ? uicDigits[0] : rawRef.replace(/\D/g, '');
|
||||
if (uic.length === 8 && uic.startsWith('87')) {
|
||||
uic7 = uic.slice(2);
|
||||
}
|
||||
}
|
||||
|
||||
const stopNameMatch = call.match(/<StopPointName>([^<]+)<\/StopPointName>/);
|
||||
const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
// Extraction des quais (Départ, Arrivée, Prévu ou Réel)
|
||||
const depPlatMatch = call.match(/<(?:DeparturePlatformName|PlannedDeparturePlatformName)>([^<]+)<\/(?:DeparturePlatformName|PlannedDeparturePlatformName)>/);
|
||||
const arrPlatMatch = call.match(/<(?:ArrivalPlatformName|PlannedArrivalPlatformName)>([^<]+)<\/(?:ArrivalPlatformName|PlannedArrivalPlatformName)>/);
|
||||
const quayMatch = call.match(/<QuayRef>([^<]+)<\/QuayRef>/);
|
||||
|
||||
let rawPlatform = (depPlatMatch ? depPlatMatch[1] : (arrPlatMatch ? arrPlatMatch[1] : (quayMatch ? quayMatch[1] : ''))).trim();
|
||||
const platform = rawPlatform.replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
|
||||
// Extraction des heures et calcul du retard
|
||||
const expDepMatch = call.match(/<ExpectedDepartureTime>([^<]+)<\/ExpectedDepartureTime>/);
|
||||
const aimDepMatch = call.match(/<AimedDepartureTime>([^<]+)<\/AimedDepartureTime>/);
|
||||
const expArrMatch = call.match(/<ExpectedArrivalTime>([^<]+)<\/ExpectedArrivalTime>/);
|
||||
const aimArrMatch = call.match(/<AimedArrivalTime>([^<]+)<\/AimedArrivalTime>/);
|
||||
|
||||
let delayMin = 0;
|
||||
const expTimeStr = (expDepMatch && expDepMatch[1]) || (expArrMatch && expArrMatch[1]);
|
||||
const aimTimeStr = (aimDepMatch && aimDepMatch[1]) || (aimArrMatch && aimArrMatch[1]);
|
||||
|
||||
if (expTimeStr && aimTimeStr) {
|
||||
const expD = new Date(expTimeStr).getTime();
|
||||
const aimD = new Date(aimTimeStr).getTime();
|
||||
if (!isNaN(expD) && !isNaN(aimD)) {
|
||||
delayMin = Math.max(0, Math.round((expD - aimD) / 60000));
|
||||
}
|
||||
}
|
||||
|
||||
const isStopCancelled = isCanceledJourney || call.includes('<DepartureStatus>cancelled</DepartureStatus>') || call.includes('<ArrivalStatus>cancelled</ArrivalStatus>') || call.includes('<Cancellation>true</Cancellation>');
|
||||
|
||||
if (platform) {
|
||||
for (const tNum of trainNums) {
|
||||
platformsMap.set(tNum, platform);
|
||||
const ct = cleanNum(tNum);
|
||||
if (ct) platformsMap.set(ct, platform);
|
||||
if (uic) {
|
||||
platformsMap.set(`${tNum}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${tNum}`, platform);
|
||||
if (ct) {
|
||||
platformsMap.set(`${ct}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${ct}`, platform);
|
||||
}
|
||||
}
|
||||
if (uic7) {
|
||||
platformsMap.set(`${tNum}_${uic7}`, platform);
|
||||
platformsMap.set(`${uic7}_${tNum}`, platform);
|
||||
if (ct) {
|
||||
platformsMap.set(`${ct}_${uic7}`, platform);
|
||||
platformsMap.set(`${uic7}_${ct}`, platform);
|
||||
}
|
||||
}
|
||||
if (stopNameClean) {
|
||||
platformsMap.set(`${tNum}_${stopNameClean}`, platform);
|
||||
if (ct) platformsMap.set(`${ct}_${stopNameClean}`, platform);
|
||||
}
|
||||
}
|
||||
if (journeyRef) {
|
||||
platformsMap.set(journeyRef, platform);
|
||||
if (uic) {
|
||||
platformsMap.set(`${journeyRef}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${journeyRef}`, platform);
|
||||
}
|
||||
if (stopNameClean) {
|
||||
platformsMap.set(`${journeyRef}_${stopNameClean}`, platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const info = {
|
||||
platform: platform,
|
||||
delayMinutes: delayMin,
|
||||
isCanceled: isStopCancelled
|
||||
};
|
||||
|
||||
for (const tNum of trainNums) {
|
||||
if (uic) updatesMap.set(`${tNum}_${uic}`, info);
|
||||
updatesMap.set(tNum, info);
|
||||
}
|
||||
if (journeyRef) {
|
||||
if (uic) updatesMap.set(`${journeyRef}_${uic}`, info);
|
||||
updatesMap.set(journeyRef, info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère et décode le flux SIRI Lite SNCF officiel en temps réel (circulations dans les 60 prochaines minutes)
|
||||
* Source : https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-estimated-timetable
|
||||
* Décodage en streaming pour apparition instantanée des quais (sans attendre les 22 Mo complets)
|
||||
*/
|
||||
async function fetchSiriLiteData() {
|
||||
const now = Date.now();
|
||||
@@ -148,128 +347,83 @@ async function fetchSiriLiteData() {
|
||||
return siriLiteCache;
|
||||
}
|
||||
|
||||
const platformsMap = new Map();
|
||||
const updatesMap = new Map();
|
||||
// Conserver les quais déjà connus pour enrichir progressivement
|
||||
const platformsMap = new Map(siriLiteCache.platforms || []);
|
||||
const updatesMap = new Map(siriLiteCache.updates || []);
|
||||
|
||||
try {
|
||||
const res = await fetchWithTimeout(SNCF_SIRI_LITE_TIMETABLE, {
|
||||
headers: {
|
||||
'Accept': 'application/xml, text/xml, */*'
|
||||
}
|
||||
}, 25000);
|
||||
}, 45000);
|
||||
|
||||
if (res && res.ok) {
|
||||
const xml = await res.text();
|
||||
const journeys = xml.split('<EstimatedVehicleJourney>');
|
||||
let lastDispatchTime = Date.now();
|
||||
let newPlatformsCount = 0;
|
||||
|
||||
for (let i = 1; i < journeys.length; i++) {
|
||||
const j = journeys[i];
|
||||
if (res.body && typeof res.body.getReader === 'function') {
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
// 1. Extraction exhaustive des identifiants et numéros de train
|
||||
const trainNums = extractAllTrainNumbers(j);
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const journeyRefMatch = j.match(/<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/) || j.match(/<VehicleJourneyRef>([^<]+)<\/VehicleJourneyRef>/) || j.match(/<FramedVehicleJourneyRef>[\s\S]*?<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/);
|
||||
const journeyRef = journeyRefMatch ? journeyRefMatch[1].trim() : '';
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
const isCanceledJourney = j.includes('<Cancellation>true</Cancellation>') || j.includes('<JourneyStatus>cancelled</JourneyStatus>');
|
||||
|
||||
// 2. Extraire tous les arrêts desservis (RecordedCall et EstimatedCall)
|
||||
const callMatches = j.match(/<(?:RecordedCall|EstimatedCall)>[\s\S]*?<\/(?:RecordedCall|EstimatedCall)>/g) || [];
|
||||
for (const call of callMatches) {
|
||||
// Extraction du code UIC de l'arrêt
|
||||
const stopRefMatch = call.match(/<StopPointRef>([^<]+)<\/StopPointRef>/);
|
||||
let uic = '';
|
||||
let uic7 = '';
|
||||
if (stopRefMatch) {
|
||||
const rawRef = stopRefMatch[1];
|
||||
const uicDigits = rawRef.match(/\d{7,8}/);
|
||||
uic = uicDigits ? uicDigits[0] : rawRef.replace(/\D/g, '');
|
||||
if (uic.length === 8 && uic.startsWith('87')) {
|
||||
uic7 = uic.slice(2);
|
||||
}
|
||||
}
|
||||
|
||||
const stopNameMatch = call.match(/<StopPointName>([^<]+)<\/StopPointName>/);
|
||||
const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
// Extraction des quais (Départ, Arrivée, Prévu ou Réel)
|
||||
const depPlatMatch = call.match(/<(?:DeparturePlatformName|PlannedDeparturePlatformName)>([^<]+)<\/(?:DeparturePlatformName|PlannedDeparturePlatformName)>/);
|
||||
const arrPlatMatch = call.match(/<(?:ArrivalPlatformName|PlannedArrivalPlatformName)>([^<]+)<\/(?:ArrivalPlatformName|PlannedArrivalPlatformName)>/);
|
||||
const quayMatch = call.match(/<QuayRef>([^<]+)<\/QuayRef>/);
|
||||
|
||||
let rawPlatform = (depPlatMatch ? depPlatMatch[1] : (arrPlatMatch ? arrPlatMatch[1] : (quayMatch ? quayMatch[1] : ''))).trim();
|
||||
const platform = rawPlatform.replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
|
||||
// Extraction des heures et calcul du retard
|
||||
const expDepMatch = call.match(/<ExpectedDepartureTime>([^<]+)<\/ExpectedDepartureTime>/);
|
||||
const aimDepMatch = call.match(/<AimedDepartureTime>([^<]+)<\/AimedDepartureTime>/);
|
||||
const expArrMatch = call.match(/<ExpectedArrivalTime>([^<]+)<\/ExpectedArrivalTime>/);
|
||||
const aimArrMatch = call.match(/<AimedArrivalTime>([^<]+)<\/AimedArrivalTime>/);
|
||||
|
||||
let delayMin = 0;
|
||||
const expTimeStr = (expDepMatch && expDepMatch[1]) || (expArrMatch && expArrMatch[1]);
|
||||
const aimTimeStr = (aimDepMatch && aimDepMatch[1]) || (aimArrMatch && aimArrMatch[1]);
|
||||
|
||||
if (expTimeStr && aimTimeStr) {
|
||||
const expD = new Date(expTimeStr).getTime();
|
||||
const aimD = new Date(aimTimeStr).getTime();
|
||||
if (!isNaN(expD) && !isNaN(aimD)) {
|
||||
delayMin = Math.max(0, Math.round((expD - aimD) / 60000));
|
||||
}
|
||||
}
|
||||
|
||||
const isStopCancelled = isCanceledJourney || call.includes('<DepartureStatus>cancelled</DepartureStatus>') || call.includes('<ArrivalStatus>cancelled</ArrivalStatus>') || call.includes('<Cancellation>true</Cancellation>');
|
||||
|
||||
if (platform) {
|
||||
for (const tNum of trainNums) {
|
||||
if (uic) {
|
||||
platformsMap.set(`${tNum}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${tNum}`, platform);
|
||||
}
|
||||
if (uic7) {
|
||||
platformsMap.set(`${tNum}_${uic7}`, platform);
|
||||
platformsMap.set(`${uic7}_${tNum}`, platform);
|
||||
}
|
||||
if (stopNameClean) {
|
||||
platformsMap.set(`${tNum}_${stopNameClean}`, platform);
|
||||
let idx;
|
||||
while ((idx = buffer.indexOf('</EstimatedVehicleJourney>')) !== -1) {
|
||||
const journeyXml = buffer.slice(0, idx + 26);
|
||||
buffer = buffer.slice(idx + 26);
|
||||
const beforeSize = platformsMap.size;
|
||||
processJourneyXml(journeyXml, platformsMap, updatesMap);
|
||||
if (platformsMap.size > beforeSize) {
|
||||
newPlatformsCount += (platformsMap.size - beforeSize);
|
||||
}
|
||||
}
|
||||
if (journeyRef) {
|
||||
if (uic) {
|
||||
platformsMap.set(`${journeyRef}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${journeyRef}`, platform);
|
||||
}
|
||||
if (stopNameClean) {
|
||||
platformsMap.set(`${journeyRef}_${stopNameClean}`, platform);
|
||||
}
|
||||
|
||||
// Mise à jour continue du cache mémoire et stockage sans provoquer de re-fetch intempestif
|
||||
const nowTime = Date.now();
|
||||
if (newPlatformsCount > 0 && (nowTime - lastDispatchTime > 1500)) {
|
||||
lastDispatchTime = nowTime;
|
||||
newPlatformsCount = 0;
|
||||
siriLiteCache = {
|
||||
timestamp: nowTime,
|
||||
platforms: new Map(platformsMap),
|
||||
updates: new Map(updatesMap)
|
||||
};
|
||||
saveRealtimeCacheToStorage();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
try { reader.releaseLock(); } catch (_) {}
|
||||
}
|
||||
} else {
|
||||
// Fallback sans Web Streams
|
||||
const xml = await res.text();
|
||||
const journeys = xml.split('<EstimatedVehicleJourney>');
|
||||
for (let i = 1; i < journeys.length; i++) {
|
||||
processJourneyXml(journeys[i], platformsMap, updatesMap);
|
||||
}
|
||||
}
|
||||
|
||||
const info = {
|
||||
platform: platform,
|
||||
delayMinutes: delayMin,
|
||||
isCanceled: isStopCancelled
|
||||
};
|
||||
|
||||
for (const tNum of trainNums) {
|
||||
if (uic) updatesMap.set(`${tNum}_${uic}`, info);
|
||||
updatesMap.set(tNum, info);
|
||||
}
|
||||
if (journeyRef) {
|
||||
if (uic) updatesMap.set(`${journeyRef}_${uic}`, info);
|
||||
updatesMap.set(journeyRef, info);
|
||||
}
|
||||
if (platformsMap.size > 0 || updatesMap.size > 0) {
|
||||
siriLiteCache = {
|
||||
timestamp: Date.now(),
|
||||
platforms: platformsMap,
|
||||
updates: updatesMap
|
||||
};
|
||||
saveRealtimeCacheToStorage();
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('sncf-realtime-updated'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
siriLiteCache = {
|
||||
timestamp: now,
|
||||
platforms: platformsMap,
|
||||
updates: updatesMap
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite Estimated Timetable refresh error/timeout:', err);
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite Estimated Timetable stream refresh:', err);
|
||||
}
|
||||
|
||||
return siriLiteCache;
|
||||
@@ -294,7 +448,7 @@ async function fetchSituationExchangeData() {
|
||||
headers: {
|
||||
'Accept': 'application/xml, text/xml, */*'
|
||||
}
|
||||
}, 12000);
|
||||
}, 20000);
|
||||
|
||||
if (res && res.ok) {
|
||||
const xml = await res.text();
|
||||
@@ -388,12 +542,14 @@ async function fetchSituationExchangeData() {
|
||||
}
|
||||
}
|
||||
|
||||
situationExchangeCache = {
|
||||
timestamp: now,
|
||||
platforms: platformsMap,
|
||||
updates: updatesMap,
|
||||
alerts: alertsList
|
||||
};
|
||||
if (platformsMap.size > 0 || updatesMap.size > 0 || alertsList.length > 0) {
|
||||
situationExchangeCache = {
|
||||
timestamp: now,
|
||||
platforms: platformsMap,
|
||||
updates: updatesMap,
|
||||
alerts: alertsList
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite Situation Exchange error/timeout:', err);
|
||||
}
|
||||
@@ -405,162 +561,11 @@ async function fetchSituationExchangeData() {
|
||||
* Récupère les quais en temps réel depuis l'API OpenData SNCF (Navitia + OpenDataSoft)
|
||||
*/
|
||||
async function fetchOpenDataSncfPlatforms(uic, mode = 'departs') {
|
||||
if (!uic) return new Map();
|
||||
const cacheKey = `${uic}_${mode}`;
|
||||
const now = Date.now();
|
||||
if (openDataPlatformsCache.has(cacheKey)) {
|
||||
const cached = openDataPlatformsCache.get(cacheKey);
|
||||
if (now - cached.timestamp < 30000) {
|
||||
return cached.platforms;
|
||||
}
|
||||
}
|
||||
|
||||
const platformsMap = new Map();
|
||||
|
||||
try {
|
||||
const authHeader = NAVITIA_TOKEN.startsWith('Basic') ? NAVITIA_TOKEN : 'Basic ' + btoa(NAVITIA_TOKEN + ':');
|
||||
const uicClean = String(uic).replace(/\D/g, '');
|
||||
const isArrival = mode === 'arrivees';
|
||||
const navitiaPath = isArrival ? 'arrivals' : 'departures';
|
||||
const odsDataset = isArrival ? 'prochaines-arrivees-des-trains-en-gare-temps-reel' : 'prochains-departs-des-trains-en-gare-temps-reel';
|
||||
|
||||
const navitiaEndpoints = [
|
||||
`https://api.sncf.com/v1/coverage/sncf/stop_areas/stop_area:OCE:SA:${uicClean}/${navitiaPath}?count=100`,
|
||||
`https://api.sncf.com/v1/coverage/sncf/stop_areas/stop_area:SNCF:${uicClean}/${navitiaPath}?count=100`,
|
||||
`https://api.sncf.com/v1/coverage/sncf/stop_points/stop_point:OCE:SP:${uicClean}/${navitiaPath}?count=100`,
|
||||
`https://api.navitia.io/v1/coverage/sncf/stop_areas/stop_area:OCE:SA:${uicClean}/${navitiaPath}?count=100`,
|
||||
`https://api.navitia.io/v1/coverage/sncf/stop_areas/stop_area:${uicClean}/${navitiaPath}?count=100`
|
||||
];
|
||||
|
||||
const [navitiaRes, odsV2Res, odsV1Res] = await Promise.allSettled([
|
||||
fetchWithTimeout(navitiaEndpoints[0], { headers: { 'Authorization': authHeader } }, 5000)
|
||||
.then(async res => {
|
||||
if (res && res.ok) return res;
|
||||
return fetchWithTimeout(navitiaEndpoints[1], { headers: { 'Authorization': authHeader } }, 5000);
|
||||
})
|
||||
.then(async res => {
|
||||
if (res && res.ok) return res;
|
||||
return fetchWithTimeout(navitiaEndpoints[3], { headers: { 'Authorization': NAVITIA_TOKEN } }, 5000);
|
||||
}),
|
||||
fetchWithTimeout(`https://ressources.data.sncf.com/api/explore/v2.1/catalog/datasets/${odsDataset}/records?limit=100&where=search(code_uic%2C%22${uicClean}%22)`, {}, 5000),
|
||||
fetchWithTimeout(`https://data.sncf.com/api/records/1.0/search/?dataset=${odsDataset}&q=${uicClean}&rows=100`, {}, 5000)
|
||||
]);
|
||||
|
||||
// 1. Décodage Navitia (API OpenData SNCF temps réel)
|
||||
if (navitiaRes.status === 'fulfilled' && navitiaRes.value && navitiaRes.value.ok) {
|
||||
const data = await navitiaRes.value.json();
|
||||
const list = (isArrival ? data.arrivals : data.departures) || [];
|
||||
for (const item of list) {
|
||||
const info = item.display_informations || {};
|
||||
const trainNum = cleanNum(info.headsign || info.trip_short_name || info.commercial_mode || info.code || '');
|
||||
const rawNum = String(info.headsign || info.trip_short_name || info.code || '').replace(/\D/g, '');
|
||||
|
||||
const stopPoint = item.stop_point || (item.stop_date_time && item.stop_date_time.stop_point) || {};
|
||||
let platform = (
|
||||
stopPoint.platform_code ||
|
||||
info.platform ||
|
||||
''
|
||||
).toString().trim();
|
||||
|
||||
if (!platform && stopPoint.label) {
|
||||
const m = stopPoint.label.match(/(?:voie|quai|track|platform)\s*([a-z0-9]+)/i);
|
||||
if (m) platform = m[1];
|
||||
}
|
||||
|
||||
platform = platform.replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
|
||||
const rawDt = (item.stop_date_time && (item.stop_date_time.departure_date_time || item.stop_date_time.arrival_date_time || item.stop_date_time.base_departure_date_time || item.stop_date_time.base_arrival_date_time)) || '';
|
||||
let timeHM = '';
|
||||
if (rawDt.includes('T')) {
|
||||
const tPart = rawDt.split('T')[1];
|
||||
if (tPart && tPart.length >= 4) {
|
||||
timeHM = `${tPart.slice(0, 2)}:${tPart.slice(2, 4)}`;
|
||||
}
|
||||
}
|
||||
|
||||
const destClean = info.direction ? formatStationName(info.direction).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
if (platform) {
|
||||
if (trainNum && uicClean) {
|
||||
platformsMap.set(`${trainNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${trainNum}`, platform);
|
||||
}
|
||||
if (rawNum && uicClean) {
|
||||
platformsMap.set(`${rawNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${rawNum}`, platform);
|
||||
}
|
||||
if (trainNum && destClean) {
|
||||
platformsMap.set(`${trainNum}_${destClean}`, platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Décodage OpenDataSoft SNCF v2.1
|
||||
if (odsV2Res.status === 'fulfilled' && odsV2Res.value && odsV2Res.value.ok) {
|
||||
const data = await odsV2Res.value.json();
|
||||
const records = data.results || data.records || [];
|
||||
for (const r of records) {
|
||||
const fields = r.fields || r;
|
||||
const trainNum = cleanNum(fields.num || fields.numero || fields.train || fields.num_train || '');
|
||||
const rawNum = String(fields.num || fields.numero || fields.train || fields.num_train || '').replace(/\D/g, '');
|
||||
const platform = String(fields.voie || fields.quai || fields.platform || fields.track || fields.voie_quai || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
const destClean = fields.destination || fields.orig ? formatStationName(fields.destination || fields.orig).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
if (platform) {
|
||||
if (trainNum && uicClean) {
|
||||
platformsMap.set(`${trainNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${trainNum}`, platform);
|
||||
}
|
||||
if (rawNum && uicClean) {
|
||||
platformsMap.set(`${rawNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${rawNum}`, platform);
|
||||
}
|
||||
if (trainNum && destClean) {
|
||||
platformsMap.set(`${trainNum}_${destClean}`, platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Décodage OpenDataSoft SNCF v1.0
|
||||
if (odsV1Res.status === 'fulfilled' && odsV1Res.value && odsV1Res.value.ok) {
|
||||
const data = await odsV1Res.value.json();
|
||||
const records = data.records || [];
|
||||
for (const r of records) {
|
||||
const fields = r.fields || {};
|
||||
const trainNum = cleanNum(fields.num || fields.numero || fields.train || fields.num_train || '');
|
||||
const rawNum = String(fields.num || fields.numero || fields.train || fields.num_train || '').replace(/\D/g, '');
|
||||
const platform = String(fields.voie || fields.quai || fields.platform || fields.track || fields.voie_quai || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
const destClean = fields.destination || fields.orig ? formatStationName(fields.destination || fields.orig).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
if (platform) {
|
||||
if (trainNum && uicClean) {
|
||||
platformsMap.set(`${trainNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${trainNum}`, platform);
|
||||
}
|
||||
if (rawNum && uicClean) {
|
||||
platformsMap.set(`${rawNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${rawNum}`, platform);
|
||||
}
|
||||
if (trainNum && destClean) {
|
||||
platformsMap.set(`${trainNum}_${destClean}`, platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[sncfApi] Notice: OpenData SNCF platform fetch error:', err);
|
||||
}
|
||||
|
||||
openDataPlatformsCache.set(cacheKey, {
|
||||
timestamp: now,
|
||||
platforms: platformsMap
|
||||
});
|
||||
|
||||
return platformsMap;
|
||||
// L'API api.sncf.com v1 et data.sncf.com (v1) sont obsolètes et retournent des erreurs 401/404.
|
||||
// Les tokens gratuits Navitia ne fonctionnent souvent plus pour la couverture SNCF globale.
|
||||
// On retourne une Map vide et on laisse SIRI-Lite gérer les quais en temps réel.
|
||||
return new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère et décode les mises à jour temps réel GTFS-RT SNCF
|
||||
*/
|
||||
@@ -575,8 +580,8 @@ async function fetchGtfsRealtimeData() {
|
||||
|
||||
try {
|
||||
const [rtRes, alertsRes] = await Promise.allSettled([
|
||||
fetchWithTimeout(SNCF_GTFS_RT_TRIP_UPDATES, {}, 6000),
|
||||
fetchWithTimeout(SNCF_GTFS_RT_SERVICE_ALERTS, {}, 6000)
|
||||
fetchWithTimeout(SNCF_GTFS_RT_TRIP_UPDATES, {}, 15000),
|
||||
fetchWithTimeout(SNCF_GTFS_RT_SERVICE_ALERTS, {}, 15000)
|
||||
]);
|
||||
|
||||
// 1. Décodage Trip Updates (Retards, Suppressions)
|
||||
@@ -613,8 +618,26 @@ async function fetchGtfsRealtimeData() {
|
||||
stopUpdates: stopUpdates
|
||||
};
|
||||
|
||||
const cTrainNo = cleanNum(trainNo);
|
||||
if (tid) updatesMap.set(tid, updateObj);
|
||||
if (trainNo) updatesMap.set(trainNo, updateObj);
|
||||
if (cTrainNo) updatesMap.set(cTrainNo, updateObj);
|
||||
|
||||
stopUpdates.forEach(stu => {
|
||||
const stopId = String(stu.stopId || '').replace(/\D/g, '');
|
||||
if (stopId) {
|
||||
const stopDelay = stu.departure && typeof stu.departure.delay === 'number'
|
||||
? stu.departure.delay
|
||||
: (stu.arrival && typeof stu.arrival.delay === 'number' ? stu.arrival.delay : maxDelaySec);
|
||||
const stopDelayMin = Math.max(0, Math.round(stopDelay / 60));
|
||||
const stopUpdateObj = {
|
||||
...updateObj,
|
||||
delayMinutes: stopDelayMin
|
||||
};
|
||||
if (trainNo) updatesMap.set(`${trainNo}_${stopId}`, stopUpdateObj);
|
||||
if (cTrainNo) updatesMap.set(`${cTrainNo}_${stopId}`, stopUpdateObj);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -640,11 +663,13 @@ async function fetchGtfsRealtimeData() {
|
||||
});
|
||||
}
|
||||
|
||||
gtfsRtCache = {
|
||||
timestamp: now,
|
||||
updates: updatesMap,
|
||||
alerts: alertsList
|
||||
};
|
||||
if (updatesMap.size > 0 || alertsList.length > 0) {
|
||||
gtfsRtCache = {
|
||||
timestamp: now,
|
||||
updates: updatesMap,
|
||||
alerts: alertsList
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[sncfApi] Notice: GTFS-RT stream refresh error:', err);
|
||||
}
|
||||
@@ -729,36 +754,35 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
const publicUrl = (typeof process !== 'undefined' && process.env && process.env.PUBLIC_URL) ? process.env.PUBLIC_URL : '';
|
||||
const stationUrl = `${publicUrl}/data/stations/${uic}.json`;
|
||||
|
||||
// Chargement parallèle tolérant aux pannes réseau / CORS (OpenData SNCF, SIRI-Lite Estimated Timetable, SIRI-Lite Situation Exchange, GTFS-RT)
|
||||
const [jsonRes, rtData, siriData, sxData, openDataPlatforms] = await Promise.all([
|
||||
fetch(stationUrl).catch(err => {
|
||||
// Chargement simultané de toutes les données : Horaires + GTFS-RT + SIRI-LITE
|
||||
const fetchStationJson = async () => {
|
||||
let cached = stationJsonCache.get(String(uic));
|
||||
if (cached) return cached;
|
||||
const jsonRes = await fetch(stationUrl).catch(err => {
|
||||
console.warn(`[sncfApi] Notice: fetch station ${uic} error:`, err);
|
||||
return null;
|
||||
}),
|
||||
fetchGtfsRealtimeData().catch(err => {
|
||||
console.warn('[sncfApi] Notice: GTFS-RT error:', err);
|
||||
return { updates: new Map(), alerts: [] };
|
||||
}),
|
||||
fetchSiriLiteData().catch(err => {
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite Timetable error:', err);
|
||||
return { platforms: new Map(), updates: new Map() };
|
||||
}),
|
||||
fetchSituationExchangeData().catch(err => {
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite Situation Exchange error:', err);
|
||||
return { platforms: new Map(), updates: new Map(), alerts: [] };
|
||||
}),
|
||||
fetchOpenDataSncfPlatforms(uic, mode).catch(err => {
|
||||
console.warn('[sncfApi] Notice: OpenData SNCF platform error:', err);
|
||||
return new Map();
|
||||
})
|
||||
]);
|
||||
});
|
||||
if (jsonRes && jsonRes.ok) {
|
||||
cached = await jsonRes.json();
|
||||
stationJsonCache.set(String(uic), cached);
|
||||
return cached;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
if (!jsonRes || !jsonRes.ok) {
|
||||
console.warn(`[sncfApi] Fichier de gare introuvable : ${stationUrl} (statut : ${jsonRes ? jsonRes.status : 'réseau bloqué'})`);
|
||||
const [stationData] = await Promise.all([
|
||||
fetchStationJson(),
|
||||
triggerRealtimeBackgroundRefresh()
|
||||
]);
|
||||
let data = stationData;
|
||||
|
||||
if (!data) {
|
||||
console.warn(`[sncfApi] Fichier de gare introuvable : ${stationUrl}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await jsonRes.json();
|
||||
const rtData = gtfsRtCache || { updates: new Map(), alerts: [] };
|
||||
const siriData = siriLiteCache || { platforms: new Map(), updates: new Map() };
|
||||
const sxData = situationExchangeCache || { platforms: new Map(), updates: new Map(), alerts: [] };
|
||||
const gareRef = findGtfsGare(uic);
|
||||
const targetStationNom = (gareRef && gareRef.nom) || (data && data.nom) || '';
|
||||
const rawList = mode === 'arrivees' ? data.arrivees : data.departs;
|
||||
@@ -850,11 +874,13 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
(uicClean && cNum && sxData.platforms.get(`${uicClean}_${cNum}`)) ||
|
||||
(cNum && cName && sxData.platforms.get(`${cNum}_${cName}`)) ||
|
||||
(rawNum && cName && sxData.platforms.get(`${rawNum}_${cName}`)) ||
|
||||
// 3. OpenData SNCF direct
|
||||
(cNum && uicClean && openDataPlatforms.get(`${cNum}_${uicClean}`)) ||
|
||||
(rawNum && uicClean && openDataPlatforms.get(`${rawNum}_${uicClean}`)) ||
|
||||
(cNum && cName && openDataPlatforms.get(`${cNum}_${cName}`)) ||
|
||||
(cNum && cDest && openDataPlatforms.get(`${cNum}_${cDest}`)) ||
|
||||
// 3. Fallback sur le numéro de train seul si connu dans SIRI-Lite / Situation Exchange
|
||||
(cNum && siriData.platforms.get(cNum)) ||
|
||||
(rawNum && siriData.platforms.get(rawNum)) ||
|
||||
(cNum && siriData.updates.get(cNum)?.platform) ||
|
||||
(rawNum && siriData.updates.get(rawNum)?.platform) ||
|
||||
(cNum && sxData.updates.get(cNum)?.platform) ||
|
||||
(rawNum && sxData.updates.get(rawNum)?.platform) ||
|
||||
''
|
||||
);
|
||||
};
|
||||
@@ -864,8 +890,19 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
const rawTime = mode === 'arrivees' ? (t.arr || t.dep) : (t.dep || t.arr);
|
||||
const timeHM = parseTimeHM(rawTime) || 0;
|
||||
|
||||
const stopUicClean = String(uic || '').replace(/\D/g, '');
|
||||
const stopUic7 = (stopUicClean.length === 8 && stopUicClean.startsWith('87')) ? stopUicClean.slice(2) : '';
|
||||
const cTrainNum = cleanNum(t.num);
|
||||
|
||||
// Recherche de mise à jour GTFS-RT, SIRI-Lite Timetable ou SIRI-Lite Situation Exchange
|
||||
const rt = !isTomorrow ? (rtData.updates.get(t.trip_id) || (t.num ? rtData.updates.get(t.num) : null) || (cleanNum(t.num) ? rtData.updates.get(cleanNum(t.num)) : null)) : null;
|
||||
const rt = !isTomorrow ? (
|
||||
(stopUicClean && cTrainNum && rtData.updates.get(`${cTrainNum}_${stopUicClean}`)) ||
|
||||
(stopUicClean && t.num && rtData.updates.get(`${t.num}_${stopUicClean}`)) ||
|
||||
(stopUic7 && cTrainNum && rtData.updates.get(`${cTrainNum}_${stopUic7}`)) ||
|
||||
rtData.updates.get(t.trip_id) ||
|
||||
(t.num ? rtData.updates.get(t.num) : null) ||
|
||||
(cTrainNum ? rtData.updates.get(cTrainNum) : null)
|
||||
) : null;
|
||||
const siri = !isTomorrow ? (siriData.updates.get(t.num) || (cleanNum(t.num) ? siriData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? siriData.updates.get(t.trip_id) : null)) : null;
|
||||
const sx = !isTomorrow ? (sxData.updates.get(t.num) || (cleanNum(t.num) ? sxData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? sxData.updates.get(t.trip_id) : null)) : null;
|
||||
|
||||
@@ -896,7 +933,13 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
statusLabel = 'supprimé';
|
||||
etat = 'supprime';
|
||||
} else if (delayMinutes > 0) {
|
||||
statusLabel = `retardé +${delayMinutes}min.`;
|
||||
if (delayMinutes >= 60) {
|
||||
const h = Math.floor(delayMinutes / 60);
|
||||
const rem = delayMinutes % 60;
|
||||
statusLabel = rem > 0 ? `retardé +${h}h${String(rem).padStart(2, '0')}` : `retardé +${h}h`;
|
||||
} else {
|
||||
statusLabel = `retardé +${delayMinutes}min.`;
|
||||
}
|
||||
etat = 'retarde';
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,935 @@
|
||||
/**
|
||||
* Moteur de classification et de corrélation des types de trains et opérateurs ferroviaires
|
||||
* ferrovia-infogare - Compatible afficheurs classiques (Hall & Quai)
|
||||
*
|
||||
* Hiérarchie de décision :
|
||||
* 1. Codes d'exploitation GTFS officiels (trip_id SNCF Open Data)
|
||||
* 2. Détection des services spécifiques (Lyria, Ouigo Classique, Intercités de Nuit, etc.)
|
||||
* 3. Plan de Numérotation officiel SNCF (séries régionales TER hermétiques par blocs de numéros)
|
||||
* 4. Détection géographique par score d'arrêts et de terminus (sans faux-positifs)
|
||||
* 5. Re-routage vers TER générique ou SNCF Voyageurs en dernier recours
|
||||
*/
|
||||
|
||||
// Normalisation des chaînes (minuscules, sans accents, espaces normalisés)
|
||||
export const normalize = (str) => {
|
||||
if (!str) return '';
|
||||
return String(str)
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
};
|
||||
|
||||
// Extraction des chiffres d'un numéro de train (sans zéros initiaux)
|
||||
export const extractTrainDigits = (num) => {
|
||||
if (!num) return '';
|
||||
const digits = String(num).replace(/\D/g, '');
|
||||
return digits.replace(/^0+/, '') || digits;
|
||||
};
|
||||
|
||||
// Services routiers par autocar (3xxxx / 3xxxxx)
|
||||
export const isCarTrainNumber = (num) => {
|
||||
const d = String(num || '').replace(/\D/g, '');
|
||||
return (d.length === 5 && d.startsWith('3')) || (d.length === 6 && d.startsWith('3'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Référentiel des marques, types de trains et logos (monochrome Hall & couleur Quai)
|
||||
*/
|
||||
export const BRANDS = {
|
||||
INOUi: {
|
||||
id: 'inoui',
|
||||
typeLabel: 'TGV inOUI',
|
||||
quaiLabel: 'TGV INOUI',
|
||||
operator: 'SNCF Voyageurs',
|
||||
category: 'TGV',
|
||||
isTgv: true,
|
||||
hallLogo: '/type/logo-inoui.svg',
|
||||
quaiLogo: '/type/quais/logo-inoui-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
TGV_GENERIC: {
|
||||
id: 'tgv',
|
||||
typeLabel: 'TGV',
|
||||
quaiLabel: 'TGV',
|
||||
operator: 'SNCF Voyageurs',
|
||||
category: 'TGV',
|
||||
isTgv: true,
|
||||
hallLogo: '/type/logo-tgv.svg',
|
||||
quaiLogo: '/type/quais/logo-tgv-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
LYRIA: {
|
||||
id: 'lyria',
|
||||
typeLabel: 'TGV Lyria',
|
||||
quaiLabel: 'TGV Lyria',
|
||||
operator: 'Lyria SAS',
|
||||
category: 'TGV',
|
||||
isTgv: true,
|
||||
hallLogo: '/type/logo-lyria.svg',
|
||||
quaiLogo: '/type/quais/logo-lyria-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
OUIGO_CLASSIQUE: {
|
||||
id: 'ouigo-classique',
|
||||
typeLabel: 'OUIGO Train Classique',
|
||||
quaiLabel: 'OUIGO Classique',
|
||||
operator: 'SNCF Voyageurs',
|
||||
category: 'GRANDES_LIGNES',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-ouigo-classique.svg',
|
||||
quaiLogo: '/type/quais/logo-ouigo-classique-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
OUIGO_GV: {
|
||||
id: 'ouigo',
|
||||
typeLabel: 'OUIGO',
|
||||
quaiLabel: 'OUIGO',
|
||||
operator: 'SNCF Voyageurs',
|
||||
category: 'TGV',
|
||||
isTgv: true,
|
||||
hallLogo: '/type/logo-ouigo.svg',
|
||||
quaiLogo: '/type/quais/logo-ouigo-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
INTERCITES_NUIT: {
|
||||
id: 'intercites-nuit',
|
||||
typeLabel: 'INTERCITÉS DE NUIT',
|
||||
quaiLabel: 'Intercités de Nuit',
|
||||
operator: 'SNCF Voyageurs',
|
||||
category: 'GRANDES_LIGNES',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-lunea.svg',
|
||||
quaiLogo: '/type/quais/logo-lunea-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
INTERCITES: {
|
||||
id: 'intercites',
|
||||
typeLabel: 'INTERCITÉS',
|
||||
quaiLabel: 'INTERCITÉS',
|
||||
operator: 'SNCF Voyageurs',
|
||||
category: 'GRANDES_LIGNES',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-intercites.svg',
|
||||
quaiLogo: '/type/quais/logo-intercite-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
EUROSTAR: {
|
||||
id: 'eurostar',
|
||||
typeLabel: 'EUROSTAR',
|
||||
quaiLabel: 'Eurostar',
|
||||
operator: 'Eurostar Group',
|
||||
category: 'INTERNATIONAL',
|
||||
isTgv: true,
|
||||
hallLogo: '/type/logo-eurostar.svg',
|
||||
quaiLogo: '/type/quais/logo-eurostar-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
THALYS: {
|
||||
id: 'thalys',
|
||||
typeLabel: 'THALYS',
|
||||
quaiLabel: 'Thalys',
|
||||
operator: 'Eurostar Group',
|
||||
category: 'INTERNATIONAL',
|
||||
isTgv: true,
|
||||
hallLogo: '/type/logo-thalys.svg',
|
||||
quaiLogo: '/type/quais/logo-thalys-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
ICE: {
|
||||
id: 'ice',
|
||||
typeLabel: 'ICE',
|
||||
quaiLabel: 'ICE',
|
||||
operator: 'DB / SNCF',
|
||||
category: 'INTERNATIONAL',
|
||||
isTgv: true,
|
||||
hallLogo: '/type/logo-ice.svg',
|
||||
quaiLogo: '/type/quais/logo-ice-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
DB: {
|
||||
id: 'db',
|
||||
typeLabel: 'DB',
|
||||
quaiLabel: 'DB',
|
||||
operator: 'Deutsche Bahn',
|
||||
category: 'INTERNATIONAL',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-db.svg',
|
||||
quaiLogo: '/type/quais/logo-db-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
FRECCIAROSSA: {
|
||||
id: 'frecciarossa',
|
||||
typeLabel: 'FRECCIAROSSA',
|
||||
quaiLabel: 'Frecciarossa',
|
||||
operator: 'Trenitalia France',
|
||||
category: 'INTERNATIONAL',
|
||||
isTgv: true,
|
||||
hallLogo: '/type/logo-frecciarossa.svg',
|
||||
quaiLogo: '/type/quais/logo-frecciarossa-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
RENFE_AVE: {
|
||||
id: 'renfe-ave',
|
||||
typeLabel: 'Renfe AVE',
|
||||
quaiLabel: 'Renfe AVE',
|
||||
operator: 'Renfe',
|
||||
category: 'INTERNATIONAL',
|
||||
isTgv: true,
|
||||
hallLogo: '/type/logo-renfeave.svg',
|
||||
quaiLogo: '/type/quais/logo-renfeave-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
AUTOCAR: {
|
||||
id: 'autocar',
|
||||
typeLabel: 'AUTOCAR',
|
||||
quaiLabel: 'Autocar',
|
||||
operator: 'SNCF Voyageurs',
|
||||
category: 'AUTOCAR',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/track-car.svg',
|
||||
quaiLogo: '/type/track-car.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
TRAMTRAIN: {
|
||||
id: 'tramtrain',
|
||||
typeLabel: 'Tram-train',
|
||||
quaiLabel: 'Tram-train',
|
||||
operator: 'TER',
|
||||
category: 'TER',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-tramtrain.svg',
|
||||
quaiLogo: '/type/quais/logo-tramtrain-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
MOBIGO: {
|
||||
id: 'mobigo',
|
||||
typeLabel: 'MOBIGO',
|
||||
quaiLabel: 'Mobigo',
|
||||
operator: 'Région Bourgogne-Franche-Comté',
|
||||
category: 'TER',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-mobigo.svg',
|
||||
quaiLogo: '/type/quais/logo-mobigo-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
ZOU: {
|
||||
id: 'zou',
|
||||
typeLabel: 'ZOU !',
|
||||
quaiLabel: 'ZOU!',
|
||||
operator: 'Région Sud / PACA',
|
||||
category: 'TER',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-zou.svg',
|
||||
quaiLogo: '/type/quais/logo-zou-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
FLUO: {
|
||||
id: 'fluo',
|
||||
typeLabel: 'FLUO',
|
||||
quaiLabel: 'TER Fluo',
|
||||
operator: 'Région Grand Est',
|
||||
category: 'TER',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-fluo.svg',
|
||||
quaiLogo: '/type/quais/logo-fluo-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
ALEOP: {
|
||||
id: 'aleop',
|
||||
typeLabel: 'ALÉOP',
|
||||
quaiLabel: 'Aléop',
|
||||
operator: 'Région Pays de la Loire',
|
||||
category: 'TER',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-aleop.svg',
|
||||
quaiLogo: '/type/quais/logo-aleop-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
BREIZHGO: {
|
||||
id: 'breizhgo',
|
||||
typeLabel: 'BREIZHGO',
|
||||
quaiLabel: 'BreizhGo',
|
||||
operator: 'Région Bretagne',
|
||||
category: 'TER',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-breizhgo.svg',
|
||||
quaiLogo: '/type/quais/logo-breizhgo-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
NOMAD: {
|
||||
id: 'nomad',
|
||||
typeLabel: 'NOMAD',
|
||||
quaiLabel: 'Nomad',
|
||||
operator: 'Région Normandie',
|
||||
category: 'TER',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-nomad.svg',
|
||||
quaiLogo: '/type/quais/logo-nomad-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
REMI: {
|
||||
id: 'remi',
|
||||
typeLabel: 'RÉMI',
|
||||
quaiLabel: 'Rémi',
|
||||
operator: 'Région Centre-Val de Loire',
|
||||
category: 'TER',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-remi.svg',
|
||||
quaiLogo: '/type/quais/logo-remi-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
LIO: {
|
||||
id: 'lio',
|
||||
typeLabel: 'LIÒ',
|
||||
quaiLabel: 'liO',
|
||||
operator: 'Région Occitanie',
|
||||
category: 'TER',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-lio.svg',
|
||||
quaiLogo: '/type/quais/logo-lio-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
HAUTS_DE_FRANCE: {
|
||||
id: 'hautsdefrance',
|
||||
typeLabel: 'HAUTS-DE-FRANCE',
|
||||
quaiLabel: 'Hauts-de-France',
|
||||
operator: 'Région Hauts-de-France',
|
||||
category: 'TER',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-hautsdefrance.svg',
|
||||
quaiLogo: '/type/quais/logo-hautsdefrance-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
AUVERGNE_RHONE_ALPES: {
|
||||
id: 'auvergerhonealpes',
|
||||
typeLabel: 'AUVERGNE-RHÔNE-ALPES',
|
||||
quaiLabel: 'Auvergne-Rhône-Alpes',
|
||||
operator: 'Région Auvergne-Rhône-Alpes',
|
||||
category: 'TER',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-auvergerhonealpes.svg',
|
||||
quaiLogo: '/type/quais/logo-auvergerhonealpes-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
NOUVELLE_AQUITAINE: {
|
||||
id: 'nouvelleaquitaine',
|
||||
typeLabel: 'NOUVELLE-AQUITAINE',
|
||||
quaiLabel: 'Nouvelle-Aquitaine',
|
||||
operator: 'Région Nouvelle-Aquitaine',
|
||||
category: 'TER',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-teraquitaine.svg',
|
||||
quaiLogo: '/type/quais/logo-nouvelleaquitaine-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
TER_GENERIC: {
|
||||
id: 'ter',
|
||||
typeLabel: 'TER',
|
||||
quaiLabel: 'TER',
|
||||
operator: 'SNCF Voyageurs',
|
||||
category: 'TER',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-ter.svg',
|
||||
quaiLogo: '/type/quais/logo-ter-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
SBB: {
|
||||
id: 'sbb',
|
||||
typeLabel: 'SBB',
|
||||
quaiLabel: 'SBB',
|
||||
operator: 'CFF / SBB',
|
||||
category: 'INTERNATIONAL',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-sbb.svg',
|
||||
quaiLogo: '/type/quais/logo-sbb-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
SNCB: {
|
||||
id: 'sncb',
|
||||
typeLabel: 'SNCB',
|
||||
quaiLabel: 'SNCB',
|
||||
operator: 'SNCB / NMBS',
|
||||
category: 'INTERNATIONAL',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-sncb.svg',
|
||||
quaiLogo: '/type/quais/logo-sncb-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
CFL: {
|
||||
id: 'cfl',
|
||||
typeLabel: 'CFL',
|
||||
quaiLabel: 'CFL',
|
||||
operator: 'CFL',
|
||||
category: 'INTERNATIONAL',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-cfl.svg',
|
||||
quaiLogo: '/type/quais/logo-cfl-color.svg',
|
||||
showVoyageursText: false
|
||||
},
|
||||
SNCF_GENERIC: {
|
||||
id: 'sncf',
|
||||
typeLabel: 'SNCF',
|
||||
quaiLabel: 'SNCF',
|
||||
operator: 'SNCF Voyageurs',
|
||||
category: 'SNCF',
|
||||
isTgv: false,
|
||||
hallLogo: '/type/logo-sncf.svg',
|
||||
quaiLogo: '/type/quais/logo-sncf-color.svg',
|
||||
showVoyageursText: true
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Plan de Numérotation officiel SNCF (Séries régionales TER)
|
||||
* Chaque autorité organisatrice régionale dispose de tranches numériques réservées.
|
||||
*/
|
||||
function getRegionByTrainNumber(numStr) {
|
||||
const clean = String(numStr || '').replace(/\D/g, '');
|
||||
if (!clean || clean.length < 3) return null;
|
||||
|
||||
// 1. Occitanie (liO) : 869xxx, 870xxx, 871xxx, 872xxx, 876xxx, 877xxx (et 5 chiffres : 869xx, 870xx, 876xx, 877xx)
|
||||
if (
|
||||
clean.startsWith('869') ||
|
||||
clean.startsWith('870') ||
|
||||
clean.startsWith('871') ||
|
||||
clean.startsWith('872') ||
|
||||
clean.startsWith('876') ||
|
||||
clean.startsWith('877')
|
||||
) {
|
||||
return BRANDS.LIO;
|
||||
}
|
||||
|
||||
// 2. Centre-Val de Loire (Rémi) : 860xxx, 861xxx, 862xxx, 863xxx (et 860xx à 863xx)
|
||||
if (
|
||||
clean.startsWith('860') ||
|
||||
clean.startsWith('861') ||
|
||||
clean.startsWith('862') ||
|
||||
clean.startsWith('863')
|
||||
) {
|
||||
return BRANDS.REMI;
|
||||
}
|
||||
|
||||
// 3. Nouvelle-Aquitaine : 864xxx, 865xxx, 866xxx, 867xxx, 868xxx (et 864xx à 868xx)
|
||||
if (
|
||||
clean.startsWith('864') ||
|
||||
clean.startsWith('865') ||
|
||||
clean.startsWith('866') ||
|
||||
clean.startsWith('867') ||
|
||||
clean.startsWith('868')
|
||||
) {
|
||||
return BRANDS.NOUVELLE_AQUITAINE;
|
||||
}
|
||||
|
||||
// 4. Grand Est (Fluo) : 83xxxx (et 83xxx)
|
||||
if (clean.startsWith('83')) {
|
||||
return BRANDS.FLUO;
|
||||
}
|
||||
|
||||
// 5. Bourgogne-Franche-Comté (Mobigo) : 89xxxx, 840xxx, 841xxx (et 89xxx, 840xx, 841xx)
|
||||
if (
|
||||
clean.startsWith('89') ||
|
||||
clean.startsWith('840') ||
|
||||
clean.startsWith('841')
|
||||
) {
|
||||
return BRANDS.MOBIGO;
|
||||
}
|
||||
|
||||
// 6. Hauts-de-France : 842xxx à 849xxx (et 842xx à 849xx)
|
||||
if (['842', '843', '844', '845', '846', '847', '848', '849'].some(p => clean.startsWith(p))) {
|
||||
return BRANDS.HAUTS_DE_FRANCE;
|
||||
}
|
||||
|
||||
// 7. Normandie (Nomad) : 852xxx, 853xxx (et 852xx, 853xx)
|
||||
if (clean.startsWith('852') || clean.startsWith('853')) {
|
||||
return BRANDS.NOMAD;
|
||||
}
|
||||
|
||||
// 8. Bretagne (BreizhGo) : 854xxx, 855xxx, 856xxx (et 854xx à 856xx)
|
||||
if (clean.startsWith('854') || clean.startsWith('855') || clean.startsWith('856')) {
|
||||
return BRANDS.BREIZHGO;
|
||||
}
|
||||
|
||||
// 9. Pays de la Loire (Aléop) : 857xxx, 858xxx, 859xxx (et 857xx à 859xx)
|
||||
if (clean.startsWith('857') || clean.startsWith('858') || clean.startsWith('859')) {
|
||||
return BRANDS.ALEOP;
|
||||
}
|
||||
|
||||
// 10. Auvergne-Rhône-Alpes : 873xxx à 875xxx, 883xxx, 884xxx, 889xxx
|
||||
if (['873', '874', '875', '883', '884', '889'].some(p => clean.startsWith(p))) {
|
||||
return BRANDS.AUVERGNE_RHONE_ALPES;
|
||||
}
|
||||
|
||||
// 11. PACA / Sud (Zou !) : 880xxx, 881xxx, 882xxx, 885xxx, 886xxx, 887xxx, 888xxx
|
||||
if (['880', '881', '882', '885', '886', '887', '888'].some(p => clean.startsWith(p))) {
|
||||
return BRANDS.ZOU;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gares de référence par région ferroviaire pour analyse de parcours
|
||||
* (Seuls des noms de villes ou gares réelles sans mots génériques comme 'centre' ou 'est')
|
||||
*/
|
||||
const REGIONAL_STATIONS = {
|
||||
lio: [
|
||||
'perpignan', 'narbonne', 'toulouse', 'nimes', 'beziers', 'sete', 'montpellier',
|
||||
'cerbere', 'portbou', 'villefranche', 'carcassonne', 'agde', 'elne', 'argeles',
|
||||
'collioure', 'port vendres', 'banyuls', 'rodez', 'albi', 'tarbes', 'montauban',
|
||||
'castres', 'lourdes', 'mende', 'foix', 'latour de carol', 'ax les thermes',
|
||||
'castelnaudary', 'frontignan', 'port la nouvelle', 'leucate', 'salses', 'rivesaltes'
|
||||
],
|
||||
remi: [
|
||||
'orleans', 'tours', 'bourges', 'blois', 'chartres', 'chateauroux', 'vierzon',
|
||||
'vendome', 'saint pierre des corps', 'amboise', 'nogent le rotrou', 'salbris',
|
||||
'chinon', 'loches', 'issoudun', 'romorantin', 'montargis', 'gien'
|
||||
],
|
||||
mobigo: [
|
||||
'dijon', 'besancon', 'belfort', 'auxerre', 'nevers', 'chalon sur saone',
|
||||
'montbeliard', 'macon', 'sens', 'dole', 'creusot', 'paray le monial',
|
||||
'beaune', 'lons le saunier', 'pontarlier', 'vesoul'
|
||||
],
|
||||
zou: [
|
||||
'marseille', 'nice', 'toulon', 'cannes', 'antibes', 'avignon', 'grasse',
|
||||
'aubagne', 'miramas', 'arles', 'hyeres', 'menton', 'cagnes sur mer',
|
||||
'frejus', 'saint raphael', 'la ciotat', 'gap', 'briancon', 'manosque'
|
||||
],
|
||||
fluo: [
|
||||
'strasbourg', 'nancy', 'metz', 'mulhouse', 'colmar', 'reims', 'troyes',
|
||||
'epinal', 'thionville', 'charleville', 'saint die', 'haguenau',
|
||||
'sarreguemines', 'bar le duc', 'luneville', 'sedan', 'forbach', 'selestat'
|
||||
],
|
||||
aleop: [
|
||||
'nantes', 'angers', 'le mans', 'saint nazaire', 'cholet', 'la roche sur yon',
|
||||
'saumur', 'laval', 'les sables d olonne', 'ancenis', 'clisson', 'chateaubriant'
|
||||
],
|
||||
breizhgo: [
|
||||
'rennes', 'brest', 'quimper', 'saint malo', 'vannes', 'lorient',
|
||||
'saint brieuc', 'morlaix', 'lannion', 'vitre', 'redon', 'guingamp'
|
||||
],
|
||||
nomad: [
|
||||
'rouen', 'caen', 'le havre', 'cherbourg', 'evreux', 'trouville deauville',
|
||||
'dieppe', 'lisieux', 'granville', 'alencon', 'bernay', 'bayeux'
|
||||
],
|
||||
hautsdefrance: [
|
||||
'lille', 'amiens', 'calais', 'dunkerque', 'valenciennes', 'arras',
|
||||
'boulogne sur mer', 'douai', 'creil', 'saint quentin', 'lens', 'bethune',
|
||||
'compiegne', 'cambrai', 'tourcoing', 'roubaix'
|
||||
],
|
||||
auvergerhonealpes: [
|
||||
'lyon', 'grenoble', 'chambery', 'annecy', 'valence', 'clermont ferrand',
|
||||
'saint etienne', 'bourg en bresse', 'roanne', 'vichy', 'aix les bains',
|
||||
'annemasse', 'amberieu', 'riom', 'moulins'
|
||||
],
|
||||
nouvelleaquitaine: [
|
||||
'bordeaux', 'limoges', 'poitiers', 'la rochelle', 'bayonne', 'pau',
|
||||
'angouleme', 'perigueux', 'agen', 'niort', 'brive la gaillarde', 'dax',
|
||||
'hendaye', 'saintes', 'arcachon'
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Calcule la région la plus probable selon les arrêts desservis et le terminus
|
||||
*/
|
||||
function detectRegionByRouteScore(routeText) {
|
||||
if (!routeText) return null;
|
||||
const normalized = ` ${normalize(routeText)} `;
|
||||
|
||||
let bestRegion = null;
|
||||
let maxScore = 0;
|
||||
|
||||
for (const [regionKey, stations] of Object.entries(REGIONAL_STATIONS)) {
|
||||
let score = 0;
|
||||
for (const station of stations) {
|
||||
const stationNorm = ` ${normalize(station)} `;
|
||||
if (normalized.includes(stationNorm)) {
|
||||
score++;
|
||||
}
|
||||
}
|
||||
if (score > maxScore) {
|
||||
maxScore = score;
|
||||
bestRegion = regionKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxScore > 0 && bestRegion) {
|
||||
switch (bestRegion) {
|
||||
case 'lio': return BRANDS.LIO;
|
||||
case 'remi': return BRANDS.REMI;
|
||||
case 'mobigo': return BRANDS.MOBIGO;
|
||||
case 'zou': return BRANDS.ZOU;
|
||||
case 'fluo': return BRANDS.FLUO;
|
||||
case 'aleop': return BRANDS.ALEOP;
|
||||
case 'breizhgo': return BRANDS.BREIZHGO;
|
||||
case 'nomad': return BRANDS.NOMAD;
|
||||
case 'hautsdefrance': return BRANDS.HAUTS_DE_FRANCE;
|
||||
case 'auvergerhonealpes': return BRANDS.AUVERGNE_RHONE_ALPES;
|
||||
case 'nouvelleaquitaine': return BRANDS.NOUVELLE_AQUITAINE;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonction centrale de classification d'un train à partir de ses données SNCF
|
||||
* @param {Object|string} trainOrType - Objet horaire ou type brut
|
||||
* @param {string} [operatorParam] - Opérateur ou transporteur optionnel
|
||||
* @param {string} [numParam] - Numéro de train optionnel
|
||||
* @returns {Object} Résultat complet de classification
|
||||
*/
|
||||
export function classifyTrain(trainOrType, operatorParam = '', numParam = '') {
|
||||
let horaire = null;
|
||||
let rawType = '';
|
||||
let opRaw = operatorParam || '';
|
||||
let numRaw = numParam || '';
|
||||
|
||||
if (trainOrType && typeof trainOrType === 'object') {
|
||||
horaire = trainOrType;
|
||||
rawType = horaire.type_train || horaire.type || horaire.train_type || horaire.commercial_mode || horaire.mode || '';
|
||||
opRaw = horaire.operator || horaire.op || horaire.transporteur || horaire.carrier || opRaw;
|
||||
numRaw = horaire.numero_train || horaire.numero || horaire.number || horaire.num || horaire.trip_short_name || numRaw;
|
||||
} else {
|
||||
rawType = String(trainOrType || '');
|
||||
}
|
||||
|
||||
const numTrain = extractTrainDigits(numRaw);
|
||||
const numInt = parseInt(numTrain, 10) || 0;
|
||||
const tripId = horaire ? String(horaire.trip_id || '') : '';
|
||||
const isSubstitution = Boolean(horaire && (horaire.is_substitution || horaire.is_sub));
|
||||
|
||||
const origName = horaire ? (horaire.gare_depart_nom || horaire.orig || horaire.origin || '') : '';
|
||||
const destName = horaire ? (horaire.gare_arrivee_nom || horaire.dest || horaire.destination || '') : '';
|
||||
const stopsText = Array.isArray(horaire?.stops) ? horaire.stops.map(s => (s.nom || s.gare_nom || s.name || '')).join(' ') : '';
|
||||
const fullRouteText = `${origName} ${destName} ${stopsText}`;
|
||||
const routeNormalized = normalize(fullRouteText);
|
||||
|
||||
const typeNorm = normalize(rawType);
|
||||
const opNorm = normalize(opRaw);
|
||||
const combined = `${typeNorm} ${opNorm}`.trim();
|
||||
|
||||
// Helper pour créer le résultat enrichi
|
||||
const buildResult = (brand, customProps = {}) => ({
|
||||
...brand,
|
||||
numTrain,
|
||||
rawType,
|
||||
rawOperator: opRaw,
|
||||
...customProps
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// 1. SERVICES AUTOCAR / CAR TER / BUS DE SUBSTITUTION
|
||||
// =========================================================================
|
||||
if (
|
||||
isSubstitution ||
|
||||
isCarTrainNumber(numTrain) ||
|
||||
combined.includes('autocar') ||
|
||||
combined.includes('carter') ||
|
||||
combined.includes('substitution') ||
|
||||
combined === 'car' ||
|
||||
combined === 'bus'
|
||||
) {
|
||||
return buildResult(BRANDS.AUTOCAR);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. TGV LYRIA (Paris - Suisse : Genève, Lausanne, Zurich, Bâle)
|
||||
// =========================================================================
|
||||
if (
|
||||
combined.includes('lyria') ||
|
||||
tripId.includes(':LYR:FR') ||
|
||||
(numInt >= 9200 && numInt <= 9299) ||
|
||||
(numInt >= 9750 && numInt <= 9799)
|
||||
) {
|
||||
return buildResult(BRANDS.LYRIA);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 3. OUIGO TRAIN CLASSIQUE (4000-4399 ou 8040-8099, Paris Austerlitz/Bercy)
|
||||
// =========================================================================
|
||||
const isOuigoClassique = (
|
||||
combined.includes('ouigoclassique') ||
|
||||
combined.includes('trainclassique') ||
|
||||
combined.includes('classique') ||
|
||||
(combined.includes('ouigo') && ((numInt >= 4000 && numInt <= 4399) || (numInt >= 8040 && numInt <= 8099))) ||
|
||||
(tripId.includes(':OGO:FR') && ((numInt >= 4000 && numInt <= 4399) || (numInt >= 8040 && numInt <= 8099)))
|
||||
);
|
||||
if (isOuigoClassique) {
|
||||
return buildResult(BRANDS.OUIGO_CLASSIQUE);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 4. OUIGO (GRANDE VITESSE)
|
||||
// =========================================================================
|
||||
if (
|
||||
combined.includes('ouigo') ||
|
||||
tripId.includes(':OGO:FR') ||
|
||||
(numInt >= 7600 && numInt <= 7999)
|
||||
) {
|
||||
return buildResult(BRANDS.OUIGO_GV);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 5. INTERCITÉS DE NUIT (Lunéa)
|
||||
// =========================================================================
|
||||
if (
|
||||
combined.includes('denuit') ||
|
||||
combined.includes('nuit') ||
|
||||
combined.includes('lunea') ||
|
||||
tripId.includes(':ICN:FR') ||
|
||||
(numInt >= 3690 && numInt <= 3799) ||
|
||||
(numInt >= 3950 && numInt <= 3999) ||
|
||||
(numInt >= 5700 && numInt <= 5799)
|
||||
) {
|
||||
return buildResult(BRANDS.INTERCITES_NUIT);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 6. INTERCITÉS (Trains d'Équilibre du Territoire de jour)
|
||||
// =========================================================================
|
||||
if (
|
||||
combined.includes('intercites') ||
|
||||
combined === 'ic' ||
|
||||
combined.startsWith('ic ') ||
|
||||
tripId.includes(':IC:FR') ||
|
||||
(numInt >= 3000 && numInt <= 3949) ||
|
||||
(numInt >= 4400 && numInt <= 4999)
|
||||
) {
|
||||
return buildResult(BRANDS.INTERCITES);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 7. EUROSTAR & THALYS
|
||||
// =========================================================================
|
||||
const isEurostarOrThalys = (
|
||||
combined.includes('eurostar') ||
|
||||
combined.includes('thalys') ||
|
||||
(tripId.includes(':TRN:FR') && (routeNormalized.includes('bruxelles') || routeNormalized.includes('london') || routeNormalized.includes('amsterdam') || routeNormalized.includes('koln'))) ||
|
||||
((numInt >= 60 && numInt <= 99) && (routeNormalized.includes('bruxelles') || routeNormalized.includes('nord'))) ||
|
||||
(numInt >= 9000 && numInt <= 9499 && (routeNormalized.includes('bruxelles') || routeNormalized.includes('london') || routeNormalized.includes('amsterdam')))
|
||||
);
|
||||
if (isEurostarOrThalys) {
|
||||
if (combined.includes('thalys')) {
|
||||
return buildResult(BRANDS.THALYS);
|
||||
}
|
||||
return buildResult(BRANDS.EUROSTAR);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 8. FRECCIAROSSA / TRENITALIA (Paris Lyon - Lyon - Milan)
|
||||
// =========================================================================
|
||||
if (
|
||||
combined.includes('frecciarossa') ||
|
||||
combined.includes('freccia') ||
|
||||
combined.includes('trenitalia') ||
|
||||
(numInt >= 6640 && numInt <= 6670)
|
||||
) {
|
||||
return buildResult(BRANDS.FRECCIAROSSA);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 9. RENFE AVE (Marseille / Lyon - Madrid / Barcelone)
|
||||
// =========================================================================
|
||||
if (
|
||||
combined.includes('renfe') ||
|
||||
combined.includes('ave') ||
|
||||
[9724, 9725, 9731, 9732].includes(numInt)
|
||||
) {
|
||||
return buildResult(BRANDS.RENFE_AVE);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 10. ICE / DB (Paris Est - Allemagne)
|
||||
// =========================================================================
|
||||
if (
|
||||
combined.includes('ice') ||
|
||||
combined === 'db' ||
|
||||
tripId.includes(':ICE:FR') ||
|
||||
(numInt >= 9550 && numInt <= 9599)
|
||||
) {
|
||||
return buildResult(BRANDS.ICE);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 11. TGV INOUI & TGV GÉNÉRIQUE
|
||||
// =========================================================================
|
||||
if (
|
||||
combined.includes('inoui') ||
|
||||
combined.includes('tgvinoui') ||
|
||||
tripId.includes(':OUI:FR') ||
|
||||
combined.includes('tgv')
|
||||
) {
|
||||
return buildResult(BRANDS.INOUi);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 12. TRAM-TRAIN
|
||||
// =========================================================================
|
||||
if (
|
||||
combined.includes('tramtrain') ||
|
||||
combined.includes('tram-train') ||
|
||||
tripId.includes(':TT:FR')
|
||||
) {
|
||||
return buildResult(BRANDS.TRAMTRAIN);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 13. TERGV
|
||||
// =========================================================================
|
||||
if (combined.includes('tergv') || combined.includes('ter-gv')) {
|
||||
return buildResult(BRANDS.HAUTS_DE_FRANCE, { typeLabel: 'TERGV', quaiLabel: 'TERGV' });
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 14. OPÉRATEURS SUISSE, BELGE, LUXEMBOURGEOIS
|
||||
// =========================================================================
|
||||
if (combined.includes('sbb') || combined.includes('cff') || combined.includes('ffs')) {
|
||||
return buildResult(BRANDS.SBB);
|
||||
}
|
||||
if (combined.includes('sncb') || combined.includes('nmbs')) {
|
||||
return buildResult(BRANDS.SNCB);
|
||||
}
|
||||
if (combined.includes('cfl')) {
|
||||
return buildResult(BRANDS.CFL);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 15. TER RÉGIONAUX - DÉTECTION ROBUSTE PAR PALIER
|
||||
// =========================================================================
|
||||
|
||||
// Palier A : Mots-clés régionaux explicites dans l'opérateur ou type
|
||||
if (combined.includes('lio') || combined.includes('occitanie') || combined.includes('languedoc') || combined.includes('midipyrenees')) {
|
||||
return buildResult(BRANDS.LIO);
|
||||
}
|
||||
if (combined.includes('mobigo') || combined.includes('bourgogne') || combined.includes('franchecomte')) {
|
||||
return buildResult(BRANDS.MOBIGO);
|
||||
}
|
||||
if (combined.includes('zou') || combined.includes('paca') || combined.includes('ler')) {
|
||||
return buildResult(BRANDS.ZOU);
|
||||
}
|
||||
if (combined.includes('fluo') || combined.includes('grandest') || combined.includes('alsace') || combined.includes('lorraine') || combined.includes('metrolor')) {
|
||||
return buildResult(BRANDS.FLUO);
|
||||
}
|
||||
if (combined.includes('aleop') || combined.includes('paysdelaloire')) {
|
||||
return buildResult(BRANDS.ALEOP);
|
||||
}
|
||||
if (combined.includes('breizh') || combined.includes('bretagne')) {
|
||||
return buildResult(BRANDS.BREIZHGO);
|
||||
}
|
||||
if (combined.includes('nomad') || combined.includes('normandie')) {
|
||||
return buildResult(BRANDS.NOMAD);
|
||||
}
|
||||
if (combined.includes('remi') || combined.includes('centrevaldeloire')) {
|
||||
return buildResult(BRANDS.REMI);
|
||||
}
|
||||
if (combined.includes('hautsdefrance') || combined.includes('nordpasdecalais') || combined.includes('picardie')) {
|
||||
return buildResult(BRANDS.HAUTS_DE_FRANCE);
|
||||
}
|
||||
if (combined.includes('auvergerhonealpes') || combined.includes('rhonealpes') || combined.includes('auvergne')) {
|
||||
return buildResult(BRANDS.AUVERGNE_RHONE_ALPES);
|
||||
}
|
||||
if (combined.includes('nouvelleaquitaine') || combined.includes('aquitaine') || combined.includes('poitoucharentes') || combined.includes('limousin')) {
|
||||
return buildResult(BRANDS.NOUVELLE_AQUITAINE);
|
||||
}
|
||||
|
||||
// Palier B : Plan de numérotation SNCF (Strict & Déterministe)
|
||||
const regionByNum = getRegionByTrainNumber(numTrain);
|
||||
if (regionByNum) {
|
||||
return buildResult(regionByNum);
|
||||
}
|
||||
|
||||
// Palier C : Analyse géographique par score d'arrêts (si train hors numérotation standard)
|
||||
const regionByRoute = detectRegionByRouteScore(fullRouteText);
|
||||
if (regionByRoute) {
|
||||
return buildResult(regionByRoute);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 16. TER STANDARD (Fallback)
|
||||
// =========================================================================
|
||||
if (combined.includes('ter')) {
|
||||
return buildResult(BRANDS.TER_GENERIC);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 17. SNCF VOYAGEURS (Fallback par défaut)
|
||||
// =========================================================================
|
||||
return buildResult(BRANDS.SNCF_GENERIC, {
|
||||
typeLabel: rawType || 'SNCF',
|
||||
quaiLabel: rawType || 'SNCF'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface pour les afficheurs de Quai classiques (ClassiquesDepartsQuais & ClassiquesArriveesQuais)
|
||||
* @param {Object|string} trainTypeOrHoraire
|
||||
* @param {string} [operator]
|
||||
* @param {boolean} [isDeparture=true]
|
||||
* @param {string} [numTrainOverride]
|
||||
* @returns {{ logo: string, label: string, showVoyageursText: boolean, isTgv: boolean, classification: Object }}
|
||||
*/
|
||||
export function getTrainBrand(trainTypeOrHoraire = '', operator = '', isDeparture = true, numTrainOverride = '') {
|
||||
const c = classifyTrain(trainTypeOrHoraire, operator, numTrainOverride);
|
||||
|
||||
// Pour les trains sans marque spécifique :
|
||||
if (c.id === 'sncf' || c.id === 'ter') {
|
||||
if (isDeparture) {
|
||||
return {
|
||||
logo: BRANDS.SNCF_GENERIC.quaiLogo,
|
||||
label: c.quaiLabel || 'SNCF',
|
||||
showVoyageursText: true,
|
||||
isTgv: Boolean(c.isTgv),
|
||||
classification: c
|
||||
};
|
||||
}
|
||||
return {
|
||||
logo: BRANDS.TER_GENERIC.quaiLogo,
|
||||
label: c.quaiLabel || 'TER',
|
||||
showVoyageursText: false,
|
||||
isTgv: Boolean(c.isTgv),
|
||||
classification: c
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
logo: c.quaiLogo,
|
||||
label: c.quaiLabel,
|
||||
showVoyageursText: Boolean(c.showVoyageursText),
|
||||
isTgv: Boolean(c.isTgv),
|
||||
classification: c
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface pour les afficheurs Hall classiques (ClassiquesDeparts & ClassiquesArrivees)
|
||||
* @param {Object} horaire
|
||||
* @returns {{ logoSrc: string, typeLabel: string, numTrain: string, isTgv: boolean, brand: Object, classification: Object }}
|
||||
*/
|
||||
export function getTrainLogoAndMeta(horaire) {
|
||||
if (!horaire) {
|
||||
return {
|
||||
logoSrc: BRANDS.TER_GENERIC.hallLogo,
|
||||
typeLabel: 'TER',
|
||||
numTrain: '',
|
||||
isTgv: false,
|
||||
brand: getTrainBrand(),
|
||||
classification: BRANDS.TER_GENERIC
|
||||
};
|
||||
}
|
||||
|
||||
const c = classifyTrain(horaire);
|
||||
const brand = getTrainBrand(horaire, '', true);
|
||||
|
||||
return {
|
||||
logoSrc: c.hallLogo,
|
||||
typeLabel: c.typeLabel,
|
||||
numTrain: c.numTrain,
|
||||
isTgv: Boolean(c.isTgv),
|
||||
brand,
|
||||
classification: c
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* Base de données exhaustive des plages de numérotation GTFS SNCF
|
||||
* Source : GTFS SNCF Open Data (feed valide 02/10/2026 au 31/03/2027)
|
||||
*
|
||||
* 1. Cars TER Réguliers (route_type = 3)
|
||||
* 2. Cars de Substitution Travaux (route_type = 2)
|
||||
* 3. Trains TER (route_type = 2)
|
||||
*/
|
||||
|
||||
export const REGIONS_META = {
|
||||
AURA: { code: 'AURA', name: 'Auvergne-Rhône-Alpes', brand: 'TER AURA', logo: 'logo-auvergerhonealpes.svg', color: '#0077c8', cssClass: 'brand-teraura' },
|
||||
BFC: { code: 'BFC', name: 'Bourgogne-Franche-Comté', brand: 'MOBIGO', logo: 'logo-mobigo.svg', color: '#cfd82c', cssClass: 'brand-mobigo' },
|
||||
BRE: { code: 'BRE', name: 'Bretagne', brand: 'BREIZHGO', logo: 'logo-breizhgo.svg', color: '#00c6d3', cssClass: 'brand-breizhgo' },
|
||||
CVL: { code: 'CVL', name: 'Centre-Val de Loire', brand: 'RÉMI', logo: 'logo-remi.svg', color: '#00874e', cssClass: 'brand-remi' },
|
||||
GE: { code: 'GE', name: 'Grand Est', brand: 'FLUO', logo: 'logo-fluo.svg', color: '#f78bb8', cssClass: 'brand-fluo' },
|
||||
HDF: { code: 'HDF', name: 'Hauts-de-France', brand: 'TER HDF', logo: 'logo-hautsdefrance.svg', color: '#c9e000', cssClass: 'brand-terhdf' },
|
||||
NOR: { code: 'NOR', name: 'Normandie', brand: 'NOMAD', logo: 'logo-nomad.svg', color: '#6bb07a', cssClass: 'brand-nomad' },
|
||||
NA: { code: 'NA', name: 'Nouvelle-Aquitaine', brand: 'TER NA', logo: 'logo-teraquitaine.svg', color: '#ea5b0c', cssClass: 'brand-terna' },
|
||||
OCC: { code: 'OCC', name: 'Occitanie', brand: 'liO', logo: 'logo-lio.svg', color: '#e3001b', cssClass: 'brand-lio' },
|
||||
PDL: { code: 'PDL', name: 'Pays de la Loire', brand: 'ALÉOP', logo: 'logo-aleop.svg', color: '#00b0ca', cssClass: 'brand-aleop' },
|
||||
PACA: { code: 'PACA', name: "Provence-Alpes-Côte d'Azur", brand: 'ZOU !', logo: 'logo-zou.svg', color: '#0055a4', cssClass: 'brand-zou' },
|
||||
IDF: { code: 'IDF', name: 'Île-de-France', brand: 'TRANSILIEN', logo: 'logo-sncf.svg', color: '#003b80', cssClass: 'brand-idf' }
|
||||
};
|
||||
|
||||
// 1. CARS TER RÉGULIERS (route_type = 3)
|
||||
export const CARS_REGULIERS_RANGES = [
|
||||
{ min: 30131, max: 31478, region: 'AURA' },
|
||||
{ min: 33155, max: 35072, region: 'AURA' },
|
||||
{ min: 405036, max: 405562, region: 'AURA' },
|
||||
{ min: 35000, max: 35065, region: 'BFC' },
|
||||
{ min: 36518, max: 37529, region: 'BFC' },
|
||||
{ min: 48022, max: 48027, region: 'BRE' },
|
||||
{ min: 424300, max: 424336, region: 'CVL' },
|
||||
{ min: 426210, max: 426223, region: 'CVL' },
|
||||
{ min: 43001, max: 45141, region: 'GE' },
|
||||
{ min: 68130, max: 68283, region: 'GE' },
|
||||
{ min: 428429, max: 429499, region: 'GE' },
|
||||
{ min: 45530, max: 45569, region: 'HDF' },
|
||||
{ min: 69400, max: 69830, region: 'HDF' },
|
||||
{ min: 48000, max: 48033, region: 'NOR' },
|
||||
{ min: 87002, max: 88061, region: 'NOR' },
|
||||
{ min: 49957, max: 49974, region: 'NA' },
|
||||
{ min: 70926, max: 70944, region: 'NA' },
|
||||
{ min: 94000, max: 94032, region: 'OCC' },
|
||||
{ min: 95201, max: 95477, region: 'OCC' },
|
||||
{ min: 457156, max: 457268, region: 'OCC' },
|
||||
{ min: 461900, max: 462402, region: 'OCC' },
|
||||
{ min: 49981, max: 49986, region: 'PDL' },
|
||||
{ min: 75000, max: 75039, region: 'PDL' }
|
||||
];
|
||||
|
||||
// 2. CARS DE SUBSTITUTION TRAVAUX (route_type = 2)
|
||||
export const CARS_SUBSTITUTION_RANGES = [
|
||||
{ min: 30000, max: 31497, region: 'AURA' },
|
||||
{ min: 33005, max: 34813, region: 'AURA' },
|
||||
{ min: 404001, max: 412498, region: 'AURA' },
|
||||
{ min: 35039, max: 35044, region: 'BFC' },
|
||||
{ min: 36501, max: 37304, region: 'BFC' },
|
||||
{ min: 415035, max: 418367, region: 'BFC' },
|
||||
{ min: 419501, max: 419627, region: 'BFC' },
|
||||
{ min: 424010, max: 424403, region: 'BFC' },
|
||||
{ min: 67864, max: 67872, region: 'BRE' },
|
||||
{ min: 421001, max: 422112, region: 'BRE' },
|
||||
{ min: 442003, max: 442008, region: 'BRE' },
|
||||
{ min: 40850, max: 40904, region: 'CVL' },
|
||||
{ min: 424020, max: 426502, region: 'CVL' },
|
||||
{ min: 43103, max: 45499, region: 'GE' },
|
||||
{ min: 427250, max: 435837, region: 'GE' },
|
||||
{ min: 45500, max: 46576, region: 'HDF' },
|
||||
{ min: 85564, max: 85661, region: 'HDF' },
|
||||
{ min: 436010, max: 436191, region: 'HDF' },
|
||||
{ min: 437480, max: 437845, region: 'HDF' },
|
||||
{ min: 439005, max: 439537, region: 'HDF' },
|
||||
{ min: 48001, max: 48271, region: 'NOR' },
|
||||
{ min: 442108, max: 442129, region: 'NOR' },
|
||||
{ min: 446802, max: 447248, region: 'NOR' },
|
||||
{ min: 70452, max: 70769, region: 'NA' },
|
||||
{ min: 448014, max: 448812, region: 'NA' },
|
||||
{ min: 450001, max: 451482, region: 'NA' },
|
||||
{ min: 453009, max: 453872, region: 'NA' },
|
||||
{ min: 455006, max: 455684, region: 'NA' },
|
||||
{ min: 60401, max: 60444, region: 'OCC' },
|
||||
{ min: 73100, max: 73303, region: 'OCC' },
|
||||
{ min: 95470, max: 95478, region: 'OCC' },
|
||||
{ min: 412062, max: 412494, region: 'OCC' },
|
||||
{ min: 455005, max: 455238, region: 'OCC' },
|
||||
{ min: 457001, max: 457452, region: 'OCC' },
|
||||
{ min: 459663, max: 459691, region: 'OCC' },
|
||||
{ min: 461001, max: 462221, region: 'OCC' },
|
||||
{ min: 464040, max: 464140, region: 'PDL' },
|
||||
{ min: 64001, max: 64006, region: 'PACA' },
|
||||
{ min: 79000, max: 79002, region: 'PACA' },
|
||||
{ min: 466752, max: 468300, region: 'PACA' },
|
||||
{ min: 445602, max: 445619, region: 'IDF' }
|
||||
];
|
||||
|
||||
// 3. TRAINS TER (route_type = 2)
|
||||
export const TRAINS_TER_RANGES = [
|
||||
// 6 CHIFFRES (Segmentation très précise)
|
||||
{ min: 830102, max: 836407, region: 'GE' },
|
||||
{ min: 837501, max: 840877, region: 'GE' },
|
||||
{ min: 841200, max: 844948, region: 'HDF' },
|
||||
{ min: 846400, max: 849966, region: 'HDF' },
|
||||
{ min: 848950, max: 848977, region: 'NOR' },
|
||||
{ min: 850016, max: 853728, region: 'NOR' },
|
||||
{ min: 854007, max: 858397, region: 'BRE' },
|
||||
{ min: 857000, max: 860993, region: 'PDL' },
|
||||
{ min: 860001, max: 863714, region: 'CVL' },
|
||||
{ min: 862551, max: 862589, region: 'PDL' },
|
||||
{ min: 863031, max: 869129, region: 'NA' },
|
||||
{ min: 870000, max: 872891, region: 'OCC' },
|
||||
{ min: 873901, max: 873994, region: 'OCC' },
|
||||
{ min: 876100, max: 878991, region: 'OCC' },
|
||||
{ min: 873003, max: 874231, region: 'AURA' },
|
||||
{ min: 879102, max: 881699, region: 'PACA' },
|
||||
{ min: 882103, max: 890392, region: 'AURA' },
|
||||
{ min: 891000, max: 895975, region: 'BFC' },
|
||||
|
||||
// 4 ET 5 CHIFFRES
|
||||
{ min: 3100, max: 3483, region: 'NOR' },
|
||||
{ min: 3903, max: 3924, region: 'CVL' },
|
||||
{ min: 5900, max: 5921, region: 'BFC' },
|
||||
{ min: 13100, max: 13191, region: 'NOR' },
|
||||
{ min: 16350, max: 16480, region: 'HDF' },
|
||||
{ min: 16521, max: 16587, region: 'NOR' },
|
||||
{ min: 16843, max: 18999, region: 'AURA' },
|
||||
{ min: 17353, max: 17705, region: 'PACA' },
|
||||
{ min: 17750, max: 18146, region: 'BFC' },
|
||||
{ min: 19701, max: 19979, region: 'HDF' },
|
||||
{ min: 22244, max: 23026, region: 'OCC' },
|
||||
{ min: 23101, max: 23794, region: 'AURA' },
|
||||
{ min: 23722, max: 23831, region: 'GE' },
|
||||
{ min: 33501, max: 33647, region: 'PACA' },
|
||||
{ min: 81390, max: 81496, region: 'GE' },
|
||||
{ min: 86002, max: 86104, region: 'PACA' },
|
||||
{ min: 86380, max: 87470, region: 'GE' },
|
||||
{ min: 86960, max: 86998, region: 'OCC' },
|
||||
{ min: 88488, max: 88877, region: 'GE' },
|
||||
{ min: 96100, max: 96299, region: 'GE' },
|
||||
{ min: 96500, max: 96769, region: 'AURA' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Détecte la région probable à partir du nom ou de l'UIC d'une gare
|
||||
*/
|
||||
export function guessRegionFromStation(stationNameOrUic) {
|
||||
if (!stationNameOrUic) return null;
|
||||
const s = String(stationNameOrUic).toLowerCase();
|
||||
|
||||
if (s.includes('dijon') || s.includes('besançon') || s.includes('belfort') || s.includes('chalon') || s.includes('nevers') || s.includes('auxerre') || s.includes('mâcon') || s.includes('sens') || s.includes('bourgogne') || s.includes('franche-comte')) return 'BFC';
|
||||
if (s.includes('lyon') || s.includes('grenoble') || s.includes('saint-etienne') || s.includes('clermont') || s.includes('chambery') || s.includes('annecy') || s.includes('valence') || s.includes('aura') || s.includes('alpes')) return 'AURA';
|
||||
if (s.includes('marseille') || s.includes('nice') || s.includes('toulon') || s.includes('avignon') || s.includes('cannes') || s.includes('arles') || s.includes('antibes') || s.includes('paca') || s.includes('azur')) return 'PACA';
|
||||
if (s.includes('toulouse') || s.includes('montpellier') || s.includes('nimes') || s.includes('perpignan') || s.includes('beziers') || s.includes('narbonne') || s.includes('tarbes') || s.includes('occitanie')) return 'OCC';
|
||||
if (s.includes('bordeaux') || s.includes('limoges') || s.includes('poitiers') || s.includes('la rochelle') || s.includes('pau') || s.includes('angouleme') || s.includes('bayonne') || s.includes('agen') || s.includes('aquitaine')) return 'NA';
|
||||
if (s.includes('strasbourg') || s.includes('metz') || s.includes('nancy') || s.includes('reims') || s.includes('mulhouse') || s.includes('colmar') || s.includes('troyes') || s.includes('charleville') || s.includes('alsace') || s.includes('lorraine')) return 'GE';
|
||||
if (s.includes('lille') || s.includes('amiens') || s.includes('calais') || s.includes('dunkerque') || s.includes('arras') || s.includes('valenciennes') || s.includes('douai') || s.includes('hauts-de-france') || s.includes('nord')) return 'HDF';
|
||||
if (s.includes('rouen') || s.includes('caen') || s.includes('le havre') || s.includes('cherbourg') || s.includes('evreux') || s.includes('dieppe') || s.includes('alencon') || s.includes('normandie')) return 'NOR';
|
||||
if (s.includes('rennes') || s.includes('brest') || s.includes('quimper') || s.includes('vannes') || s.includes('saint-brieuc') || s.includes('lorient') || s.includes('saint-malo') || s.includes('bretagne')) return 'BRE';
|
||||
if (s.includes('nantes') || s.includes('angers') || s.includes('le mans') || s.includes('saint-nazaire') || s.includes('la roche-sur-yon') || s.includes('cholet') || s.includes('laval') || s.includes('loire')) return 'PDL';
|
||||
if (s.includes('orleans') || s.includes('tours') || s.includes('bourges') || s.includes('blois') || s.includes('chartres') || s.includes('chateauroux') || s.includes('vierzon')) return 'CVL';
|
||||
if (s.includes('paris') || s.includes('saint-denis') || s.includes('versailles') || s.includes('melun')) return 'IDF';
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moteur de résolution d'identité de circulation GTFS
|
||||
* @param {string|number} trainNumStr
|
||||
* @param {Object} options { stationContext, explicitMode }
|
||||
*/
|
||||
export function resolveCirculationIdentity(trainNumStr, options = {}) {
|
||||
const { stationContext = null, explicitMode = '' } = options;
|
||||
const num = parseInt(String(trainNumStr || '').replace(/\D/g, ''), 10);
|
||||
const preferredRegion = guessRegionFromStation(stationContext);
|
||||
const modeLower = String(explicitMode || '').toLowerCase();
|
||||
|
||||
const isExplicitCar = modeLower.includes('car') || modeLower.includes('autocar') || modeLower.includes('bus');
|
||||
|
||||
if (!isNaN(num)) {
|
||||
// 1. Priorité A : Cars de substitution travaux (route_type = 2)
|
||||
const subMatches = CARS_SUBSTITUTION_RANGES.filter(r => num >= r.min && num <= r.max);
|
||||
if (subMatches.length > 0) {
|
||||
const best = (preferredRegion && subMatches.find(m => m.region === preferredRegion)) || subMatches[0];
|
||||
const meta = REGIONS_META[best.region] || REGIONS_META.AURA;
|
||||
return {
|
||||
isCar: true,
|
||||
isSubstitution: true,
|
||||
regionCode: best.region,
|
||||
brand: meta.brand,
|
||||
typeLabel: 'CAR TER',
|
||||
logoFile: 'sncf-voyageurs-logo.png',
|
||||
cssClass: meta.cssClass,
|
||||
color: meta.color
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Priorité B : Cars TER Réguliers (route_type = 3)
|
||||
const regCarMatches = CARS_REGULIERS_RANGES.filter(r => num >= r.min && num <= r.max);
|
||||
if (regCarMatches.length > 0) {
|
||||
const best = (preferredRegion && regCarMatches.find(m => m.region === preferredRegion)) || regCarMatches[0];
|
||||
const meta = REGIONS_META[best.region] || REGIONS_META.AURA;
|
||||
return {
|
||||
isCar: true,
|
||||
isSubstitution: false,
|
||||
regionCode: best.region,
|
||||
brand: meta.brand,
|
||||
typeLabel: 'CAR',
|
||||
logoFile: 'track-car.svg',
|
||||
cssClass: meta.cssClass,
|
||||
color: meta.color
|
||||
};
|
||||
}
|
||||
|
||||
// Si le numéro est typique des séries autocar (4xxxxx), forcer Car même hors intervalle strict
|
||||
if (num >= 400000 && num <= 469999) {
|
||||
return {
|
||||
isCar: true,
|
||||
isSubstitution: true,
|
||||
regionCode: preferredRegion || 'AURA',
|
||||
brand: preferredRegion && REGIONS_META[preferredRegion] ? REGIONS_META[preferredRegion].brand : 'CAR TER',
|
||||
typeLabel: 'CAR',
|
||||
logoFile: 'track-car.svg',
|
||||
cssClass: preferredRegion && REGIONS_META[preferredRegion] ? REGIONS_META[preferredRegion].cssClass : 'brand-teraura',
|
||||
color: '#0077c8'
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Priorité C : Trains TER (route_type = 2)
|
||||
const trainMatches = TRAINS_TER_RANGES.filter(r => num >= r.min && num <= r.max);
|
||||
if (trainMatches.length > 0) {
|
||||
// Si conflit de chevauchement, prioriser la région de la gare si elle correspond
|
||||
const best = (preferredRegion && trainMatches.find(m => m.region === preferredRegion)) ||
|
||||
// Sinon favoriser la plage la plus spécifique (écart min/max le plus petit)
|
||||
trainMatches.sort((a, b) => (a.max - a.min) - (b.max - b.min))[0];
|
||||
|
||||
const meta = REGIONS_META[best.region] || REGIONS_META.AURA;
|
||||
return {
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
regionCode: best.region,
|
||||
brand: meta.brand,
|
||||
typeLabel: meta.brand,
|
||||
logoFile: meta.logo,
|
||||
cssClass: meta.cssClass,
|
||||
color: meta.color
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback si rien n'a matché
|
||||
if (isExplicitCar) {
|
||||
return {
|
||||
isCar: true,
|
||||
isSubstitution: false,
|
||||
regionCode: preferredRegion || 'SNCF',
|
||||
brand: 'Car TER',
|
||||
typeLabel: 'CAR',
|
||||
logoFile: 'track-car.svg',
|
||||
cssClass: 'brand-car',
|
||||
color: '#666666'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
regionCode: preferredRegion || 'SNCF',
|
||||
brand: 'TER',
|
||||
typeLabel: 'TER',
|
||||
logoFile: 'logo-ter.svg',
|
||||
cssClass: 'brand-ter',
|
||||
color: '#0077c8'
|
||||
};
|
||||
}
|
||||
+183
-113
@@ -1,3 +1,5 @@
|
||||
import { resolveCirculationIdentity } from './gtfsRegionalRanges';
|
||||
|
||||
// Indexation optimisée des logos pour recherche rapide et insensible à la casse / accents
|
||||
const normalizeStr = (s) => {
|
||||
if (!s) return '';
|
||||
@@ -11,14 +13,14 @@ const normalizeStr = (s) => {
|
||||
// Table des logos issue de public/type/data.json
|
||||
const LOGOS_DATA = [
|
||||
{ file: "logo-aleop.svg", slug: "aleop", name: "Aleop" },
|
||||
{ file: "logo-auvergerhonealpes.svg", slug: "auvergerhonealpes", name: "Auvergerhonealpes" },
|
||||
{ file: "logo-auvergerhonealpes.svg", slug: "auvergerhonealpes", name: "TER AURA" },
|
||||
{ file: "logo-breizhgo.svg", slug: "breizhgo", name: "Breizhgo" },
|
||||
{ file: "logo-cfl.svg", slug: "cfl", name: "CFL" },
|
||||
{ file: "logo-db.svg", slug: "db", name: "DB" },
|
||||
{ file: "logo-eurostar.svg", slug: "eurostar", name: "Eurostar" },
|
||||
{ file: "logo-fluo.svg", slug: "fluo", name: "Fluo" },
|
||||
{ file: "logo-frecciarossa.svg", slug: "frecciarossa", name: "Frecciarossa" },
|
||||
{ file: "logo-hautsdefrance.svg", slug: "hautsdefrance", name: "Hautsdefrance" },
|
||||
{ file: "logo-hautsdefrance.svg", slug: "hautsdefrance", name: "TER HDF" },
|
||||
{ file: "logo-ice.svg", slug: "ice", name: "ICE" },
|
||||
{ file: "logo-intercites.svg", slug: "intercites", name: "Intercités" },
|
||||
{ file: "logo-ler.svg", slug: "ler", name: "Ler" },
|
||||
@@ -43,21 +45,10 @@ const LOGOS_DATA = [
|
||||
{ file: "logo-sncf1985.svg", slug: "sncf1985", name: "Sncf1985" },
|
||||
{ file: "logo-sncf1992.svg", slug: "sncf1992", name: "Sncf1992" },
|
||||
{ file: "logo-teoz.svg", slug: "teoz", name: "Teoz" },
|
||||
{ file: "logo-ter.svg", slug: "ter", name: "TER" },
|
||||
{ file: "logo-ter-rhone-alpes.svg", slug: "ter-rhone-alpes", name: "TER Rhone-Alpes" },
|
||||
{ file: "logo-teralsace.svg", slug: "teralsace", name: "Ter Alsace" },
|
||||
{ file: "logo-teraquitaine.svg", slug: "teraquitaine", name: "Ter Aquitaine" },
|
||||
{ file: "logo-terbassenormandie.svg", slug: "terbassenormandie", name: "Ter Basse Normandie" },
|
||||
{ file: "logo-terbourgogne.svg", slug: "terbourgogne", name: "Ter Bourgogne" },
|
||||
{ file: "logo-terbretagne.svg", slug: "terbretagne", name: "Ter Bretagne" },
|
||||
{ file: "logo-tercentre.svg", slug: "tercentre", name: "Ter Centre" },
|
||||
{ file: "logo-terlanguedocroussillon.svg", slug: "terlanguedocroussillon", name: "Ter Languedoc-Roussillon" },
|
||||
{ file: "logo-termidipyrenees.svg", slug: "termidipyrenees", name: "Ter Midi-Pyrenees" },
|
||||
{ file: "logo-ternordpasdecalais.svg", slug: "ternordpasdecalais", name: "Ter Nord-pas-de-Calais" },
|
||||
{ file: "logo-terpoitoucharentes.svg", slug: "terpoitoucharentes", name: "Ter Poitou Charentes" },
|
||||
{ file: "logo-tgv.svg", slug: "tgv", name: "TGV" },
|
||||
{ file: "logo-inoui.svg", slug: "inoui", name: "TGV InOui" },
|
||||
{ file: "logo-lyria.svg", slug: "lyria", name: "TGV Lyria" },
|
||||
{ file: "logo-thalys.svg", slug: "thalys", name: "Thalys" },
|
||||
{ file: "logo-thello.svg", slug: "thello", name: "Thello" },
|
||||
{ file: "track-car.svg", slug: "track-car", name: "Car TER" },
|
||||
@@ -81,11 +72,24 @@ LOGOS_DATA.forEach((item) => {
|
||||
|
||||
/**
|
||||
* Détermine le logo SVG / PNG et le libellé de train à afficher à partir d'un objet horaire
|
||||
* Supporte les données SNCF Open Data (GTFS / SIRI) et Supabase
|
||||
* Intègre la détection déterministe GTFS (Cars réguliers, Cars substitution, Trains régionaux)
|
||||
* @param {Object} horaire
|
||||
* @param {string} [stationContext] - Nom de la gare courante pour arbitrage géographique
|
||||
*/
|
||||
export function getTrainLogoAndMeta(horaire) {
|
||||
export function getTrainLogoAndMeta(horaire, stationContext = null) {
|
||||
if (!horaire) {
|
||||
return { logoSrc: '/type/logo-sncf.svg', typeLabel: 'TER', numTrain: '' };
|
||||
return {
|
||||
logoSrc: '/type/logo-ter.svg',
|
||||
logoFile: 'logo-ter.svg',
|
||||
typeLabel: 'TER',
|
||||
numTrain: '',
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'TER',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-ter',
|
||||
brandColor: '#0077c8'
|
||||
};
|
||||
}
|
||||
|
||||
const opRaw = String(
|
||||
@@ -108,107 +112,173 @@ export function getTrainLogoAndMeta(horaire) {
|
||||
|
||||
const opNorm = normalizeStr(opRaw);
|
||||
|
||||
// 1. Recherche directe dans la base des logos
|
||||
let matchedLogo = logoLookup.get(opNorm);
|
||||
|
||||
// 2. Recherche par mots-clés si pas de correspondance exacte
|
||||
if (!matchedLogo) {
|
||||
if (opNorm.includes('inoui') || opNorm.includes('tgv')) {
|
||||
matchedLogo = logoLookup.get('inoui') || logoLookup.get('tgv');
|
||||
} else if (opNorm.includes('ouigoclassique')) {
|
||||
matchedLogo = logoLookup.get('ouigoclassique');
|
||||
} else if (opNorm.includes('ouigo')) {
|
||||
matchedLogo = logoLookup.get('ouigo');
|
||||
} else if (opNorm.includes('mobigo')) {
|
||||
matchedLogo = logoLookup.get('mobigo');
|
||||
} else if (opNorm.includes('fluo')) {
|
||||
matchedLogo = logoLookup.get('fluo');
|
||||
} else if (opNorm.includes('remi')) {
|
||||
matchedLogo = logoLookup.get('remi');
|
||||
} else if (opNorm.includes('nomad')) {
|
||||
matchedLogo = logoLookup.get('nomad');
|
||||
} else if (opNorm.includes('aleop')) {
|
||||
matchedLogo = logoLookup.get('aleop');
|
||||
} else if (opNorm.includes('breizhgo')) {
|
||||
matchedLogo = logoLookup.get('breizhgo');
|
||||
} else if (opNorm.includes('hautsdefrance')) {
|
||||
matchedLogo = logoLookup.get('hautsdefrance');
|
||||
} else if (opNorm.includes('lio')) {
|
||||
matchedLogo = logoLookup.get('lio');
|
||||
} else if (opNorm.includes('zou')) {
|
||||
matchedLogo = logoLookup.get('zou');
|
||||
} else if (opNorm.includes('intercites') || opNorm.includes('ic')) {
|
||||
matchedLogo = logoLookup.get('intercites');
|
||||
} else if (opNorm.includes('ice')) {
|
||||
matchedLogo = logoLookup.get('ice');
|
||||
} else if (opNorm.includes('eurostar')) {
|
||||
matchedLogo = logoLookup.get('eurostar');
|
||||
} else if (opNorm.includes('thalys')) {
|
||||
matchedLogo = logoLookup.get('thalys');
|
||||
} else if (opNorm.includes('lyria')) {
|
||||
matchedLogo = logoLookup.get('lyria');
|
||||
} else if (opNorm.includes('frecciarossa') || opNorm.includes('trenitalia')) {
|
||||
matchedLogo = logoLookup.get('frecciarossa') || logoLookup.get('trenitalia');
|
||||
} else if (opNorm.includes('renfe') || opNorm.includes('ave')) {
|
||||
matchedLogo = logoLookup.get('renfeave');
|
||||
} else if (opNorm.includes('car') || opNorm.includes('autocar') || opNorm.includes('bus')) {
|
||||
matchedLogo = logoLookup.get('trackcar') || { file: 'track-car.svg', name: 'Car TER' };
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Détermination du libellé affiché (typeLabel)
|
||||
let typeLabel = opRaw || 'TER';
|
||||
|
||||
// 1. Grands transporteurs nationaux et internationaux prioritaires (Explicites)
|
||||
if (opNorm.includes('inoui') || opNorm.includes('tgvinoui')) {
|
||||
typeLabel = 'TGV inOUI';
|
||||
} else if (opNorm === 'tgv') {
|
||||
typeLabel = 'TGV';
|
||||
} else if (opNorm.includes('ouigoclassique')) {
|
||||
typeLabel = 'OUIGO Train Classique';
|
||||
} else if (opNorm.includes('ouigo')) {
|
||||
typeLabel = 'OUIGO';
|
||||
} else if (opNorm.includes('mobigo')) {
|
||||
typeLabel = 'MOBIGO';
|
||||
} else if (opNorm.includes('fluo')) {
|
||||
typeLabel = 'FLUO';
|
||||
} else if (opNorm.includes('nomad')) {
|
||||
typeLabel = 'NOMAD';
|
||||
} else if (opNorm.includes('remi')) {
|
||||
typeLabel = 'RÉMI';
|
||||
} else if (opNorm.includes('aleop')) {
|
||||
typeLabel = 'ALÉOP';
|
||||
} else if (opNorm.includes('breizhgo')) {
|
||||
typeLabel = 'BREIZHGO';
|
||||
} else if (opNorm.includes('hautsdefrance')) {
|
||||
typeLabel = 'HAUTS-DE-FRANCE';
|
||||
} else if (opNorm.includes('lio')) {
|
||||
typeLabel = 'LIÒ';
|
||||
} else if (opNorm.includes('zou')) {
|
||||
typeLabel = 'ZOU !';
|
||||
} else if (opNorm.includes('intercites') || opNorm === 'ic') {
|
||||
typeLabel = 'INTERCITÉS';
|
||||
} else if (opNorm.includes('ice')) {
|
||||
typeLabel = 'ICE';
|
||||
} else if (opNorm.includes('eurostar')) {
|
||||
typeLabel = 'EUROSTAR';
|
||||
} else if (opNorm.includes('lyria')) {
|
||||
typeLabel = 'TGV Lyria';
|
||||
} else if (opNorm.includes('car') || opNorm.includes('autocar') || opNorm.includes('bus')) {
|
||||
typeLabel = 'AUTOCAR';
|
||||
} else if (opNorm.includes('ter')) {
|
||||
typeLabel = 'TER';
|
||||
return {
|
||||
logoSrc: '/type/logo-inoui.svg',
|
||||
logoFile: 'logo-inoui.svg',
|
||||
typeLabel: 'TGV inOUI',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'TGV inOUI',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-inoui',
|
||||
brandColor: '#7a1f3d'
|
||||
};
|
||||
}
|
||||
|
||||
// Spécificité régionale Bourgogne-Franche-Comté : TER commençant par 89xxxx ou 84xxxx -> MOBIGO
|
||||
if (typeLabel === 'TER' && (numTrain.startsWith('89') || numTrain.startsWith('84'))) {
|
||||
typeLabel = 'MOBIGO';
|
||||
if (!matchedLogo || matchedLogo.file === 'logo-ter.svg' || matchedLogo.file === 'logo-sncf.svg') {
|
||||
matchedLogo = logoLookup.get('mobigo') || matchedLogo;
|
||||
}
|
||||
if (opNorm === 'tgv' || (opNorm.includes('tgv') && !opNorm.includes('ouigo') && !opNorm.includes('lyria'))) {
|
||||
return {
|
||||
logoSrc: '/type/logo-tgv.svg',
|
||||
logoFile: 'logo-tgv.svg',
|
||||
typeLabel: 'TGV',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'TGV',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-tgv',
|
||||
brandColor: '#6e1e24'
|
||||
};
|
||||
}
|
||||
|
||||
const logoFile = (matchedLogo && matchedLogo.file) || 'logo-sncf.svg';
|
||||
const logoSrc = `/type/${logoFile}`;
|
||||
if (opNorm.includes('ouigoclassique') || opNorm.includes('ouigotrainclassique')) {
|
||||
return {
|
||||
logoSrc: '/type/logo-ouigo-classique.svg',
|
||||
logoFile: 'logo-ouigo-classique.svg',
|
||||
typeLabel: 'OUIGO Classique',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'OUIGO Classique',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-ouigo-classique',
|
||||
brandColor: '#ff007f'
|
||||
};
|
||||
}
|
||||
|
||||
return { logoSrc, typeLabel, numTrain };
|
||||
if (opNorm.includes('ouigo')) {
|
||||
return {
|
||||
logoSrc: '/type/logo-ouigo.svg',
|
||||
logoFile: 'logo-ouigo.svg',
|
||||
typeLabel: 'OUIGO',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'OUIGO',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-ouigo',
|
||||
brandColor: '#00a3e0'
|
||||
};
|
||||
}
|
||||
|
||||
if (opNorm.includes('intercites') || opNorm === 'ic') {
|
||||
return {
|
||||
logoSrc: '/type/logo-intercites.svg',
|
||||
logoFile: 'logo-intercites.svg',
|
||||
typeLabel: 'INTERCITÉS',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'INTERCITÉS',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-intercites',
|
||||
brandColor: '#a61c28'
|
||||
};
|
||||
}
|
||||
|
||||
if (opNorm.includes('eurostar')) {
|
||||
return {
|
||||
logoSrc: '/type/logo-eurostar.svg',
|
||||
logoFile: 'logo-eurostar.svg',
|
||||
typeLabel: 'EUROSTAR',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'EUROSTAR',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-eurostar',
|
||||
brandColor: '#003865'
|
||||
};
|
||||
}
|
||||
|
||||
if (opNorm.includes('ice') || opNorm.includes('db')) {
|
||||
return {
|
||||
logoSrc: '/type/logo-ice.svg',
|
||||
logoFile: 'logo-ice.svg',
|
||||
typeLabel: 'ICE',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'ICE',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-ice',
|
||||
brandColor: '#eb1800'
|
||||
};
|
||||
}
|
||||
|
||||
if (opNorm.includes('lyria')) {
|
||||
return {
|
||||
logoSrc: '/type/logo-lyria.svg',
|
||||
logoFile: 'logo-lyria.svg',
|
||||
typeLabel: 'TGV Lyria',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'TGV Lyria',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-lyria',
|
||||
brandColor: '#b4192b'
|
||||
};
|
||||
}
|
||||
|
||||
if (opNorm.includes('frecciarossa') || opNorm.includes('trenitalia')) {
|
||||
return {
|
||||
logoSrc: '/type/logo-frecciarossa.svg',
|
||||
logoFile: 'logo-frecciarossa.svg',
|
||||
typeLabel: 'Frecciarossa',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'Trenitalia',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-trenitalia',
|
||||
brandColor: '#c8102e'
|
||||
};
|
||||
}
|
||||
|
||||
if (opNorm.includes('renfe') || opNorm.includes('ave')) {
|
||||
return {
|
||||
logoSrc: '/type/logo-renfeave.svg',
|
||||
logoFile: 'logo-renfeave.svg',
|
||||
typeLabel: 'AVE',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'Renfe',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-renfe',
|
||||
brandColor: '#6f2c91'
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Moteur déterministe GTFS : Déduction par numéro de train et contexte de gare
|
||||
const identity = resolveCirculationIdentity(numTrain, {
|
||||
stationContext: stationContext || horaire.gare_depart_nom || horaire.gare_arrivee_nom || '',
|
||||
explicitMode: opRaw
|
||||
});
|
||||
|
||||
return {
|
||||
logoSrc: `/type/${identity.logoFile}`,
|
||||
logoFile: identity.logoFile,
|
||||
typeLabel: identity.typeLabel,
|
||||
numTrain,
|
||||
isCar: identity.isCar,
|
||||
isSubstitution: identity.isSubstitution,
|
||||
brand: identity.brand,
|
||||
regionCode: identity.regionCode,
|
||||
brandClass: identity.cssClass,
|
||||
brandColor: identity.color
|
||||
};
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
const horaire_date = "2026-06-14";
|
||||
const baseDate = new Date(horaire_date);
|
||||
console.log("baseDate before setHours:", baseDate.toString());
|
||||
const sched = 330; // 05:30
|
||||
baseDate.setHours(Math.floor(sched / 60), sched % 60, 0, 0);
|
||||
console.log("baseDate after setHours:", baseDate.toString());
|
||||
|
||||
const currentTime = new Date("2026-06-14T04:49:15+02:00");
|
||||
console.log("currentTime:", currentTime.toString());
|
||||
|
||||
const diffMinutes = (baseDate.getTime() - currentTime.getTime()) / 60000;
|
||||
console.log("diffMinutes:", diffMinutes);
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const gareDisplay = "Ouges";
|
||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||
console.log("flexNeedle:", flexNeedle);
|
||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type').ilike('nom', flexNeedle).limit(1);
|
||||
console.log("gdata:", gdata);
|
||||
let fetchedType = null;
|
||||
if (!gerr && gdata && gdata.length) fetchedType = gdata[0] && gdata[0].type;
|
||||
console.log("fetchedType:", fetchedType);
|
||||
}
|
||||
|
||||
run();
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data } = await supabase.from('gares').select('id,nom,type').ilike('nom', '%Ouges%');
|
||||
console.log("Matched gares:", data);
|
||||
}
|
||||
|
||||
run();
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data: horaires, error: hErr } = await supabase.from('horaires').select('*').limit(2);
|
||||
console.log("Horaires sample:", horaires);
|
||||
const { data: perturbs, error: pErr } = await supabase.from('perturbations').select('*').limit(2);
|
||||
console.log("Perturbations sample:", perturbs);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,23 +0,0 @@
|
||||
const horaires = [{ heure_depart: '21:00', numero_train: '123' }];
|
||||
const isArrival = false;
|
||||
const currentTime = new Date();
|
||||
currentTime.setHours(20, 55, 0, 0);
|
||||
|
||||
for (const train of horaires) {
|
||||
const numTrain = train.num_train || train.numero_train || train.numero;
|
||||
const timeStr = isArrival
|
||||
? (train.heure_arrivee || train.arrival_time || train.time || train.heure)
|
||||
: (train.heure_depart || train.departure_time || train.time || train.heure);
|
||||
|
||||
let trainTime;
|
||||
if (typeof timeStr === 'string' && timeStr.length <= 5 && timeStr.includes(':')) {
|
||||
const [hh, mm] = timeStr.split(':');
|
||||
trainTime = new Date(currentTime);
|
||||
trainTime.setHours(parseInt(hh, 10), parseInt(mm, 10), 0, 0);
|
||||
} else {
|
||||
trainTime = new Date(timeStr);
|
||||
}
|
||||
const diffMs = trainTime.getTime() - currentTime.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
console.log(`numTrain=${numTrain}, timeStr=${timeStr}, diffMins=${diffMins}`);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
const normalizeName = (s) => {
|
||||
if (!s) return '';
|
||||
try {
|
||||
const t = String(s).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
|
||||
return t.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ');
|
||||
} catch (e) {
|
||||
return String(s).toLowerCase().trim();
|
||||
}
|
||||
};
|
||||
|
||||
const parseTimeHM = (v) => {
|
||||
if (!v) return null;
|
||||
const m = String(v).match(/(\d{1,2}):(\d{2})/);
|
||||
if (m) return parseInt(m[1], 10)*60 + parseInt(m[2], 10);
|
||||
return null;
|
||||
};
|
||||
|
||||
async function run() {
|
||||
const gareDisplay = "Ouges";
|
||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||
const { data: gdata } = await supabase.from('gares').select('id,type').ilike('nom', flexNeedle).limit(1);
|
||||
const stationType = gdata && gdata.length ? gdata[0].type.toLowerCase() : null;
|
||||
console.log("stationType:", stationType);
|
||||
|
||||
const thresholdForStationType = (typeGare) => {
|
||||
const tG = String(typeGare || '').trim().toLowerCase();
|
||||
if (tG === 'interurbaine' || tG.includes('interurb')) return 360;
|
||||
if (tG === 'ville' || tG.includes('ville')) return 30;
|
||||
return 30;
|
||||
};
|
||||
const threshold = thresholdForStationType(stationType);
|
||||
console.log("threshold:", threshold);
|
||||
|
||||
const { data: allData } = await supabase.from('horaires').select('*').limit(200);
|
||||
const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges"));
|
||||
const h = horaires[0];
|
||||
|
||||
const stops = h.gares_desservies;
|
||||
const targetStationName = gareDisplay;
|
||||
|
||||
const stopForStation = (stops || []).find(s => {
|
||||
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
|
||||
const ns = normalizeName(nm || '');
|
||||
const target = normalizeName(targetStationName || '');
|
||||
return (targetStationName && (ns === target || ns.includes(target) || target.includes(ns)));
|
||||
}) || null;
|
||||
|
||||
h._displayPlatform = (stopForStation && stopForStation.quai_reel) || h.quai_reel || '';
|
||||
console.log("_displayPlatform:", h._displayPlatform);
|
||||
|
||||
const platform = (h._displayPlatform || '').toString().trim();
|
||||
let showPlatform = Boolean(platform);
|
||||
console.log("Boolean(platform):", showPlatform);
|
||||
|
||||
if (showPlatform) {
|
||||
let diffMinutes = null;
|
||||
h._depHM = parseTimeHM(h.heure_depart);
|
||||
const sched = h._depHM;
|
||||
console.log("sched:", sched);
|
||||
|
||||
const currentTime = new Date();
|
||||
if (sched !== null) {
|
||||
const baseDate = new Date();
|
||||
baseDate.setHours(Math.floor(sched / 60), sched % 60, 0, 0);
|
||||
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
|
||||
if (sched < 240 && nowHM > 1200) baseDate.setDate(baseDate.getDate() + 1);
|
||||
else if (sched > 1200 && nowHM < 240) baseDate.setDate(baseDate.getDate() - 1);
|
||||
diffMinutes = (baseDate.getTime() - currentTime.getTime()) / 60000;
|
||||
}
|
||||
console.log("diffMinutes:", diffMinutes);
|
||||
|
||||
if (diffMinutes !== null) {
|
||||
showPlatform = (diffMinutes <= threshold);
|
||||
} else {
|
||||
showPlatform = false;
|
||||
}
|
||||
}
|
||||
console.log("FINAL showPlatform:", showPlatform);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,10 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const supabaseUrl = 'https://supabase-ferrovia.mr-patator.fr';
|
||||
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc1OTUxMjAxLCJleHAiOjE5MzM2MzEyMDF9.ZFuzZSHVOMX-poU1fhWTjZ2sn7LFz9NYATTwJknlSr0';
|
||||
const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
||||
|
||||
async function test() {
|
||||
const { data: horaires, error } = await supabase.from('horaires').select('*').eq('numero_train', '891807');
|
||||
console.log(horaires[0]);
|
||||
}
|
||||
test();
|
||||
@@ -1,16 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function test() {
|
||||
const { data: horaires } = await supabase.from('horaires').select('*').limit(100);
|
||||
const matched = horaires.filter(h => JSON.stringify(h).includes("Ouges"));
|
||||
const h = matched[0];
|
||||
console.log("Train:", h.numero_train);
|
||||
console.log("Stops:", h.gares_desservies.map(s => ({
|
||||
gare_nom: s.gare_nom,
|
||||
quai_reel: s.quai_reel,
|
||||
gare_id: s.gare_id
|
||||
})));
|
||||
}
|
||||
test();
|
||||
@@ -1,10 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data } = await supabase.from('perturbations').select('*');
|
||||
console.log("Perturbations:", data);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,10 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const supabaseUrl = 'https://supabase-ferrovia.mr-patator.fr';
|
||||
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc1OTUxMjAxLCJleHAiOjE5MzM2MzEyMDF9.ZFuzZSHVOMX-poU1fhWTjZ2sn7LFz9NYATTwJknlSr0';
|
||||
const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
||||
|
||||
async function test() {
|
||||
const { data: horaires, error } = await supabase.from('horaires_departs').select('*');
|
||||
console.log('Error:', error);
|
||||
}
|
||||
test();
|
||||
@@ -1,24 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
const channel = supabase
|
||||
.channel('test_channel')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: 'INSERT', schema: 'public', table: 'annonces_diffusions' },
|
||||
(payload) => {
|
||||
console.log('Realtime Event Received!', payload);
|
||||
}
|
||||
)
|
||||
.subscribe((status) => {
|
||||
console.log('Subscription status:', status);
|
||||
});
|
||||
|
||||
console.log('Waiting for events...');
|
||||
// We will kill this process manually or let it timeout
|
||||
setTimeout(() => {
|
||||
console.log('Timeout. Exiting.');
|
||||
process.exit(0);
|
||||
}, 10000);
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data } = await supabase.from('horaires').select('numero_train, heure_depart, circule_dimanche').ilike('gares_desservies', '%Ouges%');
|
||||
console.log("Trains Ouges on Sunday:", data);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,10 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data } = await supabase.from('horaires').select('numero_train, circule_dimanche').in('numero_train', ['891801', '891813']);
|
||||
console.log("Trains Ouges on Sunday:", data);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,11 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data: allData } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies, circule_dimanche').limit(200);
|
||||
const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges") && h.circule_dimanche === true);
|
||||
console.log("Trains Ouges on Sunday:", horaires.length);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,14 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data: allData } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, heure_depart, gares_desservies, circule_dimanche').limit(200);
|
||||
const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges") && h.circule_dimanche === true);
|
||||
horaires.forEach(h => {
|
||||
const stop = h.gares_desservies.find(s => s.gare_nom === 'Ouges');
|
||||
console.log(`Train ${h.numero_train} departs Ouges at ${stop ? stop.heure_depart : h.heure_depart}`);
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,14 +0,0 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data: allData } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, heure_depart, gares_desservies, circule_dimanche').limit(200);
|
||||
const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges") && h.circule_dimanche === true);
|
||||
horaires.forEach(h => {
|
||||
const stop = h.gares_desservies.find(s => s.gare_nom === 'Ouges');
|
||||
console.log(`Train ${h.numero_train} departs Ouges at ${stop ? stop.heure_depart : h.heure_depart} with quai_reel: ${stop ? stop.quai_reel : 'none'}`);
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
||||
Reference in new issue
Block a user