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
@@ -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