import React, { useEffect, useState, useCallback } from 'react'; import './ClassiquesArriveesQuais.css'; import supabase from '../../../../lib/supabase'; import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime'; import useAutoAnnonce from '../../../../hooks/useAutoAnnonce'; function Clock() { const [now, setNow] = useState(new Date()); useEffect(() => { const id = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(id); }, []); const hh = String(now.getHours()).padStart(2,'0'); const mm = String(now.getMinutes()).padStart(2,'0'); const ss = String(now.getSeconds()).padStart(2,'0'); return (
{hh} {mm} {ss}
); } export default function ClassiquesArriveesQuais() { const paramsForThis = new URLSearchParams(window.location.search || ''); const gareParam = paramsForThis.get('gare'); const gareIdParam = paramsForThis.get('gare_id') || paramsForThis.get('gareId'); const quaiParam = paramsForThis.get('quai'); const gareDisplayParam = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g,' ')).replace(/-/g,' ') : null; const mock = { time: '16h44', status: "à l'heure", trainType: 'Fluo', trainNumber: '891811', destinationBig: 'Seurre', destinationStation: '', stops: [ 'Ouges','Saulon','Longecourt','Aiserey','Brazey-en-Plaine','Saint-Jean-de-Losne','Chaugey','Pagny','Seurre','Mervans','Louhans','Saint-Amour','Bourg-en-Bresse' ], }; const [selectedHoraire, setSelectedHoraire] = useState(null); const [loadingHoraire, setLoadingHoraire] = useState(false); const [, setHoraireError] = useState(null); const [, setCandidatesCount] = useState(0); const [stationType, setStationType] = useState(null); const [currentTime, setCurrentTime] = useState(new Date()); const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null); useAnnonceSonoreRealtime(resolvedGareId); useAutoAnnonce(selectedHoraire ? [selectedHoraire] : [], currentTime, true); const [alternateDelayPhase, setAlternateDelayPhase] = useState(true); const [alternateCancelPhase, setAlternateCancelPhase] = useState(true); useEffect(() => { const id = setInterval(() => setCurrentTime(new Date()), 1000); return () => clearInterval(id); }, []); useEffect(() => { const interval = setInterval(() => { setAlternateDelayPhase(prev => !prev); }, 5000); return () => clearInterval(interval); }, []); useEffect(() => { const interval = setInterval(() => { setAlternateCancelPhase(prev => !prev); }, 5000); return () => clearInterval(interval); }, []); const [perturbations, setPerturbations] = useState([]); const [, setTravaux] = useState([]); const weekdayNameForColumn = useCallback((d) => { const i = d.getDay(); switch(i) { case 1: return 'circule_lundi'; case 2: return 'circule_mardi'; case 3: return 'circule_mercredi'; case 4: return 'circule_jeudi'; case 5: return 'circule_vendredi'; case 6: return 'circule_samedi'; case 0: return 'circule_dimanche'; default: return null; } }, []); const isPublicHoliday = useCallback((d) => { const y = d.getFullYear(); const m = d.getMonth() + 1; const day = d.getDate(); const s = `${y}-${String(m).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const holidays = [ `${y}-01-01`, `${y}-05-01`, `${y}-05-08`, `${y}-07-14`, `${y}-08-15`, `${y}-11-01`, `${y}-11-11`, `${y}-12-25`, ]; if (y === 2026) { holidays.push('2026-04-06'); holidays.push('2026-05-14'); holidays.push('2026-05-25'); } return holidays.includes(s); }, []); const circulatesOn = useCallback((h, d) => { if (isPublicHoliday(d)) { const v = h['circule_jours_feries']; if (typeof v === 'boolean') return v; if (typeof v === 'number') return v === 1; if (typeof v === 'string') { const s = v.trim().toLowerCase(); return s === 'true' || s === '1' || s === 't' || s === 'y' || s === 'yes'; } return (typeof v === 'undefined' || v === null) ? false : Boolean(v); } const col = weekdayNameForColumn(d); if (!col) return true; const v = h[col]; if (typeof v === 'boolean') return v; if (typeof v === 'number') return v === 1; if (typeof v === 'string') { const s = v.trim().toLowerCase(); return s === 'true' || s === '1' || s === 't' || s === 'y' || s === 'yes'; } return (typeof v === 'undefined' || v === null) ? true : Boolean(v); }, [isPublicHoliday, weekdayNameForColumn]); const normalizeName = useCallback((s) => { if (!s) return ''; try { return String(s).normalize('NFD').replace(/\p{M}/gu,'').toLowerCase().trim().replace(/[-_]+/g,' ').replace(/\s+/g,' '); } catch (e) { return String(s).toLowerCase().trim(); } }, []); 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); const d = new Date(s); if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes(); return null; }, []); const formatTimeNoSeconds = useCallback((v) => { if (v == null) return ''; const tryDate = new Date(v); if (!isNaN(tryDate.getTime())) { const hh = String(tryDate.getHours()).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]}`; const m2 = v.match(/T(\d{2}):(\d{2})/); if (m2) return `${m2[1]}h${m2[2]}`; return v.slice(0,5).replace(':','h'); } return String(v).slice(0,5).replace(':','h'); }, []); const formatTimeFromHM = useCallback((totalMinutes) => { if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return ''; const m = ((totalMinutes % 1440) + 1440) % 1440; const hh = String(Math.floor(m / 60)).padStart(2, '0'); const mm = String(m % 60).padStart(2, '0'); return `${hh}h${mm}`; }, []); const 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; }, []); const getStops = (h) => { if (!h) return []; if (Array.isArray(h.stops) && h.stops.length) return h.stops; if (Array.isArray(h.gares_desservies) && h.gares_desservies.length) return h.gares_desservies; if (Array.isArray(h.garesDesservies) && h.garesDesservies.length) return h.garesDesservies; if (Array.isArray(h.arrets) && h.arrets.length) return h.arrets; const candidates = ['stops','gares_desservies','garesDesservies','gares_desservies_json','arrets','gares_desservies_json']; for (const key of candidates) { const raw = h[key]; if (!raw) continue; if (Array.isArray(raw)) return raw; if (typeof raw === 'string') { try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) return parsed; } catch (e) {} } } return []; }; const safeParseArrets = (p) => { const raw = p && (p.perturbations_arrets || p.arrets || p.arrets_perturbations); if (!raw) return []; if (Array.isArray(raw)) return raw; try { const parsed = JSON.parse(raw); if (parsed && typeof parsed === 'object') { if (Array.isArray(parsed.arrets)) return parsed.arrets; return parsed; } return []; } catch (e) { return []; } }; const parsePerturbArretsObject = useCallback((p) => { const raw = p && (p.perturbations_arrets || p.arrets || p.arrets_perturbations); if (!raw) return { arrets: [], jours: null }; if (typeof raw === 'object') { if (Array.isArray(raw.arrets)) return { arrets: raw.arrets, jours: raw.jours_perturbation || raw.jours || null }; if (Array.isArray(raw)) return { arrets: raw, jours: null }; return { arrets: [], jours: null }; } try { const parsed = JSON.parse(raw); if (parsed && typeof parsed === 'object') { return { arrets: Array.isArray(parsed.arrets) ? parsed.arrets : (Array.isArray(parsed) ? parsed : []), jours: parsed.jours_perturbation || parsed.jours || null }; } return { arrets: [], jours: null }; } catch (e) { return { arrets: [], jours: null }; } }, []); const getPerturbationJours = useCallback((p) => { if (!p) return null; const candidate = p.jours_perturbation || p.jours || null; const result = new Set(); const pushDate = (d) => { if (!d) return; try { const dd = new Date(d); if (!isNaN(dd.getTime())) result.add(dd.toISOString().slice(0,10)); } catch(e){} }; if (candidate) { if (Array.isArray(candidate)) candidate.forEach(item => pushDate(item.date || item)); else if (typeof candidate === 'string') { try { const parsed = JSON.parse(candidate); if (Array.isArray(parsed)) parsed.forEach(it => pushDate(it.date || it)); } catch (e) { pushDate(candidate); } } else if (typeof candidate === 'object') { if (Array.isArray(candidate)) candidate.forEach(it => pushDate(it.date || it)); else pushDate(candidate.date || null); } } try { const parsedObj = parsePerturbArretsObject(p); const jp = parsedObj.jours; if (jp) { if (Array.isArray(jp)) jp.forEach(it => pushDate(it.date || it)); else if (typeof jp === 'string') { try { const parsed = JSON.parse(jp); if (Array.isArray(parsed)) parsed.forEach(it => pushDate(it.date || it)); } catch (e) { pushDate(jp); } } else if (typeof jp === 'object') pushDate(jp.date || null); } } catch (e) {} return result.size ? result : null; }, [parsePerturbArretsObject]); const parseDelayMinutesFromArret = useCallback((arret) => { if (!arret) return null; const keys = ['retard_minutes','retard','delay_minutes','delay','retard_min']; for (const k of keys) { if (typeof arret[k] !== 'undefined' && arret[k] !== null && arret[k] !== '') { const n = Number(arret[k]); if (!isNaN(n)) return Math.round(n); } } return null; }, []); const buildPerturbMap = useCallback((list = []) => { const idx = { byHoraire: {}, byNumber: {}, byStopId: {}, byStopName: {}, global: [] }; (list || []).forEach(p => { const horaireId = p.horaire_id || p.horaire || p.horaireId || p.horaireID; if (horaireId != null) { const k = String(horaireId); idx.byHoraire[k] = idx.byHoraire[k] || []; idx.byHoraire[k].push(p); } const number = p.number || p.train_number || p.train || p.numero || p.numero_train; if (number != null) { const kn = String(number); idx.byNumber[kn] = idx.byNumber[kn] || []; idx.byNumber[kn].push(p); } const arrets = safeParseArrets(p); if (Array.isArray(arrets) && arrets.length) { arrets.forEach(a => { const stopId = a.gare_id || a.gareId || a.id || a.station_id || a.stationId || a.uic || a.code_uic || a.gare_uic || a.code; const stopName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom; if (stopId != null) { const ks = String(stopId); idx.byStopId[ks] = idx.byStopId[ks] || []; idx.byStopId[ks].push({ perturbation: p, arret: a }); } if (stopName) { const key = normalizeName(stopName); idx.byStopName[key] = idx.byStopName[key] || []; idx.byStopName[key].push({ perturbation: p, arret: a }); } }); } if ((horaireId == null && number == null && (!Array.isArray(arrets) || !arrets.length))) { idx.global.push(p); } }); return idx; }, [normalizeName]); const findPerturbsForHoraireAndStop = useCallback((horaire, stationId, stationName, idx) => { const res = []; const map = idx || buildPerturbMap(perturbations || []); const hId = horaire && (horaire.id || horaire.horaire_id || horaire.numero || horaire.numero_train); const number = horaire && (horaire.numero_train || horaire.number || horaire.numero || horaire.train_number); if (hId != null) { const kh = String(hId); if (map.byHoraire && map.byHoraire[kh]) res.push(...map.byHoraire[kh]); } if (number != null) { const kn = String(number); if (map.byNumber && map.byNumber[kn]) res.push(...map.byNumber[kn]); } if (stationId != null) { const ks = String(stationId); if (map.byStopId && map.byStopId[ks]) map.byStopId[ks].forEach(x => res.push(x.perturbation)); } if (stationName && map.byStopName) { const key = normalizeName(stationName); if (map.byStopName[key]) map.byStopName[key].forEach(x => res.push(x.perturbation)); try { const needle = key; for (const existingKey of Object.keys(map.byStopName || {})) { if (existingKey === needle) continue; if (existingKey.includes(needle) || needle.includes(existingKey)) { map.byStopName[existingKey].forEach(x => res.push(x.perturbation)); } } } catch (e) {} } return Array.from(new Set(res)); }, [buildPerturbMap, perturbations, normalizeName]); const computeStatusForHoraireAtStop = useCallback((horaire, stationId, stationName, idx, serviceDate = null) => { const found = findPerturbsForHoraireAndStop(horaire, stationId, stationName, idx) || []; const toYMD = (v) => { if (!v) return null; const d = new Date(v); if (isNaN(d.getTime())) return null; return d.toISOString().slice(0,10); }; let horaireYmd = toYMD(horaire && (horaire._date || horaire.date || horaire.date_service || horaire.service_date)); if (!horaireYmd && serviceDate) horaireYmd = serviceDate; let maxDelay = null; for (const p of found) { try { if (horaireYmd) { const pDate = toYMD(p.date || p.date_service); const db = toYMD(p.date_debut || p.start_date); const df = toYMD(p.date_fin || p.end_date); if (pDate && pDate !== horaireYmd) continue; if (db && df && !(horaireYmd >= db && horaireYmd <= df)) continue; } } catch (e) {} try { const joursSet = getPerturbationJours(p); if (joursSet && horaireYmd) { if (!joursSet.has(horaireYmd)) continue; } } catch (e) {} const parsedObj = parsePerturbArretsObject(p) || { arrets: [], jours: null }; const arrets = parsedObj.arrets || []; let matched = false; for (const a of arrets) { const aId = a.gare_id || a.gareId || a.id || a.station_id || a.stationId || a.uic || a.code; const aName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom; const na = normalizeName(aName || ''); const ns = normalizeName(stationName || ''); const nameMatches = stationName && (na === ns || na.includes(ns) || ns.includes(na)); if ((stationId != null && String(aId) === String(stationId)) || nameMatches) { matched = true; const t = (a.type_perturbation || a.type || '').toString().toLowerCase(); if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) return { label: 'supprimé', detail: '', delayMinutes: null }; if (t.includes('retard') || t.includes('delay') || /retard/.test(t)) { const d = parseDelayMinutesFromArret(a) || parseDelayMinutesFromArret(p) || 0; if (maxDelay == null || d > maxDelay) maxDelay = d; } } } if (!matched) { const ref = p.horaire_id || p.horaire || p.horaireId || p.numero_train || p.train_number || p.numero; const numberRef = p.number || p.train_number || p.numero || p.numero_train; const appliesByRef = ref && (String(ref) === String(horaire.id) || String(ref) === String(horaire.numero_train) || String(ref) === String(horaire.numero)); const appliesByNumber = numberRef && (String(numberRef) === String(horaire.numero_train) || String(numberRef) === String(horaire.numero) || String(numberRef) === String(horaire.number)); const isGlobal = (!Array.isArray(arrets) || arrets.length === 0) && !ref && !numberRef; if (appliesByRef || appliesByNumber || isGlobal) { const t = (p.type_perturbation || p.type || '').toString().toLowerCase(); if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) return { label: 'supprimé', detail: '', delayMinutes: null }; if (t.includes('retard') || t.includes('delay') || /retard/.test(t)) { const d = parseDelayMinutesFromArret(p) || 0; if (maxDelay == null || d > maxDelay) maxDelay = d; } } } } if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay }; return { label: "à l'heure", detail: '', delayMinutes: null }; }, [findPerturbsForHoraireAndStop, parseDelayMinutesFromArret, getPerturbationJours, normalizeName, parsePerturbArretsObject]); const perturbMap = React.useMemo(() => buildPerturbMap(perturbations || []), [perturbations, buildPerturbMap]); const serviceDate = React.useMemo(() => { const todayYmd = new Date().toLocaleDateString('en-CA'); const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); const tomorrowYmd = tomorrow.toLocaleDateString('en-CA'); return selectedHoraire && selectedHoraire.isTomorrow ? tomorrowYmd : todayYmd; }, [selectedHoraire]); useEffect(() => { let mounted = true; let inFlight = false; const fetchPerturb = async () => { if (!mounted) return; if (inFlight) return; inFlight = true; try { if (!supabase) return; const [{ data: perturbData, error: pError }, { data: travauxData, error: tError }] = await Promise.all([ supabase.from('perturbations').select('*'), supabase.from('travaux').select('*') ]); if (!mounted) return; if (pError || tError) { } else { const todayYmd = new Date().toLocaleDateString('en-CA'); const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); const tomorrowYmd = tomorrow.toLocaleDateString('en-CA'); const filteredPerturb = (perturbData || []).filter(p => { try { const toYMD = (v) => { if (!v) return null; const d = new Date(v); if (isNaN(d.getTime())) return null; return d.toISOString().slice(0,10); }; const pDate = toYMD(p.date || p.date_service); const db = toYMD(p.date_debut || p.start_date); const df = toYMD(p.date_fin || p.end_date); const appliesToToday = (!pDate || pDate === todayYmd) && (!db || !df || (todayYmd >= db && todayYmd <= df)); const appliesToTomorrow = (!pDate || pDate === tomorrowYmd) && (!db || !df || (tomorrowYmd >= db && tomorrowYmd <= df)); if (!appliesToToday && !appliesToTomorrow) return false; try { const joursSet = getPerturbationJours(p); if (joursSet) { const hasToday = joursSet.has(todayYmd); const hasTomorrow = joursSet.has(tomorrowYmd); if (!hasToday && !hasTomorrow) return false; } } catch (e) {} return true; } catch (e) { return true; } }); setPerturbations(filteredPerturb || []); setTravaux(travauxData || []); } } catch (e) { } finally { inFlight = false; } }; fetchPerturb(); const id = setInterval(fetchPerturb, 15000); return () => { mounted = false; clearInterval(id); }; }, [getPerturbationJours]); useEffect(() => { if (!selectedHoraire || !selectedHoraire.horaire) return; const horaire = selectedHoraire.horaire; const stop = selectedHoraire.stop || null; const stationId = stop ? (stop.gare_id || stop.gareId || stop.id || stop.stop_id || stop.stopId) : null; const stationName = stop ? (typeof stop === 'string' ? stop : (stop.gare_nom || stop.nom || stop.name || stop.stop_name || '')) : null; try { const map = buildPerturbMap(perturbations || []); const todayYmd = new Date().toLocaleDateString('en-CA'); const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); const tomorrowYmd = tomorrow.toLocaleDateString('en-CA'); const serviceDate = selectedHoraire.isTomorrow ? tomorrowYmd : todayYmd; const status = computeStatusForHoraireAtStop(horaire, stationId, stationName, map, serviceDate); const originalBase = parseTimeHM(selectedHoraire.rawTime || horaire.heure_arrivee || horaire.time || horaire.heure); const displayTimeOriginal = (typeof originalBase === 'number') ? formatTimeFromHM(originalBase) : (selectedHoraire.rawTime ? formatTimeNoSeconds(selectedHoraire.rawTime) : (horaire.heure_arrivee || horaire.time || horaire.heure ? formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure) : null)); let updatedTime = null; if (status && typeof status.delayMinutes === 'number' && !Number.isNaN(status.delayMinutes) && typeof originalBase === 'number') { updatedTime = formatTimeFromHM(originalBase + status.delayMinutes); } // Guard to prevent infinite render loop: only set selectedHoraire if status or time actually changed const statusChanged = !horaire._status || horaire._status.label !== status.label || horaire._status.delayMinutes !== status.delayMinutes; const timeChanged = selectedHoraire._displayTime !== displayTimeOriginal || selectedHoraire._updatedTime !== updatedTime; if (statusChanged || timeChanged) { setSelectedHoraire(prev => ({ ...(prev||{}), horaire: { ...(prev && prev.horaire ? prev.horaire : {}), _status: status }, _displayTime: displayTimeOriginal, _updatedTime: updatedTime })); } } catch (e) {} }, [selectedHoraire, perturbations, buildPerturbMap, computeStatusForHoraireAtStop, parseTimeHM, formatTimeFromHM, formatTimeNoSeconds]); useEffect(() => { let mounted = true; async function loadNextForQuai() { setLoadingHoraire(true); setHoraireError(null); setSelectedHoraire(null); if (!supabase) { setHoraireError('Supabase non initialisé'); setLoadingHoraire(false); return; } try { let fetchedType = null; try { let fetchedId = gareIdParam || null; if (gareIdParam) { const { data: g } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).single(); if (g) { fetchedType = g.type; fetchedId = g.id; } } else if (gareDisplayParam) { const { data: gdata } = await supabase.from('gares').select('id,type,nom').ilike('nom', `%${gareDisplayParam}%`); if (gdata && gdata.length) { const exactMatch = gdata.find(g => { const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim(); const n2 = String(gareDisplayParam || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim(); return n1 === n2; }); const chosen = exactMatch || gdata[0]; fetchedType = chosen ? chosen.type : null; fetchedId = chosen ? chosen.id : null; } } if (mounted) { setStationType(fetchedType); setResolvedGareId(fetchedId); } } catch (e) {} const { data: all, error } = await supabase.from('horaires').select('id, *'); if (error) { if (mounted) setHoraireError(error.message || String(error)); setLoadingHoraire(false); return; } if (!mounted) return; if (!Array.isArray(all) || all.length === 0) { setLoadingHoraire(false); return; } const normalizedTarget = gareDisplayParam ? normalizeName(gareDisplayParam) : null; const targetQuai = quaiParam ? String(quaiParam).toLowerCase().replace(/\s+/g,'') : null; const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(now.getDate() + 1); const nowHM = now.getHours()*60 + now.getMinutes(); const candidates = []; for (const h of all) { if (!h) continue; const circulatesToday = circulatesOn(h, now); const circulatesTomorrow = circulatesOn(h, tomorrow); if (!circulatesToday && !circulatesTomorrow) continue; const stops = getStops(h); let maxIdx = null; if (Array.isArray(stops) && stops.length) { for (const st of stops) { if (typeof st.index !== 'undefined' && st.index !== null) { const n = Number(st.index); if (!Number.isNaN(n)) maxIdx = maxIdx == null ? n : Math.max(maxIdx, n); } } } let stopMatch = null; if (normalizedTarget) { const departNameNorm = normalizeName(h.gare_depart_nom || h.gare_depart || h.origin_name || ''); if (departNameNorm && (departNameNorm === normalizedTarget || departNameNorm.includes(normalizedTarget) || normalizedTarget.includes(departNameNorm))) { // Target is the origin of this train, which means it doesn't arrive here, skip. continue; } else { const destNameNorm = normalizeName(h.gare_arrivee_nom || h.gare_arrivee || ''); if (destNameNorm && (destNameNorm === normalizedTarget || destNameNorm.includes(normalizedTarget) || normalizedTarget.includes(destNameNorm))) { let finalStop = { heure_arrivee: h.heure_arrivee || h.time || h.heure, gare_nom: h.gare_arrivee_nom || h.gare_arrivee }; if (Array.isArray(stops) && stops.length) { for (const s of stops) { const rawName = (typeof s === 'string') ? s : (s.gare_nom || s.nom || s.name || s.stop_name || s.station || ''); const nm = normalizeName(rawName); if (!nm) continue; if (nm === normalizedTarget || nm.includes(normalizedTarget) || normalizedTarget.includes(nm)) { if (typeof s === 'object') { if (!s.heure_arrivee && finalStop.heure_arrivee) s.heure_arrivee = finalStop.heure_arrivee; finalStop = s; } else { finalStop.gare_nom = s; } break; } } } stopMatch = finalStop; } } } if (!stopMatch && gareIdParam) { const gid = String(gareIdParam); const arriveeId = h.gare_arrivee_id || h.gare_id || h.destination_id || null; if (arriveeId != null && String(arriveeId) === gid) { stopMatch = { heure_arrivee: h.heure_arrivee || h.time || h.heure, gare_id: arriveeId, gare_nom: h.gare_arrivee_nom || h.gare_arrivee }; } if (!stopMatch && Array.isArray(stops) && stops.length) { for (const s of stops) { const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null; if (sid != null && String(sid) === gid) { stopMatch = s; break; } } } } if (!stopMatch && normalizedTarget && Array.isArray(stops) && stops.length) { for (const s of stops) { const rawName = (typeof s === 'string') ? s : (s.gare_nom || s.nom || s.name || s.stop_name || s.stopName || s.station || s.station_name || ''); const nm = normalizeName(rawName); if (!nm) continue; if (nm === normalizedTarget || nm.includes(normalizedTarget) || normalizedTarget.includes(nm)) { const isOrigin = Boolean((typeof s === 'object') && (s.isFirst || s.is_first || s.isOrigin || (typeof s.index !== 'undefined' && Number(s.index) === 0))); if (isOrigin) continue; stopMatch = s; break; } } } if (!stopMatch) continue; const platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.quay)) || h.quai_reel || h.platform || h.quai || ''; const normPlatform = String(platform || '').toLowerCase().replace(/\s+/g,''); if (targetQuai) { if (!normPlatform) continue; if (!(normPlatform === targetQuai || normPlatform.includes(targetQuai) || targetQuai.includes(normPlatform))) continue; } const stopIsObject = stopMatch && typeof stopMatch === 'object'; const hasArrival = stopIsObject && ( typeof stopMatch.heure_arrivee !== 'undefined' || typeof stopMatch.arrival_time !== 'undefined' || typeof stopMatch.arrival !== 'undefined' || typeof stopMatch.time !== 'undefined' || typeof stopMatch.arrival_time !== 'undefined' || typeof stopMatch.horaire_time !== 'undefined' ); const hasDepartureOnly = stopIsObject && !hasArrival && ( typeof stopMatch.heure_depart !== 'undefined' || typeof stopMatch.departure_time !== 'undefined' || typeof stopMatch.departure !== 'undefined' ); if (stopIsObject && hasDepartureOnly) continue; const rawTime = (hasArrival ? (stopMatch.heure_arrivee || stopMatch.time || stopMatch.heure || stopMatch.arrival_time || stopMatch.arrival) : (h.heure_arrivee || h.time || h.heure || h.arrival_time || h.arrival)); const arrHM = parseTimeHM(rawTime); if (arrHM == null) continue; let minutesUntil = arrHM - nowHM; if (minutesUntil < 0) minutesUntil = arrHM + 24*60 - nowHM; candidates.push({ horaire: h, stop: stopMatch, arrHM, minutesUntil, rawTime, circulatesToday, circulatesTomorrow }); } if (!candidates.length) { setCandidatesCount(0); setLoadingHoraire(false); setSelectedHoraire(null); return; } const todayCandidates = candidates.filter(c => c.circulatesToday && c.arrHM >= nowHM - 1).sort((a,b) => a.arrHM - b.arrHM); const tomorrowCandidates = candidates.filter(c => c.circulatesTomorrow && (c.arrHM < nowHM - 1 || !c.circulatesToday)).sort((a,b) => a.arrHM - b.arrHM); let chosen = null; if (todayCandidates.length > 0) { chosen = { ...todayCandidates[0], isTomorrow: false }; } else if (tomorrowCandidates.length > 0) { chosen = { ...tomorrowCandidates[0], isTomorrow: true }; } if (mounted) { setCandidatesCount(candidates.length); setSelectedHoraire(chosen || null); } } catch (err) { console.error('Erreur supabase quai', err); if (mounted) setHoraireError(err.message || String(err)); } finally { if (mounted) setLoadingHoraire(false); } } loadNextForQuai(); return () => { mounted = false; }; }, [gareDisplayParam, 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); })(); if (!loadingHoraire && !selectedHoraire) { return (
Aucune Arrivée Prévue
); } return (
{/* LEFT PANEL: Lilac/Light Grey Background */}
{/* SNCF Logo */} SNCF
{(() => { const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null; const statusObj = horaire && horaire._status ? horaire._status : null; const isSuppressed = statusObj && statusObj.label === 'supprimé'; const isDelayed = statusObj && typeof statusObj.delayMinutes === 'number'; const delayMinutes = statusObj ? statusObj.delayMinutes : null; let timeText = horaire ? formatTimeNoSeconds(selectedHoraire.rawTime || horaire.heure_arrivee || horaire.time || horaire.heure) : mock.time; let statusText = horaire ? (horaire.status || (statusObj && statusObj.label) || mock.status) : mock.status; if (showPlatform) { if (isSuppressed) { if (!alternateCancelPhase) { timeText = 'supprimé'; statusText = ''; } else { statusText = 'supprimé'; } } else if (isDelayed) { if (!alternateDelayPhase) { timeText = `retardé +${delayMinutes} min`; statusText = ''; } else { statusText = `retardé +${delayMinutes} min`; } } } else { timeText = '--h--'; statusText = ''; } const isTimeSuppressed = isSuppressed && !alternateCancelPhase; const isTimeDelayed = isDelayed && !alternateDelayPhase; const isLongText = timeText.length > 5; let timeClass = ''; if (isLongText) timeClass += ' aff-quais-arrivees-redesign__time--long-text'; if (isTimeSuppressed) timeClass += ' aff-quais-arrivees-redesign__time--suppressed'; if (isTimeDelayed) timeClass += ' aff-quais-arrivees-redesign__time--delayed'; let statusClass = ''; if (isSuppressed) statusClass += ' aff-quais-arrivees-redesign__status--suppressed'; if (isDelayed) statusClass += ' aff-quais-arrivees-redesign__status--delayed'; return ( <> {timeText} {statusText && {String(statusText).toLowerCase()}} ); })()}
{(() => { const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null; return showPlatform ? (horaire ? (horaire.gare_depart_nom || horaire.gare_depart || horaire.origin_name || horaire.origin || '') : mock.destinationBig) : '--'; })()}
{(() => { const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null; const trainType = horaire ? (horaire.trainType || horaire.train_type || horaire.type_train || horaire.op || horaire.operator) : mock.trainType; const trainNumber = horaire ? (horaire.numero_train || horaire.numero || horaire.number || '') : mock.trainNumber; return showPlatform ? ( <> {trainType} {trainNumber} ) : ''; })()}
{/* Accessibility Icons */}
{/* Watermark */}
arrivée
{/* RIGHT PANEL: Green Background with Provenance Stops */}
{showPlatform &&
gares de provenance :
}
{/* BOTTOM FOOTER: Full Width */}
ain.
); }