RELEASE V5.0.0 : Voici les modifications apportées :

-> IMPLÉMENTATION DONNÉES GTFS / GTFS-RT / SIRI LTE SNCF
-> RESTRUCTURATION DE L'APP.

FONCTIONNALITÉS EN COURS DE DÉVELOPPEMENT :

-> REFONTE DU DESIGN DES AFFICHEURS CLASSIQUES
-> AMÉLIORATION DES
This commit is contained in:
mathis committed 2026-08-30 01:58:16 +02:00
1 parent 0e0d8c435f
commit 72de543dc6
18 files changed
+1550 -544

No files matched your search

+106 -77
View File
@@ -8,8 +8,7 @@ import Displays from './pages/Displays';
import ClassiquesDeparts from './pages/afficheurs/classiques/ClassiquesDeparts';
import ClassiquesArrivees from './pages/afficheurs/classiques/ClassiquesArrivees';
import ClassiquesDepartsQuais from './pages/afficheurs/classiques/quais/ClassiquesDepartsQuais';
import sessionMemory from './lib/sessionMemory';
import ClassiquesArriveesQuais from "./pages/afficheurs/classiques/quais/ClassiquesArriveesQuais";
import ClassiquesArriveesQuais from './pages/afficheurs/classiques/quais/ClassiquesArriveesQuais';
import AFLDeparts from './pages/afficheurs/afl/AFLDeparts';
import AFLArrivees from './pages/afficheurs/afl/AFLArrivees';
import AFLDepartsQuais from './pages/afficheurs/afl/AFLDepartsQuais';
@@ -18,19 +17,19 @@ import EVAArrivees from './pages/afficheurs/eva/EVAArrivees';
import EVADeparts from './pages/afficheurs/eva/EVADeparts';
import SelectAfficheur from './pages/SelectAfficheur';
import AfficheurViewportWrapper from './components/AfficheurViewportWrapper';
import sessionMemory from './lib/sessionMemory';
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 pour normaliser le chemin utilisé en internal route
// retirer query string et hash
p = String(p).split(/[?#]/)[0];
// traiter les cas comme /index.html
if (p.indexOf('index.html') !== -1) return '/';
// supprimer espaces en début/fin
p = p.trim();
// supprimer slashs finaux sauf pour la racine
if (p !== '/') p = p.replace(/\/+$/, '');
return p || '/';
}
@@ -49,24 +48,16 @@ function useSimpleRouter() {
function navigate(path) {
const target = normalizePath(path);
// toujours pousser l'URL complète (path peut contenir query/hash),
// mais stocker en internal state la version normalisée (sans query/hash)
// comparer l'URL complète (pathname + search + hash) afin de permettre
// des navigations qui ne changent que la querystring (ex. ?gare=...)
const currentFull = window.location.pathname + (window.location.search || '') + (window.location.hash || '');
if (currentFull !== path) {
console.debug('[router] navigate ->', path, '(normalized ->', target, ')');
try {
window.history.pushState({}, '', path);
} catch (e) {
// fallback si path est invalide
try { window.history.pushState({}, '', target); } catch (err) { /* ignore */ }
try { window.history.pushState({}, '', target); } catch (err) {}
}
// mettre à jour la route interne (sans query/hash)
setRoute(target);
} else {
// si l'URL complète est identique, forcer un update de state pour
// déclencher un rendu si besoin (rare)
setRoute(target);
}
}
@@ -77,122 +68,160 @@ function useSimpleRouter() {
function App() {
const { route, navigate } = useSimpleRouter();
const source = getSourceFromPath(route);
const prefix = source === 'supabase' ? '/ferrovia' : '';
const cleanRoute = (route.replace(/^\/ferrovia/, '') || '/');
// Composant Home explicite (panels cliquables)
function Home({ navigate }) {
useEffect(() => {
console.debug('[Home] monté, pathname =', window.location.pathname);
}, []);
function Home({ navigate, prefix, source }) {
const panelStyle = {
width: '40%',
width: '42%',
cursor: 'pointer',
padding: 24,
borderRadius: 8,
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
borderRadius: 10,
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
background: '#fff',
textAlign: 'center'
textAlign: 'center',
border: '1px solid #e0e8f6',
transition: 'transform 0.15s ease'
};
return (
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '40px', alignItems: 'center', background: '#e9f0ff', paddingTop: 32, paddingBottom: 32 }}>
<div style={{ width: '100%', textAlign: 'center', marginBottom: 16 }}>
<h1 style={{ margin: 0, fontSize: 28 }}>Accueil</h1>
<p style={{ margin: '8px 0 20px', color: '#333' }}>Choisissez une section ci-dessous</p>
</div>
<div style={{ display: 'flex', justifyContent: 'space-around', width: '100%' }}>
<div
role="button"
tabIndex={0}
onClick={() => { try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate('/departs'); }}
onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate('/departs'); })() : null)}
style={panelStyle}
aria-label="Accéder aux départs"
>
<h2>Départs</h2>
<p>Consultez les prochains départs d'une gare. Cliquez pour choisir une gare et voir les horaires.</p>
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '32px 24px', alignItems: 'center', background: '#e9f0ff', borderRadius: '12px' }}>
<div style={{ width: '100%', textAlign: 'center', marginBottom: 20 }}>
<div style={{ display: 'inline-block', padding: '4px 12px', borderRadius: '20px', background: source === 'supabase' ? '#e6f4ea' : '#e8f0fe', color: source === 'supabase' ? '#137333' : '#1a73e8', fontWeight: 600, fontSize: '13px', marginBottom: '8px' }}>
{source === 'supabase' ? 'Mode Base de Données (Ferrovia)' : 'Mode Officiel SNCF (GTFS / Temps Réel)'}
</div>
<h1 style={{ margin: '8px 0', fontSize: 28, color: '#002257' }}>Accueil</h1>
<p style={{ margin: '4px 0 16px', color: '#555' }}>
{source === 'supabase'
? "Consultez les afficheurs basés sur les données de votre BDD Supabase."
: "Consultez les afficheurs connectés aux flux réels GTFS et Open Data SNCF."}
</p>
</div>
<div
role="button"
tabIndex={0}
onClick={() => { try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} navigate('/arrivees'); }}
onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} navigate('/arrivees'); })() : null)}
style={panelStyle}
aria-label="Accéder aux arrivées"
>
<h2>Arrivées</h2>
<p>Consultez les prochaines arrivées d'une gare. Cliquez pour choisir une gare et voir les informations.</p>
</div>
<div style={{ display: 'flex', justifyContent: 'space-around', width: '100%', maxWidth: '800px' }}>
<div
role="button"
tabIndex={0}
onClick={() => { try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate(`${prefix}/departs`); }}
onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate(`${prefix}/departs`); })() : null)}
style={panelStyle}
aria-label="Accéder aux départs"
>
<h2 style={{ color: '#00439e', marginTop: 0 }}>🚆 Départs</h2>
<p style={{ color: '#666', fontSize: '14px' }}>Consultez les prochains départs d'une gare. Choisissez une gare et le style d'afficheur.</p>
</div>
<div
role="button"
tabIndex={0}
onClick={() => { try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} navigate(`${prefix}/arrivees`); }}
onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} navigate(`${prefix}/arrivees`); })() : null)}
style={panelStyle}
aria-label="Accéder aux arrivées"
>
<h2 style={{ color: '#00439e', marginTop: 0 }}>🏁 Arrivées</h2>
<p style={{ color: '#666', fontSize: '14px' }}>Consultez les prochaines arrivées d'une gare en temps réel.</p>
</div>
</div>
</div>
);
}
// Routes pour afficheurs : format /afficheurs/{mode}/{type}?gare=...
// route plus spécifique pour l'afficheur "quais"
if (route.startsWith('/afficheurs/departs/classiques/quais')) {
// Routes pour afficheurs : supporte le mode racine ET le mode /ferrovia
if (cleanRoute.startsWith('/afficheurs/departs/classiques/quais')) {
return <AfficheurViewportWrapper><ClassiquesDepartsQuais /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/arrivees/classiques/quais')) {
if (cleanRoute.startsWith('/afficheurs/arrivees/classiques/quais')) {
return <AfficheurViewportWrapper><ClassiquesArriveesQuais /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/departs/classiques')) {
if (cleanRoute.startsWith('/afficheurs/departs/classiques')) {
return <AfficheurViewportWrapper><ClassiquesDeparts /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/departs/afl/quais')) {
if (cleanRoute.startsWith('/afficheurs/departs/afl/quais')) {
return <AfficheurViewportWrapper><AFLDepartsQuais /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/departs/afl')) {
if (cleanRoute.startsWith('/afficheurs/departs/afl')) {
return <AfficheurViewportWrapper><AFLDeparts /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/departs/eva')) {
if (cleanRoute.startsWith('/afficheurs/departs/eva')) {
return <AfficheurViewportWrapper><EVADeparts /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/arrivees/afl/quais')) {
if (cleanRoute.startsWith('/afficheurs/arrivees/afl/quais')) {
return <AfficheurViewportWrapper><AFLArriveesQuais /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/arrivees/afl')) {
if (cleanRoute.startsWith('/afficheurs/arrivees/afl')) {
return <AfficheurViewportWrapper><AFLArrivees /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/arrivees/eva')) {
if (cleanRoute.startsWith('/afficheurs/arrivees/eva')) {
return <AfficheurViewportWrapper><EVAArrivees /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/arrivees/classiques')) {
if (cleanRoute.startsWith('/afficheurs/arrivees/classiques')) {
return <AfficheurViewportWrapper><ClassiquesArrivees /></AfficheurViewportWrapper>;
}
let content;
if (route === '/departs') {
content = <Departs onBack={() => navigate('/')} navigate={navigate} />;
} else if (route === '/arrivees') {
content = <Arrivees onBack={() => navigate('/')} navigate={navigate} />;
} else if (route === '/select-afficheur') {
if (cleanRoute === '/departs') {
content = <Departs onBack={() => navigate(prefix || '/')} navigate={navigate} />;
} else if (cleanRoute === '/arrivees') {
content = <Arrivees onBack={() => navigate(prefix || '/')} navigate={navigate} />;
} else if (cleanRoute === '/select-afficheur') {
content = <SelectAfficheur onBack={() => {
const params = new URLSearchParams(window.location.search);
const gare = params.get('gare') || '';
const mode = params.get('mode') || 'departs';
navigate(`/afficheurs/${encodeURIComponent(gare)}?mode=${mode}`);
navigate(`${prefix}/afficheurs/${encodeURIComponent(gare)}?mode=${mode}`);
}} navigate={navigate} />;
} else if (route.startsWith('/afficheurs')) {
content = <Displays onBack={() => navigate('/')} navigate={navigate} />;
} else if (cleanRoute.startsWith('/afficheurs')) {
content = <Displays onBack={() => navigate(prefix || '/')} navigate={navigate} />;
} else {
content = <Home navigate={navigate} />;
content = <Home navigate={navigate} prefix={prefix} source={source} />;
}
return (
<div className="sncf-voyageurs" style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<wcs-galactic app-name="Affichage Ferroviaire" version="1.0.0">
<wcs-galactic-menu-item>
<a href="/departs" onClick={(e) => { e.preventDefault(); navigate('/departs'); }}>Départs</a>
<a href={`${prefix}/departs`} onClick={(e) => { e.preventDefault(); navigate(`${prefix}/departs`); }}>Départs</a>
</wcs-galactic-menu-item>
<wcs-galactic-menu-item>
<a href="/arrivees" onClick={(e) => { e.preventDefault(); navigate('/arrivees'); }}>Arrivées</a>
<a href={`${prefix}/arrivees`} onClick={(e) => { e.preventDefault(); navigate(`${prefix}/arrivees`); }}>Arrivées</a>
</wcs-galactic-menu-item>
<wcs-galactic-menu-item>
{source === 'supabase' ? (
<a
href="/"
onClick={(e) => { e.preventDefault(); navigate('/'); }}
style={{ color: '#008b47', fontWeight: 600 }}
title="Basculer vers le mode SNCF GTFS"
>
🌐 Passer en mode SNCF Live
</a>
) : (
<a
href="/ferrovia"
onClick={(e) => { e.preventDefault(); navigate('/ferrovia'); }}
style={{ color: '#00439e', fontWeight: 600 }}
title="Basculer vers la Base de Données Supabase"
>
🗄️ Passer en mode BDD Ferrovia
</a>
)}
</wcs-galactic-menu-item>
</wcs-galactic>
<header style={{ padding: 12, background: '#00439e', color: '#fff' }}>
<div style={{ maxWidth: 1000, margin: '0 auto' }}>
<h1 style={{ margin: 0, fontSize: 20 }}>Affichage Ferroviaire — Accueil</h1>
<div style={{ fontSize: 12, opacity: 0.9 }}>Route actuelle: {route}</div>
<header style={{ padding: 12, background: source === 'supabase' ? '#003366' : '#00439e', color: '#fff', transition: 'background 0.3s' }}>
<div style={{ maxWidth: 1000, margin: '0 auto', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h1 style={{ margin: 0, fontSize: 20 }}>
{source === 'supabase' ? 'Affichage Ferroviaire — BDD Supabase (/ferrovia)' : 'Affichage Ferroviaire — SNCF GTFS / Temps Réel'}
</h1>
<div style={{ fontSize: 12, opacity: 0.9 }}>Route actuelle: {route}</div>
</div>
<div style={{ fontSize: '13px', background: 'rgba(255,255,255,0.15)', padding: '6px 12px', borderRadius: '6px' }}>
Source: <strong>{source === 'supabase' ? 'Supabase Database' : 'SNCF Open Data / GTFS'}</strong>
</div>
</div>
</header>
+123 -59
View File
@@ -1,102 +1,168 @@
import React, { useEffect, useState } from 'react';
import supabase from '../lib/supabase';
import React, { useEffect, useState, useMemo } from 'react';
import sessionMemory from '../lib/sessionMemory';
import { getSourceFromPath, searchGares } from '../services/railwayService';
export default function Arrivees({ onBack, navigate }) {
const [gares, setGares] = useState([]);
const [loading, setLoading] = useState(true);
const source = getSourceFromPath(window.location.pathname);
const prefix = source === 'supabase' ? '/ferrovia' : '';
const [allGares, setAllGares] = useState([]);
const [sncfResults, setSncfResults] = useState([]);
const [loading, setLoading] = useState(source === 'supabase');
const [error, setError] = useState(null);
const [selected, setSelected] = useState('');
const [search, setSearch] = useState('');
// Pour Supabase : charger les gares une seule fois au montage
useEffect(() => {
let mounted = true;
async function loadGares() {
setLoading(true);
setError(null);
if (!supabase) {
if (mounted) {
setError(
"Supabase non initialisé : vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY dans votre .env et redémarrez le serveur de développement (npm start)."
);
setLoading(false);
if (source === 'supabase') {
async function loadSupabaseGares() {
setLoading(true);
setError(null);
try {
const data = await searchGares('', 'supabase');
if (mounted) setAllGares(Array.isArray(data) ? data.map(r => r.nom || r) : []);
} catch (err) {
console.error('Supabase error:', err);
if (mounted) setError(err.message || String(err));
} finally {
if (mounted) setLoading(false);
}
}
loadSupabaseGares();
}
return () => { mounted = false; };
}, [source]);
// Pour SNCF : recherche dynamique avec debounce dès 2 caractères
useEffect(() => {
let active = true;
if (source === 'sncf') {
const q = search.trim();
if (q.length < 2) {
setSncfResults([]);
return;
}
try {
const { data, error } = await supabase.from('gares').select('nom');
if (error) throw error;
if (mounted) setGares(Array.isArray(data) ? data.map((r) => r.nom) : []);
} catch (err) {
console.error('Supabase error:', err);
if (mounted) setError(err.message || String(err));
} finally {
if (mounted) setLoading(false);
}
const timer = setTimeout(async () => {
try {
const results = await searchGares(q, 'sncf');
if (active) setSncfResults(results);
} catch (e) {
if (active) setSncfResults([]);
}
}, 200);
return () => {
active = false;
clearTimeout(timer);
};
}
}, [search, source]);
loadGares();
// Liste finale affichée
const displayedGares = useMemo(() => {
if (source === 'supabase') {
const q = search.toLowerCase().trim();
return allGares.filter(nom => String(nom).toLowerCase().includes(q)).map(nom => ({ nom }));
}
return sncfResults;
}, [source, allGares, sncfResults, search]);
return () => {
mounted = false;
};
}, []);
const handleSelectGare = (nom) => {
setSelected(nom);
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
if (navigate) {
navigate(`${prefix}/afficheurs/${encodeURIComponent(nom)}?mode=arrivees`);
}
};
return (
<div style={{ padding: 20 }}>
<h1>Arrivées</h1>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<h1 style={{ margin: 0 }}>Arrivées</h1>
<span style={{
fontSize: '12px',
fontWeight: '600',
padding: '4px 10px',
borderRadius: '12px',
background: source === 'supabase' ? '#e6f4ea' : '#e8f0fe',
color: source === 'supabase' ? '#137333' : '#1a73e8'
}}>
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF (GTFS)'}
</span>
</div>
{loading && <p>Chargement des gares...</p>}
{error && <p style={{ color: 'red' }}>Erreur : {error}</p>}
{!loading && !error && (
<div>
{/* Barre de recherche centrée */}
{/* Barre de recherche */}
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }}>
<input
type="search"
placeholder="Rechercher une gare..."
placeholder={source === 'sncf' ? "Rechercher une gare SNCF (ex: Dijon, Paris, Lyon...)" : "Rechercher une gare..."}
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{ width: '60%', padding: '10px 12px', borderRadius: 6, border: '1px solid #ccc' }}
aria-label="Rechercher une gare"
autoFocus
/>
</div>
{/* Résultats sous forme de lignes */}
<div role="list" aria-label="Résultats des gares">
{gares
.filter((nom) => nom.toLowerCase().includes(search.trim().toLowerCase()))
.map((nom) => (
<div key={nom} role="listitem">
{/* Invitation à la recherche en mode SNCF */}
{source === 'sncf' && search.trim().length < 2 && (
<div style={{ textAlign: 'center', padding: '30px 16px', color: '#666', background: '#fff', borderRadius: '8px', border: '1px dashed #ccc' }}>
<p style={{ margin: 0, fontSize: '15px' }}>
🔍 <strong>Saisissez au moins 2 caractères</strong> pour rechercher parmi les gares du référentiel GTFS SNCF.
</p>
</div>
)}
{/* Aucun résultat */}
{source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && (
<div style={{ textAlign: 'center', padding: '20px', color: '#777' }}>
Aucune gare SNCF trouvée pour "{search}".
</div>
)}
{/* Résultats */}
{displayedGares.length > 0 && (
<div role="list" aria-label="Résultats des gares" style={{ background: '#fff', borderRadius: '8px', padding: '8px 16px', boxShadow: '0 1px 4px rgba(0,0,0,0.06)' }}>
{displayedGares.map((g) => {
const nom = g.nom;
return (
<div key={g.uic || nom} role="listitem">
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', cursor: 'pointer' }}
onClick={() => {
setSelected(nom);
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
if (navigate) navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=arrivees`);
}}
onKeyDown={(e) => (e.key === 'Enter' ? (navigate ? navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=arrivees`) : setSelected(nom)) : null)}
tabIndex={0}
>
<div>{nom}</div>
{selected === nom && <div style={{ color: '#0070f3' }}>Sélectionnée</div>}
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 0', cursor: 'pointer' }}
onClick={() => handleSelectGare(nom)}
onKeyDown={(e) => (e.key === 'Enter' ? handleSelectGare(nom) : null)}
tabIndex={0}
>
<div>
<strong style={{ fontSize: '15px' }}>{nom}</strong>
{g.commune && <span style={{ marginLeft: '8px', color: '#666', fontSize: '13px' }}>({g.commune} - {g.departement})</span>}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{g.uic && <span style={{ fontSize: '11px', color: '#888', background: '#f1f3f4', padding: '2px 6px', borderRadius: '4px' }}>UIC {g.uic}</span>}
{selected === nom && <div style={{ color: '#0070f3' }}>Sélectionnée</div>}
</div>
</div>
<div style={{ height: 1, background: '#eef1f6', margin: '2px 0' }} />
</div>
<div style={{ height: 1, background: '#e0e0e0', margin: '6px 0' }} />
</div>
))}
</div>
);
})}
</div>
)}
{selected && (
<div style={{ marginTop: 12 }}>
<div style={{ marginTop: 16 }}>
<p>Gare sélectionnée : <strong>{selected}</strong></p>
<button onClick={() => {
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
if (navigate) {
navigate(`/afficheurs/${encodeURIComponent(selected)}?mode=arrivees`);
} else {
alert(`Afficher les arrivées pour ${selected}`);
navigate(`${prefix}/afficheurs/${encodeURIComponent(selected)}?mode=arrivees`);
}
}}>Voir les arrivées</button>
</div>
@@ -110,5 +176,3 @@ export default function Arrivees({ onBack, navigate }) {
</div>
);
}
+124 -61
View File
@@ -1,103 +1,168 @@
import React, { useEffect, useState } from 'react';
import supabase from '../lib/supabase';
import React, { useEffect, useState, useMemo } from 'react';
import sessionMemory from '../lib/sessionMemory';
import { getSourceFromPath, searchGares } from '../services/railwayService';
export default function Departs({ onBack, navigate }) {
const [gares, setGares] = useState([]);
const [loading, setLoading] = useState(true);
const source = getSourceFromPath(window.location.pathname);
const prefix = source === 'supabase' ? '/ferrovia' : '';
const [allGares, setAllGares] = useState([]);
const [sncfResults, setSncfResults] = useState([]);
const [loading, setLoading] = useState(source === 'supabase');
const [error, setError] = useState(null);
const [selected, setSelected] = useState('');
const [search, setSearch] = useState('');
// Pour Supabase : charger les gares une seule fois au montage
useEffect(() => {
let mounted = true;
async function loadGares() {
setLoading(true);
setError(null);
if (!supabase) {
// Le client Supabase n'a pas pu être initialisé (variables d'env manquantes)
if (mounted) {
setError(
"Supabase non initialisé : vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY dans votre .env et redémarrez le serveur de développement (npm start)."
);
setLoading(false);
if (source === 'supabase') {
async function loadSupabaseGares() {
setLoading(true);
setError(null);
try {
const data = await searchGares('', 'supabase');
if (mounted) setAllGares(Array.isArray(data) ? data.map(r => r.nom || r) : []);
} catch (err) {
console.error('Supabase error:', err);
if (mounted) setError(err.message || String(err));
} finally {
if (mounted) setLoading(false);
}
}
loadSupabaseGares();
}
return () => { mounted = false; };
}, [source]);
// Pour SNCF : recherche dynamique avec debounce dès 2 caractères
useEffect(() => {
let active = true;
if (source === 'sncf') {
const q = search.trim();
if (q.length < 2) {
setSncfResults([]);
return;
}
try {
const { data, error } = await supabase.from('gares').select('nom');
if (error) throw error;
if (mounted) setGares(Array.isArray(data) ? data.map((r) => r.nom) : []);
} catch (err) {
console.error('Supabase error:', err);
if (mounted) setError(err.message || String(err));
} finally {
if (mounted) setLoading(false);
}
const timer = setTimeout(async () => {
try {
const results = await searchGares(q, 'sncf');
if (active) setSncfResults(results);
} catch (e) {
if (active) setSncfResults([]);
}
}, 200);
return () => {
active = false;
clearTimeout(timer);
};
}
}, [search, source]);
loadGares();
// Liste finale affichée
const displayedGares = useMemo(() => {
if (source === 'supabase') {
const q = search.toLowerCase().trim();
return allGares.filter(nom => String(nom).toLowerCase().includes(q)).map(nom => ({ nom }));
}
return sncfResults;
}, [source, allGares, sncfResults, search]);
return () => {
mounted = false;
};
}, []);
const handleSelectGare = (nom) => {
setSelected(nom);
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
if (navigate) {
navigate(`${prefix}/afficheurs/${encodeURIComponent(nom)}?mode=departs`);
}
};
return (
<div style={{ padding: 20 }}>
<h1>Départs</h1>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<h1 style={{ margin: 0 }}>Départs</h1>
<span style={{
fontSize: '12px',
fontWeight: '600',
padding: '4px 10px',
borderRadius: '12px',
background: source === 'supabase' ? '#e6f4ea' : '#e8f0fe',
color: source === 'supabase' ? '#137333' : '#1a73e8'
}}>
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF (GTFS)'}
</span>
</div>
{loading && <p>Chargement des gares...</p>}
{error && <p style={{ color: 'red' }}>Erreur : {error}</p>}
{!loading && !error && (
<div>
{/* Barre de recherche centrée */}
{/* Barre de recherche */}
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }}>
<input
type="search"
placeholder="Rechercher une gare..."
placeholder={source === 'sncf' ? "Rechercher une gare SNCF (ex: Dijon, Paris, Lyon...)" : "Rechercher une gare..."}
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{ width: '60%', padding: '10px 12px', borderRadius: 6, border: '1px solid #ccc' }}
aria-label="Rechercher une gare"
autoFocus
/>
</div>
{/* Résultats sous forme de lignes */}
<div role="list" aria-label="Résultats des gares">
{gares
.filter((nom) => nom.toLowerCase().includes(search.trim().toLowerCase()))
.map((nom) => (
<div key={nom} role="listitem">
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', cursor: 'pointer' }}
onClick={() => {
setSelected(nom);
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
if (navigate) navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=departs`);
}}
onKeyDown={(e) => (e.key === 'Enter' ? (navigate ? navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=departs`) : setSelected(nom)) : null)}
tabIndex={0}
>
<div>{nom}</div>
{selected === nom && <div style={{ color: '#0070f3' }}>Sélectionnée</div>}
{/* Invitation à la recherche en mode SNCF */}
{source === 'sncf' && search.trim().length < 2 && (
<div style={{ textAlign: 'center', padding: '30px 16px', color: '#666', background: '#fff', borderRadius: '8px', border: '1px dashed #ccc' }}>
<p style={{ margin: 0, fontSize: '15px' }}>
🔍 <strong>Saisissez au moins 2 caractères</strong> pour rechercher parmi les gares du référentiel GTFS SNCF.
</p>
</div>
)}
{/* Aucun résultat */}
{source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && (
<div style={{ textAlign: 'center', padding: '20px', color: '#777' }}>
Aucune gare SNCF trouvée pour "{search}".
</div>
)}
{/* Résultats */}
{displayedGares.length > 0 && (
<div role="list" aria-label="Résultats des gares" style={{ background: '#fff', borderRadius: '8px', padding: '8px 16px', boxShadow: '0 1px 4px rgba(0,0,0,0.06)' }}>
{displayedGares.map((g) => {
const nom = g.nom;
return (
<div key={g.uic || nom} role="listitem">
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 0', cursor: 'pointer' }}
onClick={() => handleSelectGare(nom)}
onKeyDown={(e) => (e.key === 'Enter' ? handleSelectGare(nom) : null)}
tabIndex={0}
>
<div>
<strong style={{ fontSize: '15px' }}>{nom}</strong>
{g.commune && <span style={{ marginLeft: '8px', color: '#666', fontSize: '13px' }}>({g.commune} - {g.departement})</span>}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{g.uic && <span style={{ fontSize: '11px', color: '#888', background: '#f1f3f4', padding: '2px 6px', borderRadius: '4px' }}>UIC {g.uic}</span>}
{selected === nom && <div style={{ color: '#0070f3' }}>Sélectionnée</div>}
</div>
</div>
<div style={{ height: 1, background: '#eef1f6', margin: '2px 0' }} />
</div>
<div style={{ height: 1, background: '#e0e0e0', margin: '6px 0' }} />
</div>
))}
</div>
);
})}
</div>
)}
{selected && (
<div style={{ marginTop: 12 }}>
<div style={{ marginTop: 16 }}>
<p>Gare sélectionnée : <strong>{selected}</strong></p>
<button onClick={() => {
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
if (navigate) {
navigate(`/afficheurs/${encodeURIComponent(selected)}?mode=departs`);
} else {
alert(`Afficher les départs pour ${selected}`);
navigate(`${prefix}/afficheurs/${encodeURIComponent(selected)}?mode=departs`);
}
}}>Voir les départs</button>
</div>
@@ -111,5 +176,3 @@ export default function Departs({ onBack, navigate }) {
</div>
);
}
+40 -32
View File
@@ -1,8 +1,12 @@
import React from 'react';
import WcsButton from '../components/WcsButton';
import sessionMemory from '../lib/sessionMemory';
import { getSourceFromPath } from '../services/railwayService';
export default function Displays({ onBack, navigate }) {
const source = getSourceFromPath(window.location.pathname);
const prefix = source === 'supabase' ? '/ferrovia' : '';
const types = [
{ id: 'classiques', label: 'Classiques' },
{ id: 'eva', label: 'EVA' },
@@ -10,25 +14,23 @@ export default function Displays({ onBack, navigate }) {
{ id: 'transilien', label: 'TRANSILIEN' }
];
// Récupérer la gare : soit dans le pathname (/afficheurs/<gare>),
// soit via la querystring ?gare=... si la route est déjà /afficheurs/<type>/<mode>
const parts = typeof window !== 'undefined' ? window.location.pathname.split('/').filter(Boolean) : [];
// Nettoyage du chemin pour extraire la gare même avec /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;
if (parts[0] === 'afficheurs' && parts[1] && !typeIds.includes(parts[1])) {
// cas /afficheurs/<gare>
try {
gare = decodeURIComponent(parts.slice(1).join('/'));
} catch (e) {
gare = parts.slice(1).join('/');
}
} else {
// si l'URL est /afficheurs/<type>/<mode>, regarder dans la querystring ?gare=...
const paramsForGare = new URLSearchParams(window.location.search || '');
const g = paramsForGare.get('gare');
if (g) {
try {
// décoder puis remplacer les tirets par des espaces pour l'affichage
gare = decodeURIComponent(String(g).replace(/\+/g, ' ')).replace(/-/g, ' ');
} catch (e) {
gare = String(g).replace(/-/g, ' ');
@@ -36,37 +38,52 @@ export default function Displays({ onBack, navigate }) {
}
}
// déterminer le mode (departs/arrivees) : querystring > localStorage > default('departs')
// Déterminer le mode (departs/arrivees)
const params = new URLSearchParams(window.location.search || '');
let mode = params.get('mode');
if (!mode) {
try {
const stored = sessionMemory.get('afficheurMode');
if (stored) mode = stored;
} catch (e) {
// ignore
}
} catch (e) {}
}
if (!mode) mode = 'departs';
return (
<div style={{ padding: 20 }}>
<h1>Choisissez un type d'afficheur</h1>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<h1 style={{ margin: 0 }}>Choisissez un type d'afficheur</h1>
<span style={{
fontSize: '12px',
fontWeight: '600',
padding: '4px 10px',
borderRadius: '12px',
background: source === 'supabase' ? '#e6f4ea' : '#e8f0fe',
color: source === 'supabase' ? '#137333' : '#1a73e8'
}}>
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF (GTFS)'}
</span>
</div>
{gare && <p>Gare sélectionnée : <strong>{gare}</strong></p>}
<p>Mode sélectionné : <strong>{mode === 'arrivees' ? 'Arrivées' : 'Départs'}</strong></p>
{/* Boutons directs pour afficher les afficheurs classiques supprimés */}
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', marginTop: 16 }}>
{types.map((t) => (
<div key={t.id} role="button" tabIndex={0} style={{ width: 220, padding: 16, borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.08)', background: '#fff', cursor: 'pointer' }} onClick={() => {
if (typeof navigate === 'function') {
const kebab = gare ? String(gare).trim().replace(/\s+/g, '-').replace(/\//g, '/') : '';
navigate(`/select-afficheur?gare=${encodeURIComponent(kebab)}&mode=${mode}&type=${t.id}`);
return;
}
alert(`${t.label} pour ${gare || '—'}`);
}}>
<div
key={t.id}
role="button"
tabIndex={0}
style={{ width: 220, padding: 16, borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.08)', background: '#fff', cursor: 'pointer' }}
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}`);
return;
}
alert(`${t.label} pour ${gare || '—'}`);
}}
>
<h3 style={{ margin: '0 0 8px' }}>{t.label}</h3>
<p style={{ margin: 0, color: '#555' }}>Afficher le style {t.label} pour la gare {gare || ''}.</p>
</div>
@@ -74,29 +91,21 @@ export default function Displays({ onBack, navigate }) {
</div>
<div style={{ marginTop: 24 }}>
{/* Bouton de retour au style WCS SNCF */}
<WcsButton
// Styliser le bouton comme un bouton d'action WCS. On peut ajuster `mode` ou
// ajouter `className` (ex: "wcs-primary") si vous avez des classes globales.
mode="plain"
className="wcs-primary"
onClick={() => {
// Appelle le callback fourni par le routeur simple si disponible
try {
if (typeof onBack === 'function') {
onBack();
return;
}
} catch (e) {
// ignore
}
} catch (e) {}
// Fallback : si l'historique du navigateur permet un back, on l'utilise
if (typeof window !== 'undefined' && window.history && window.history.length > 1) {
window.history.back();
} else {
// Redirection sûre vers la racine
window.location.href = '/';
window.location.href = prefix || '/';
}
}}
label="Retour"
@@ -105,4 +114,3 @@ export default function Displays({ onBack, navigate }) {
</div>
);
}
+32 -54
View File
@@ -1,8 +1,11 @@
import React, { useEffect, useState } from 'react';
import supabase from '../lib/supabase';
import WcsButton from '../components/WcsButton';
import { getSourceFromPath, getPlatformsForStation } from '../services/railwayService';
export default function SelectAfficheur({ onBack, navigate }) {
const source = getSourceFromPath(window.location.pathname);
const prefix = source === 'supabase' ? '/ferrovia' : '';
const params = new URLSearchParams(window.location.search || '');
const gareParam = params.get('gare') || '';
const modeParam = params.get('mode') || 'departs';
@@ -21,50 +24,13 @@ export default function SelectAfficheur({ onBack, navigate }) {
useEffect(() => {
let mounted = true;
async function loadPlatforms() {
if (!supabase) {
setLoading(false);
return;
}
try {
setLoading(true);
const { data, error } = await supabase.from('horaires').select('quai_reel, voie, quai, gare_depart_nom, gare_arrivee_nom, gares_desservies');
if (error) throw error;
const sortedPlatforms = await getPlatformsForStation(decodedGare || gareParam, source);
if (!mounted) return;
const platformSet = new Set();
const normGare = decodedGare.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
(data || []).forEach(h => {
let stopsList = [];
if (Array.isArray(h.gares_desservies)) {
stopsList = h.gares_desservies;
} else if (typeof h.gares_desservies === 'string') {
try { stopsList = JSON.parse(h.gares_desservies); } catch (e) {}
}
if (!Array.isArray(stopsList)) stopsList = [];
let foundStop = stopsList.find(s => {
const name = (s.gare_nom || s.nom || s.name || '').toString().normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
return name === normGare || name.includes(normGare) || normGare.includes(name);
});
if (foundStop) {
const platform = (foundStop.quai_reel || foundStop.quai || foundStop.platform || foundStop.quay || '').toString().trim();
if (platform) platformSet.add(platform);
} else {
const origin = (h.gare_depart_nom || '').toString().normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
const dest = (h.gare_arrivee_nom || '').toString().normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
if (origin === normGare || origin.includes(normGare) || normGare.includes(origin) ||
dest === normGare || dest.includes(normGare) || normGare.includes(dest)) {
const platform = (h.quai_reel || h.voie || h.quai || '').toString().trim();
if (platform) platformSet.add(platform);
}
}
});
const sortedPlatforms = Array.from(platformSet).sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
setPlatforms(sortedPlatforms);
if (sortedPlatforms.length > 0) {
setPlatforms(sortedPlatforms || []);
if (sortedPlatforms && sortedPlatforms.length > 0) {
setSelectedPlatform(sortedPlatforms[0]);
}
} catch (err) {
@@ -78,12 +44,12 @@ export default function SelectAfficheur({ onBack, navigate }) {
return () => {
mounted = false;
};
}, [gareParam, decodedGare]);
}, [gareParam, decodedGare, source]);
const handleGeneral = () => {
if (typeof navigate === 'function') {
const kebab = gareParam ? String(gareParam).trim() : '';
navigate(`/afficheurs/${modeParam}/${typeParam}?gare=${encodeURIComponent(kebab)}`);
navigate(`${prefix}/afficheurs/${modeParam}/${typeParam}?gare=${encodeURIComponent(kebab)}`);
}
};
@@ -95,7 +61,7 @@ export default function SelectAfficheur({ onBack, navigate }) {
}
if (typeof navigate === 'function') {
const kebab = gareParam ? String(gareParam).trim() : '';
navigate(`/afficheurs/${modeParam}/${typeParam}/quais?gare=${encodeURIComponent(kebab)}&quai=${encodeURIComponent(quai)}`);
navigate(`${prefix}/afficheurs/${modeParam}/${typeParam}/quais?gare=${encodeURIComponent(kebab)}&quai=${encodeURIComponent(quai)}`);
}
};
@@ -109,15 +75,27 @@ export default function SelectAfficheur({ onBack, navigate }) {
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
}}>
<h2 style={{
marginTop: 0,
marginBottom: '8px',
color: '#002257',
fontSize: '28px',
fontWeight: 700
}}>
Sélection de l'Afficheur
</h2>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<h2 style={{
margin: 0,
color: '#002257',
fontSize: '28px',
fontWeight: 700
}}>
Sélection de l'Afficheur
</h2>
<span style={{
fontSize: '12px',
fontWeight: '600',
padding: '4px 10px',
borderRadius: '12px',
background: source === 'supabase' ? '#e6f4ea' : '#e8f0fe',
color: source === 'supabase' ? '#137333' : '#1a73e8'
}}>
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF (GTFS)'}
</span>
</div>
<p style={{
margin: '0 0 24px 0',
color: '#555555',
@@ -268,7 +246,7 @@ export default function SelectAfficheur({ onBack, navigate }) {
if (typeof onBack === 'function') {
onBack();
} else if (typeof navigate === 'function') {
navigate(`/afficheurs?gare=${encodeURIComponent(gareParam)}&mode=${modeParam}`);
navigate(`${prefix}/afficheurs?gare=${encodeURIComponent(gareParam)}&mode=${modeParam}`);
}
}}
label="Retour aux types d'afficheurs"
+80 -16
View File
@@ -5,21 +5,34 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0');
const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`;
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`;
if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h');
}
return String(v).slice(0,5).replace(':','h');
@@ -155,10 +168,17 @@ export default function AFLArrivees(){
useAnnonceSonoreRealtime(resolvedGareId, 'AFLArrivees');
useAutoAnnonce(horaires, currentTime, true);
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
useEffect(() => {
let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('interurbaine');
setResolvedGareId(null);
}
return;
}
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
@@ -188,9 +208,7 @@ export default function AFLArrivees(){
}
} catch (e) { /* ignore */ }
};
if (supabase) {
fetchStationType();
}
fetchStationType();
return () => { mounted = false; };
}, [gareIdParam, gareDisplay]);
@@ -315,11 +333,14 @@ export default function AFLArrivees(){
// eslint-disable-next-line no-unused-vars
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes();
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
}, []);
@@ -626,18 +647,57 @@ export default function AFLArrivees(){
});
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
// Récupération Supabase — maintenant avec polling pour synchroniser sans recharger la page
// Récupération des données (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu
useEffect(() => {
let mounted = true;
let inFlight = false; // empêche les fetchs concurrents
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => {
if (!mounted) return;
if (inFlight) return; // éviter les appels simultanés si le fetch précédent n'est pas terminé
if (inFlight) return;
inFlight = true;
if (source === 'sncf') {
setLoading(true);
setError(null);
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'arrivees'
});
if (!mounted) { inFlight = false; return; }
if (sncfErr) {
setError(sncfErr);
} else {
const formatted = (sncfData || []).map(t => {
const timeStr = t.heure_arrivee || t.heure_depart || t.time || '';
return {
...t,
_displayTime: formatTimeNoSeconds(timeStr),
_displayPlatform: t.quai || t.voie || '',
_arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr),
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
};
});
setHoraires(formatted.slice(0, 10));
setAllHoraires(formatted);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
} finally {
if (mounted) setLoading(false);
inFlight = false;
}
return;
}
if (!supabase) {
// Supabase non initialisé — avertir et définir un message d'erreur pour l'UI
// eslint-disable-next-line no-console
console.warn('Supabase non initialisé — vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY');
setError('Supabase non initialisé — vérifiez les variables d\'environnement');
setLoading(false);
@@ -1022,7 +1082,7 @@ export default function AFLArrivees(){
const intervalId = setInterval(fetchData, intervalMs);
return () => { mounted = false; clearInterval(intervalId); };
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]);
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]);
// Helpers pour perturbations (memoized plus haut)
@@ -1301,6 +1361,10 @@ export default function AFLArrivees(){
<div className="afl-arr-voie-box-compact">
<BusIcon size="52px" color="#173e31" />
</div>
) : showPlatform ? (
<div className="afl-arr-voie-box-compact">
<div className="afl-arr-voie-value" style={{ fontSize: '58px', lineHeight: 1 }}>{rawPlatform}</div>
</div>
) : null
)}
</div>
+48 -9
View File
@@ -4,19 +4,22 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import './AFLArriveesQuais.css';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import Logo from '../../../components/Logo';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0');
const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}:${mm}`;
}
const parts = String(v).split(':');
if (parts.length >= 2) {
return `${parts[0].padStart(2, '0')}:${parts[1].padStart(2, '0')}`;
const m = String(v).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m[2]}`;
}
return v;
};
@@ -137,9 +140,12 @@ const AFLArriveesQuais = () => {
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2})[h:](\d{2})/);
if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[h:](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
@@ -351,6 +357,39 @@ const AFLArriveesQuais = () => {
const fetchData = useCallback(async () => {
try {
const source = getSourceFromPath(window.location.pathname);
if (source === 'sncf') {
const { data: sncfData } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'arrivees',
quai: quaiParam
});
if (sncfData && sncfData.length > 0) {
const train = sncfData[0];
const enriched = {
...train,
_rawTime: train.heure_arrivee || train.heure_depart,
_displayTime: formatTimeNoSeconds(train.heure_arrivee || train.heure_depart),
_status: {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: train.retard_minutes || null
},
_nextStationStatus: {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure")
},
_isToday: true
};
setNextArrival(enriched);
} else {
setNextArrival(null);
}
setLoading(false);
return;
}
if (!supabase) return;
const [{ data: horairesData }, { data: subData }, { data: perturbData }, { data: travauxData }] = await Promise.all([
fetchAllRows(supabase.from('horaires').select('*')),
supabase.from('travaux_substitutions').select('*'),
@@ -615,7 +654,7 @@ const AFLArriveesQuais = () => {
console.error(err);
setLoading(false);
}
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, gareParam, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
useEffect(() => {
fetchData();
@@ -706,7 +745,7 @@ const AFLArriveesQuais = () => {
if (gareDisplay && Array.isArray(stopsList)) {
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
}
const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || trainData.quai_reel || trainData.voie || trainData.quai || quaiParam || '').toString().trim();
const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || trainData.quai_reel || trainData.voie || trainData.quai || trainData._displayPlatform || quaiParam || '').toString().trim() || quaiParam || '1';
// Extraction des origines (si multi-origines / tranches)
let origins = [];
+87 -17
View File
@@ -5,21 +5,34 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0');
const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`;
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`;
if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h');
}
return String(v).slice(0,5).replace(':','h');
@@ -165,7 +178,15 @@ export default function AFLDeparts(){
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
useEffect(() => {
let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('interurbaine');
setResolvedGareId(null);
}
return;
}
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
@@ -195,9 +216,7 @@ export default function AFLDeparts(){
}
} catch (e) { /* ignore */ }
};
if (supabase) {
fetchStationType();
}
fetchStationType();
return () => { mounted = false; };
}, [gareIdParam, gareDisplay]);
@@ -322,11 +341,14 @@ export default function AFLDeparts(){
// eslint-disable-next-line no-unused-vars
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes();
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
}, []);
@@ -638,18 +660,54 @@ export default function AFLDeparts(){
return () => clearInterval(id);
}, []);
// Récupération Supabase — maintenant avec polling pour synchroniser sans recharger la page
// Récupération des données (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu
useEffect(() => {
let mounted = true;
let inFlight = false; // empêche les fetchs concurrents
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => {
if (!mounted) return;
if (inFlight) return; // éviter les appels simultanés si le fetch précédent n'est pas terminé
if (inFlight) return;
inFlight = true;
if (source === 'sncf') {
setLoading(true);
setError(null);
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'departs'
});
if (!mounted) { inFlight = false; return; }
if (sncfErr) {
setError(sncfErr);
} else {
const formatted = (sncfData || []).map(t => ({
...t,
_displayTime: formatTimeNoSeconds(t.heure_depart),
_displayPlatform: t.quai || t.voie || '',
_depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart),
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
}));
setHoraires(formatted.slice(0, 10));
setAllHoraires(formatted);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
} finally {
if (mounted) setLoading(false);
inFlight = false;
}
return;
}
if (!supabase) {
// Supabase non initialisé — avertir et définir un message d'erreur pour l'UI
// eslint-disable-next-line no-console
console.warn('Supabase non initialisé — vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY');
setError('Supabase non initialisé — vérifiez les variables d\'environnement');
setLoading(false);
@@ -1049,7 +1107,7 @@ export default function AFLDeparts(){
const intervalId = setInterval(fetchData, intervalMs);
return () => { mounted = false; clearInterval(intervalId); };
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]);
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]);
// Helpers pour perturbations (memoized plus haut)
@@ -1194,10 +1252,18 @@ export default function AFLDeparts(){
// Calcul des vias
const stopsList = getStops(horaire);
const viaNames = [];
if (Array.isArray(stopsList)) {
if (Array.isArray(stopsList) && stopsList.length > 0) {
let isAfterStart = false;
const hasStartInStops = stopsList.some(s => {
const sName = s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '');
return normalizeName(sName) === normalizeName(stationName || '');
});
if (!hasStartInStops) {
isAfterStart = true;
}
for (const s of stopsList) {
const sName = s.gare_nom || s.nom || s.name || s.station_name || '';
const sName = s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '');
if (!sName) continue;
const norm = normalizeName(sName);
if (norm === normalizeName(stationName || '')) {
@@ -1336,6 +1402,10 @@ export default function AFLDeparts(){
<div className="afl-voie-box-compact">
<BusIcon size="52px" color="#002d62" />
</div>
) : showPlatform ? (
<div className="afl-voie-box-compact">
<div className="afl-voie-value" style={{ fontSize: '58px', lineHeight: 1 }}>{rawPlatform}</div>
</div>
) : null
)}
</div>
+49 -10
View File
@@ -4,19 +4,22 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import './AFLDepartsQuais.css';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import Logo from '../../../components/Logo';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0');
const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}:${mm}`;
}
const parts = String(v).split(':');
if (parts.length >= 2) {
return `${parts[0].padStart(2, '0')}:${parts[1].padStart(2, '0')}`;
const m = String(v).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m[2]}`;
}
return v;
};
@@ -185,9 +188,12 @@ const AFLDepartsQuais = () => {
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2})[h:](\d{2})/);
if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[h:](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
@@ -421,6 +427,39 @@ const AFLDepartsQuais = () => {
const fetchData = useCallback(async () => {
try {
const source = getSourceFromPath(window.location.pathname);
if (source === 'sncf') {
const { data: sncfData } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'departs',
quai: quaiParam
});
if (sncfData && sncfData.length > 0) {
const train = sncfData[0];
const enriched = {
...train,
_rawTime: train.heure_depart,
_displayTime: formatTimeNoSeconds(train.heure_depart),
_status: {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: train.retard_minutes || null
},
_nextStationStatus: {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure")
},
_isToday: true
};
setNextDeparture(enriched);
} else {
setNextDeparture(null);
}
setLoading(false);
return;
}
if (!supabase) return;
const [{ data: horairesData }, { data: subData }, { data: perturbData }, { data: travauxData }] = await Promise.all([
fetchAllRows(supabase.from('horaires').select('*')),
supabase.from('travaux_substitutions').select('*'),
@@ -689,7 +728,7 @@ const AFLDepartsQuais = () => {
console.error(err);
setLoading(false);
}
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, gareParam, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
useEffect(() => {
fetchData();
@@ -780,7 +819,7 @@ const AFLDepartsQuais = () => {
if (gareDisplay && Array.isArray(stopsList)) {
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
}
const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || trainData.quai_reel || trainData.voie || trainData.quai || quaiParam || '').toString().trim();
const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || trainData.quai_reel || trainData.voie || trainData.quai || trainData._displayPlatform || quaiParam || '').toString().trim() || quaiParam || '1';
// Extraction des destinations (prise en compte des trains multi-branches / tranches)
let destinations = [];
@@ -839,7 +878,7 @@ const AFLDepartsQuais = () => {
return sName && (sName === normCurrent || sName.includes(normCurrent) || normCurrent.includes(sName));
});
const rawDownstream = startIdx !== -1 ? stopsList.slice(startIdx + 1) : stopsList.slice(1);
const rawDownstream = startIdx !== -1 ? stopsList.slice(startIdx + 1) : stopsList;
const seen = new Set();
rawDownstream.forEach(s => {
@@ -6,21 +6,34 @@ import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { fetchAllRows } from '../../../lib/fetchAllRows';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0');
const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`;
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`;
if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h');
}
return String(v).slice(0,5).replace(':','h');
@@ -93,7 +106,7 @@ export default function ClassiquesArrivees(){
const [allHoraires, setAllHoraires] = useState([]);
const [perturbations, setPerturbations] = useState([]);
// type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)
const [stationType, setStationType] = useState(null);
const [, setStationType] = useState(null);
const [, setTravaux] = useState([]);
const [, setLoading] = useState(false);
const [, setError] = useState(null);
@@ -107,7 +120,15 @@ export default function ClassiquesArrivees(){
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
useEffect(() => {
let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('interurbaine');
setResolvedGareId(null);
}
return;
}
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
@@ -137,9 +158,7 @@ export default function ClassiquesArrivees(){
}
} catch (e) { /* ignore */ }
};
if (supabase) {
fetchStationType();
}
fetchStationType();
return () => { mounted = false; };
}, [gareIdParam, gareDisplay]);
@@ -263,29 +282,18 @@ export default function ClassiquesArrivees(){
// parse HH:mm or ISO into minutes since midnight (utilisable ailleurs dans le composant)
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes();
return null;
}, []);
// retourne le threshold (en minutes) pour afficher le quai selon le type de gare
const thresholdForStationType = useCallback((typeGare) => {
const tG = String(typeGare || '').trim().toLowerCase();
// Vérification du type de gare
if (tG === 'interurbaine' || tG.includes('interurb')) {
return 360; // 6 heures
}
if (tG === 'ville' || tG.includes('ville')) {
return 30; // 30 minutes
}
// par défaut, 30 minutes
return 30;
}, []);
const formatTimeFromHM = useCallback((totalMinutes) => {
if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return '';
@@ -604,18 +612,57 @@ export default function ClassiquesArrivees(){
return () => clearTimeout(timeoutId);
}, []);
// Récupération Supabase — maintenant avec polling pour synchroniser sans recharger la page
// Récupération des données (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu
useEffect(() => {
let mounted = true;
let inFlight = false; // empêche les fetchs concurrents
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => {
if (!mounted) return;
if (inFlight) return; // éviter les appels simultanés si le fetch précédent n'est pas terminé
if (inFlight) return;
inFlight = true;
if (source === 'sncf') {
setLoading(true);
setError(null);
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'arrivees'
});
if (!mounted) { inFlight = false; return; }
if (sncfErr) {
setError(sncfErr);
} else {
const formatted = (sncfData || []).map(t => {
const timeStr = t.heure_arrivee || t.heure_depart || t.time || '';
return {
...t,
_displayTime: formatTimeNoSeconds(timeStr),
_displayPlatform: t.quai || t.voie || '',
_arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr),
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
};
});
setHoraires(formatted.slice(0, defilementMode ? 20 : 10));
setAllHoraires(formatted);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
} finally {
if (mounted) setLoading(false);
inFlight = false;
}
return;
}
if (!supabase) {
// Supabase non initialisé — avertir et définir un message d'erreur pour l'UI
// eslint-disable-next-line no-console
console.warn('Supabase non initialisé — vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY');
setError('Supabase non initialisé — vérifiez les variables d\'environnement');
setLoading(false);
@@ -1013,7 +1060,7 @@ export default function ClassiquesArrivees(){
const intervalId = setInterval(fetchData, intervalMs);
return () => { mounted = false; clearInterval(intervalId); };
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
// Helpers pour perturbations (memoized plus haut)
@@ -1089,21 +1136,6 @@ export default function ClassiquesArrivees(){
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
let showPlatform = !isSuppressed && Boolean(platform);
if (showPlatform) {
const nowHM = currentTime.getHours()*60 + currentTime.getMinutes();
const sched = (typeof horaire._arrHM === 'number') ? horaire._arrHM : parseTimeHM(horaire.heure_arrivee || horaire.time || horaire.heure || horaire._rawTime || '');
const threshold = thresholdForStationType(stationType);
if (typeof sched === 'number') {
let diff = sched - nowHM;
// Gérer le passage à minuit
if (diff < -720) { // e.g., now 23:00, sched 01:00, diff is ~ -22 hours
diff += 1440; // treat as next day
}
showPlatform = (diff >= 0 && diff <= threshold);
} else {
showPlatform = false;
}
}
const isSub = isSubstitution(horaire);
const infoText = isSub
@@ -6,21 +6,34 @@ import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { fetchAllRows } from '../../../lib/fetchAllRows';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0');
const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`;
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`;
if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h');
}
return String(v).slice(0,5).replace(':','h');
@@ -93,7 +106,7 @@ export default function ClassiquesDeparts(){
const [allHoraires, setAllHoraires] = useState([]);
const [perturbations, setPerturbations] = useState([]);
// type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)
const [stationType, setStationType] = useState(null);
const [, setStationType] = useState(null);
const [, setTravaux] = useState([]);
const [, setLoading] = useState(false);
const [, setError] = useState(null);
@@ -107,7 +120,15 @@ export default function ClassiquesDeparts(){
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
useEffect(() => {
let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('interurbaine');
setResolvedGareId(null);
}
return;
}
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
@@ -137,9 +158,7 @@ export default function ClassiquesDeparts(){
}
} catch (e) { /* ignore */ }
};
if (supabase) {
fetchStationType();
}
fetchStationType();
return () => { mounted = false; };
}, [gareIdParam, gareDisplay]);
@@ -263,29 +282,18 @@ export default function ClassiquesDeparts(){
// parse HH:mm or ISO into minutes since midnight (utilisable ailleurs dans le composant)
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes();
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
}, []);
// retourne le threshold (en minutes) pour afficher le quai selon le type de gare
const thresholdForStationType = useCallback((typeGare) => {
const tG = String(typeGare || '').trim().toLowerCase();
// Vérification du type de gare
if (tG === 'interurbaine' || tG.includes('interurb')) {
return 360; // 6 heures
}
if (tG === 'ville' || tG.includes('ville')) {
return 30; // 30 minutes
}
// par défaut, 30 minutes
return 30;
}, []);
const formatTimeFromHM = useCallback((totalMinutes) => {
if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return '';
@@ -604,18 +612,54 @@ export default function ClassiquesDeparts(){
return () => clearTimeout(timeoutId);
}, []);
// Récupération Supabase — maintenant avec polling pour synchroniser sans recharger la page
// Récupération des données (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu
useEffect(() => {
let mounted = true;
let inFlight = false; // empêche les fetchs concurrents
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => {
if (!mounted) return;
if (inFlight) return; // éviter les appels simultanés si le fetch précédent n'est pas terminé
if (inFlight) return;
inFlight = true;
if (source === 'sncf') {
setLoading(true);
setError(null);
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'departs'
});
if (!mounted) { inFlight = false; return; }
if (sncfErr) {
setError(sncfErr);
} else {
const formatted = (sncfData || []).map(t => ({
...t,
_displayTime: formatTimeNoSeconds(t.heure_depart),
_displayPlatform: t.quai || t.voie || '',
_depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart),
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
}));
setHoraires(formatted.slice(0, defilementMode ? 20 : 10));
setAllHoraires(formatted);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
} finally {
if (mounted) setLoading(false);
inFlight = false;
}
return;
}
if (!supabase) {
// Supabase non initialisé — avertir et définir un message d'erreur pour l'UI
// eslint-disable-next-line no-console
console.warn('Supabase non initialisé — vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY');
setError('Supabase non initialisé — vérifiez les variables d\'environnement');
setLoading(false);
@@ -1013,7 +1057,7 @@ export default function ClassiquesDeparts(){
const intervalId = setInterval(fetchData, intervalMs);
return () => { mounted = false; clearInterval(intervalId); };
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
// Helpers pour perturbations (memoized plus haut)
@@ -1089,21 +1133,6 @@ export default function ClassiquesDeparts(){
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
let showPlatform = !isSuppressed && Boolean(platform);
if (showPlatform) {
const nowHM = currentTime.getHours()*60 + currentTime.getMinutes();
const sched = (typeof horaire._depHM === 'number') ? horaire._depHM : parseTimeHM(horaire.heure_depart || horaire.time || horaire.heure || horaire._rawTime || '');
const threshold = thresholdForStationType(stationType);
if (typeof sched === 'number') {
let diff = sched - nowHM;
// Gérer le passage à minuit
if (diff < -720) { // e.g., now 23:00, sched 01:00, diff is ~ -22 hours
diff += 1440; // treat as next day
}
showPlatform = (diff >= 0 && diff <= threshold);
} else {
showPlatform = false;
}
}
const isSub = isSubstitution(horaire);
const infoText = isSub
@@ -4,6 +4,8 @@ import supabase from '../../../../lib/supabase';
import { fetchAllRows } from '../../../../lib/fetchAllRows';
import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../../services/railwayService';
import { getSncfTimetable } from '../../../../services/sncfApi';
function Clock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
@@ -46,7 +48,7 @@ export default function ClassiquesArriveesQuais() {
const [loadingHoraire, setLoadingHoraire] = useState(false);
const [, setHoraireError] = useState(null);
const [, setCandidatesCount] = useState(0);
const [stationType, setStationType] = useState(null);
const [, setStationType] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date());
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
@@ -111,11 +113,14 @@ export default function ClassiquesArriveesQuais() {
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes();
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
}, []);
@@ -123,15 +128,26 @@ export default function ClassiquesArriveesQuais() {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2,'0');
const hh = String(tryDate.getHours() % 24).padStart(2,'0');
const mm = String(tryDate.getMinutes()).padStart(2,'0');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`;
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`;
if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h');
}
return String(v).slice(0,5).replace(':','h');
@@ -145,13 +161,7 @@ export default function ClassiquesArriveesQuais() {
return `${hh}h${mm}`;
}, []);
const thresholdForStationType = useCallback((type) => {
if (!type) return 0;
const t = String(type).trim().toLowerCase();
if (t === 'interurbaine' || t.includes('interurb')) return 360;
if (t === 'ville' || t.includes('ville')) return 30;
return 0;
}, []);
@@ -457,6 +467,58 @@ export default function ClassiquesArriveesQuais() {
setLoadingHoraire(true);
setHoraireError(null);
setSelectedHoraire(null);
const source = getSourceFromPath(window.location.pathname);
if (source === 'sncf') {
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplayParam || gareParam,
mode: 'arrivees',
quai: quaiParam
});
if (!mounted) return;
if (sncfErr) {
setHoraireError(sncfErr);
} else if (sncfData && sncfData.length > 0) {
const train = sncfData[0];
const now = new Date();
const nowHM = now.getHours() * 60 + now.getMinutes();
const arrHM = parseTimeHM(train.heure_arrivee || train.heure_depart);
const chosen = {
horaire: {
...train,
_status: train._status || {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: train.retard_minutes || null,
causeDetails: train.cause_retard || null
}
},
stop: {
gare_nom: train.gare_arrivee_nom || gareDisplayParam || gareParam,
heure_arrivee: train.heure_arrivee || train.heure_depart,
quai: train.quai || quaiParam,
quai_reel: train.quai || quaiParam
},
arrHM: arrHM != null ? arrHM : nowHM,
rawTime: train.heure_arrivee || train.heure_depart,
circulatesToday: true,
circulatesTomorrow: false,
isTomorrow: false
};
setSelectedHoraire(chosen);
setCandidatesCount(sncfData.length);
setStationType('interurbaine');
}
} catch (e) {
if (mounted) setHoraireError(e.message || String(e));
} finally {
if (mounted) setLoadingHoraire(false);
}
return;
}
if (!supabase) {
setHoraireError('Supabase non initialisé');
setLoadingHoraire(false);
@@ -650,16 +712,10 @@ export default function ClassiquesArriveesQuais() {
loadNextForQuai();
return () => { mounted = false; };
}, [gareDisplayParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]);
}, [gareDisplayParam, gareParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]);
// Rail is handled purely in CSS for better performance and alignment
const showPlatform = (() => {
const sched = selectedHoraire ? selectedHoraire.arrHM : null;
if (sched === null || !stationType) return true;
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
const threshold = thresholdForStationType(stationType);
return nowHM >= (sched - threshold);
})();
const showPlatform = true;
if (!loadingHoraire && !selectedHoraire) {
return (
@@ -789,7 +845,7 @@ export default function ClassiquesArriveesQuais() {
try { const p = JSON.parse(horaire.gares_desservies); if (Array.isArray(p)) stops = p; } catch (e) { /* ignore */ }
}
}
if (!Array.isArray(stops) || stops.length === 0) stops = mock.stops || [];
if (!Array.isArray(stops) || stops.length === 0) stops = [];
const labeled = stops.map((stop, idx) => {
let label = '';
@@ -870,7 +926,7 @@ export default function ClassiquesArriveesQuais() {
{/* BOTTOM FOOTER: Full Width */}
<div className="aff-quais-arrivees-redesign__footer">
<div className="aff-quais-arrivees-redesign__footer-left">
ain.
{quaiParam ? `Voie ${quaiParam}` : (selectedHoraire?.horaire?.quai ? `Voie ${selectedHoraire.horaire.quai}` : (selectedHoraire?.stop?.quai ? `Voie ${selectedHoraire.stop.quai}` : ''))}
</div>
<div className="aff-quais-arrivees-redesign__footer-right">
<Clock />
@@ -4,6 +4,8 @@ import supabase from '../../../../lib/supabase';
import { fetchAllRows } from '../../../../lib/fetchAllRows';
import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../../services/railwayService';
import { getSncfTimetable } from '../../../../services/sncfApi';
function Clock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
@@ -46,7 +48,7 @@ export default function ClassiquesDepartsQuais() {
const [loadingHoraire, setLoadingHoraire] = useState(false);
const [, setHoraireError] = useState(null);
const [, setCandidatesCount] = useState(0);
const [stationType, setStationType] = useState(null);
const [, setStationType] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date());
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
@@ -111,11 +113,14 @@ export default function ClassiquesDepartsQuais() {
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes();
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
}, []);
@@ -123,15 +128,26 @@ export default function ClassiquesDepartsQuais() {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2,'0');
const hh = String(tryDate.getHours() % 24).padStart(2,'0');
const mm = String(tryDate.getMinutes()).padStart(2,'0');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`;
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`;
if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h');
}
return String(v).slice(0,5).replace(':','h');
@@ -145,13 +161,7 @@ export default function ClassiquesDepartsQuais() {
return `${hh}h${mm}`;
}, []);
const thresholdForStationType = useCallback((type) => {
if (!type) return 0;
const t = String(type).trim().toLowerCase();
if (t === 'interurbaine' || t.includes('interurb')) return 360;
if (t === 'ville' || t.includes('ville')) return 30;
return 0;
}, []);
@@ -457,6 +467,58 @@ export default function ClassiquesDepartsQuais() {
setLoadingHoraire(true);
setHoraireError(null);
setSelectedHoraire(null);
const source = getSourceFromPath(window.location.pathname);
if (source === 'sncf') {
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplayParam || gareParam,
mode: 'departs',
quai: quaiParam
});
if (!mounted) return;
if (sncfErr) {
setHoraireError(sncfErr);
} else if (sncfData && sncfData.length > 0) {
const train = sncfData[0];
const now = new Date();
const nowHM = now.getHours() * 60 + now.getMinutes();
const depHM = parseTimeHM(train.heure_depart);
const chosen = {
horaire: {
...train,
_status: train._status || {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: train.retard_minutes || null,
causeDetails: train.cause_retard || null
}
},
stop: {
gare_nom: train.gare_depart_nom || gareDisplayParam || gareParam,
heure_depart: train.heure_depart,
quai: train.quai || quaiParam,
quai_reel: train.quai || quaiParam
},
depHM: depHM != null ? depHM : nowHM,
rawTime: train.heure_depart,
circulatesToday: true,
circulatesTomorrow: false,
isTomorrow: false
};
setSelectedHoraire(chosen);
setCandidatesCount(sncfData.length);
setStationType('interurbaine');
}
} catch (e) {
if (mounted) setHoraireError(e.message || String(e));
} finally {
if (mounted) setLoadingHoraire(false);
}
return;
}
if (!supabase) {
setHoraireError('Supabase non initialisé');
setLoadingHoraire(false);
@@ -648,16 +710,10 @@ export default function ClassiquesDepartsQuais() {
loadNextForQuai();
return () => { mounted = false; };
}, [gareDisplayParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]);
}, [gareDisplayParam, gareParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]);
// Rail is handled purely in CSS for better performance and alignment
const showPlatform = (() => {
const sched = selectedHoraire ? selectedHoraire.depHM : null;
if (sched === null || !stationType) return true;
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
const threshold = thresholdForStationType(stationType);
return nowHM >= (sched - threshold);
})();
const showPlatform = true;
if (!loadingHoraire && !selectedHoraire) {
return (
@@ -786,7 +842,7 @@ export default function ClassiquesDepartsQuais() {
try { const p = JSON.parse(horaire.gares_desservies); if (Array.isArray(p)) stops = p; } catch (e) { /* ignore */ }
}
}
if (!Array.isArray(stops) || stops.length === 0) stops = mock.stops || [];
if (!Array.isArray(stops) || stops.length === 0) stops = [];
const labeled = stops.map((stop, idx) => {
let label = '';
@@ -868,7 +924,7 @@ export default function ClassiquesDepartsQuais() {
{/* BOTTOM FOOTER: Full Width */}
<div className="aff-quais-departs-redesign__footer">
<div className="aff-quais-departs-redesign__footer-left">
ain.
{quaiParam ? `Voie ${quaiParam}` : (selectedHoraire?.horaire?.quai ? `Voie ${selectedHoraire.horaire.quai}` : (selectedHoraire?.stop?.quai ? `Voie ${selectedHoraire.stop.quai}` : ''))}
</div>
<div className="aff-quais-departs-redesign__footer-right">
<Clock />
+74 -11
View File
@@ -5,21 +5,34 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HH:MM avec deux points (ex: 22:39)
const formatTimeWithColon = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0');
const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}:${mm}`;
}
if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2, '0')}:${m[2]}`;
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}:${m2[2]}`;
if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m3[2]}`;
}
return v.slice(0, 5).replace('h', ':');
}
return String(v).slice(0, 5).replace('h', ':');
@@ -445,7 +458,15 @@ export default function EVAArrivees() {
// Récupération type de gare
useEffect(() => {
let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('ville');
setResolvedGareId(null);
}
return;
}
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
@@ -475,9 +496,7 @@ export default function EVAArrivees() {
}
} catch (e) { /* ignore */ }
};
if (supabase) {
fetchStationType();
}
fetchStationType();
return () => { mounted = false; };
}, [gareIdParam, gareDisplay]);
@@ -831,14 +850,55 @@ export default function EVAArrivees() {
});
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
// Récupération Supabase avec polling
// Récupération des données (SNCF GTFS ou Supabase) avec polling
useEffect(() => {
let mounted = true;
let inFlight = false;
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => {
if (!mounted || inFlight) return;
inFlight = true;
if (source === 'sncf') {
setLoading(true);
setError(null);
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'arrivees'
});
if (!mounted) { inFlight = false; return; }
if (sncfErr) {
setError(sncfErr);
} else {
const formatted = (sncfData || []).map(t => {
const timeStr = t.heure_arrivee || t.heure_depart || t.time || '';
return {
...t,
_displayTime: formatTimeWithColon(timeStr),
_displayPlatform: t.quai || t.voie || '',
_arrHM: t._arrHM || 0,
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
};
});
setHoraires(formatted.slice(0, 10));
setAllHoraires(formatted);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
} finally {
if (mounted) setLoading(false);
inFlight = false;
}
return;
}
if (!supabase) {
setLoading(false);
inFlight = false;
@@ -936,9 +996,12 @@ export default function EVAArrivees() {
const parseTimeHM = (s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
@@ -1129,7 +1192,7 @@ export default function EVAArrivees() {
fetchData();
const intervalId = setInterval(fetchData, 15000);
return () => { mounted = false; clearInterval(intervalId); };
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]);
}, [gareDisplay, gareParam, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]);
// Alternance retard / texte toutes les 2s
const [alternateDelay, setAlternateDelay] = useState(false);
+71 -11
View File
@@ -5,21 +5,34 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HH:MM avec deux points (ex: 22:39)
const formatTimeWithColon = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0');
const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}:${mm}`;
}
if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2, '0')}:${m[2]}`;
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}:${m2[2]}`;
if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m3[2]}`;
}
return v.slice(0, 5).replace('h', ':');
}
return String(v).slice(0, 5).replace('h', ':');
@@ -441,7 +454,15 @@ export default function EVADeparts() {
// Récupération type de gare
useEffect(() => {
let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('ville');
setResolvedGareId(null);
}
return;
}
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
@@ -471,9 +492,7 @@ export default function EVADeparts() {
}
} catch (e) { /* ignore */ }
};
if (supabase) {
fetchStationType();
}
fetchStationType();
return () => { mounted = false; };
}, [gareIdParam, gareDisplay]);
@@ -827,14 +846,52 @@ export default function EVADeparts() {
});
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
// Récupération Supabase avec polling
// Récupération des données (SNCF GTFS ou Supabase) avec polling
useEffect(() => {
let mounted = true;
let inFlight = false;
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => {
if (!mounted || inFlight) return;
inFlight = true;
if (source === 'sncf') {
setLoading(true);
setError(null);
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'departs'
});
if (!mounted) { inFlight = false; return; }
if (sncfErr) {
setError(sncfErr);
} else {
const formatted = (sncfData || []).map(t => ({
...t,
_displayTime: formatTimeWithColon(t.heure_depart),
_displayPlatform: t.quai || t.voie || '',
_depHM: t._depHM || 0,
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
}));
setHoraires(formatted.slice(0, 10));
setAllHoraires(formatted);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
} finally {
if (mounted) setLoading(false);
inFlight = false;
}
return;
}
if (!supabase) {
setLoading(false);
inFlight = false;
@@ -952,9 +1009,12 @@ export default function EVADeparts() {
const parseTimeHM = (s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
@@ -1145,7 +1205,7 @@ export default function EVADeparts() {
fetchData();
const intervalId = setInterval(fetchData, 15000);
return () => { mounted = false; clearInterval(intervalId); };
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]);
}, [gareDisplay, gareParam, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]);
// Alternance retard / texte toutes les 2s
const [alternateDelay, setAlternateDelay] = useState(false);