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:
1 parent
0e0d8c435f
commit
72de543dc6
18 files changed
+1550
-544
No files matched your search
+106
-77
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user