Initial commit

Created from https://vercel.com/new
This commit is contained in:
MrPatator230 committed 2026-03-19 15:46:03 +00:00
commit 3fcdf7e5ef
172 files changed
+52525

No files matched your search

@@ -0,0 +1,746 @@
import React, { useEffect, useState, useCallback } from 'react';
import './ClassiquesArrivees.css';
import supabase from '../../../lib/supabase';
// 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 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');
};
function Logo({op}){
const mapping = {
'TGV inOUI': 'logo-inoui.svg',
'TGV inOUI ': 'logo-inoui.svg',
'TGV': 'logo-tgv.svg',
'TER': 'sncf-voyageurs-logo.png',
'MOBIGO': 'logo-mobigo.svg',
'TGV Lyria': 'logo-lyria.svg',
'TGV Lyria ': 'logo-lyria.svg',
};
const defaultFileFromOp = (name) => {
if (!name) return null;
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
return `logo-${slug}.svg`;
};
const fileName = mapping[op] || mapping[op && op.split && op.split(' ')[0]] || defaultFileFromOp(op);
const [hasImg, setHasImg] = React.useState(true);
return (
<div className="logo-box-arrivees" aria-hidden>
{hasImg && fileName ? (
<img
src={`/type/${fileName}`}
alt={op}
className="op-logo-arrivees"
onError={()=> setHasImg(false)}
/>
) : (
<div className="logo-text-arrivees" aria-hidden>{(op||'').split(' ')[0]}</div>
)}
</div>
);
}
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 (
<div className="clock-container-arrivees" aria-hidden>
<div className="clock-box-arrivees">
<div className="clock-main-arrivees">{hh}:{mm}</div>
<div className="clock-seconds-arrivees">{ss}</div>
</div>
</div>
);
}
export default function ClassiquesArrivees(){
// lire le paramètre de requête ?gare=Nom-de-la-gare
const params = new URLSearchParams(window.location.search);
const gareParam = params.get('gare');
// support pour ?gare_id=123 ou ?gareId=123
const gareIdParam = params.get('gare_id') || params.get('gareId');
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');
const debug = useCallback((...args)=>{ if (debugEnabled) console.debug(...args); }, [debugEnabled]);
// État pour alterner l'affichage toutes les 3 secondes
const [showStatus, setShowStatus] = useState(false);
useEffect(() => {
const id = setInterval(() => setShowStatus(s => !s), 3000);
return () => clearInterval(id);
}, []);
// Données
const [horaires, setHoraires] = useState([]);
const [allHoraires, setAllHoraires] = useState([]);
const [perturbations, setPerturbations] = useState([]);
// type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)
const [stationType, setStationType] = useState(null);
const [, setTravaux] = useState([]);
const [, setLoading] = useState(false);
const [, setError] = useState(null);
const [rawDebug, setRawDebug] = useState({ count: 0, sample: [] });
const normalizeName = useCallback((s) => {
if (!s) return '';
try { const t = String(s).normalize('NFD').replace(/\p{M}/gu,'').toLowerCase().trim(); return t.replace(/[-_]+/g,' ').replace(/\s+/g,' '); } catch (e) { return String(s).toLowerCase().trim(); }
}, []);
const safeParseArrets = useCallback((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 getStops = useCallback((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;
const candidates = ['stops','gares_desservies','garesDesservies','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) { /* ignore */ }
}
}
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 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}`;
}, []);
// parse HH:mm or ISO into minutes since midnight (utilisable dans le rendu)
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;
}, []);
// retourne le threshold (en minutes) pour afficher le quai selon le type de gare
const thresholdForStationType = useCallback((type) => {
if (!type) return 0;
const t = String(type).toLowerCase();
if (t.includes('interurb') || t.includes('interurbaine')) return 360; // 6 heures
if (t.includes('ville')) return 30; // 30 minutes
return 0;
}, []);
const buildPerturbMap = useCallback((list = perturbations, horairesList = allHoraires) => {
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); }
});
if (Array.isArray(horairesList) && horairesList.length) {
try {
const byH = Object.keys(idx.byHoraire || {});
if (byH && byH.length) {
for (const hid of byH) {
const h = horairesList.find(x => String(x.id || x.horaire_id || x.horaireId || x.numero) === String(hid));
if (h) {
const num = h.numero_train || h.number || h.numero || null;
if (num != null) {
const numKey = String(num);
idx.byNumber[numKey] = idx.byNumber[numKey] || [];
(idx.byHoraire[hid] || []).forEach(p => idx.byNumber[numKey].push(p));
}
}
}
}
} catch (e) {}
}
return idx;
}, [perturbations, allHoraires, safeParseArrets, normalizeName]);
const findPerturbsForHoraireAndStop = useCallback((horaire, stationId, stationName, idx) => {
const res = [];
const map = idx || buildPerturbMap();
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, 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 || p.start_date || p.date_debut);
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, parsePerturbArretsObject, getPerturbationJours, parseDelayMinutesFromArret, normalizeName]);
const applyPerturbationsToHoraires = useCallback((list = [], perturbList = perturbations, displayStationId = null, displayStationName = null, serviceDate = null) => {
const map = buildPerturbMap(perturbList || []);
const targetStationId = displayStationId || gareIdParam || null;
const targetStationName = displayStationName || gareDisplay || null;
return (list || []).map(h => {
const horaire = { ...h };
const stops = getStops(horaire);
const stopsStatus = [];
for (let i = 0; i < stops.length; i++) {
const s = stops[i] || {};
const stationId = s.gare_id || s.gareId || s.id || s.station_id || s.stationId || s.uic || s.code;
const stationName = s.gare_nom || s.nom || s.name || s.station_name || s.station_nom;
const st = computeStatusForHoraireAtStop(horaire, stationId, stationName, map, serviceDate);
stopsStatus.push({ stationId, stationName, status: st });
}
// compute display time using delay at the display station (arrivée)
const displayStatus = computeStatusForHoraireAtStop(horaire, targetStationId, targetStationName, map, serviceDate);
let displayTime = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.arrival_time || horaire.time || horaire.heure || '');
if (displayStatus && typeof displayStatus.delayMinutes === 'number' && !isNaN(displayStatus.delayMinutes)) {
if (typeof horaire._arrHM === 'number') {
displayTime = formatTimeFromHM(horaire._arrHM + displayStatus.delayMinutes);
} else {
const tryParse = (s) => {
if (!s) return null;
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 base = tryParse(horaire._rawArrTime || horaire.heure_arrivee || horaire.arrival_time || horaire.time || horaire.heure || '');
if (typeof base === 'number') displayTime = formatTimeFromHM(base + displayStatus.delayMinutes);
}
}
horaire._stopsStatus = stopsStatus;
// prefer platform from stop data (arrivée) if available — check several possible keys
const stopForStation = (stops || []).find(s => {
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
const ns = normalizeName(nm || '');
const target = normalizeName(targetStationName || '');
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && (ns === target || ns.includes(target) || target.includes(ns)));
}) || null;
horaire._status = displayStatus;
horaire._displayTime = displayTime;
// arrival platform keys preferred, then fallback to generic platform
horaire._displayPlatform = (stopForStation && (stopForStation.quai_arrivee || stopForStation.platform_arrivee || stopForStation.quay_arrivee || stopForStation.arrival_platform || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || horaire.arrival_platform || horaire.platform || '';
return horaire;
});
}, [buildPerturbMap, computeStatusForHoraireAtStop, formatTimeFromHM, perturbations, gareDisplay, gareIdParam, getStops, normalizeName]);
useEffect(() => {
let mounted = true;
let inFlight = false;
const fetchData = async () => {
if (!mounted) return;
if (inFlight) return;
inFlight = true;
if (!supabase) {
setError('Supabase non initialisé — vérifiez les variables d\'environnement');
setLoading(false);
inFlight = false;
return;
}
setLoading(true);
setError(null);
try {
const [{ data: horairesData, error: hError }, { data: perturbData, error: pError }, { data: travauxData, error: tError }] = await Promise.all([
supabase.from('horaires').select('id, *'),
supabase.from('perturbations').select('*'),
supabase.from('travaux').select('*')
]);
if (!mounted) { inFlight = false; return; }
if (hError || pError || tError) {
setError((hError || pError || tError).message || String(hError || pError || tError));
setLoading(false);
inFlight = false;
return;
}
debug('Supabase horaires fetched', { count: (horairesData || []).length });
if (mounted) setRawDebug({ count: (horairesData || []).length, sample: (horairesData || []).slice(0,3) });
let parsedHoraires = (horairesData || []).map(h => h || {});
parsedHoraires = parsedHoraires.map(h => {
try {
if (h && typeof h.stops === 'string') {
const parsed = JSON.parse(h.stops);
if (Array.isArray(parsed)) return { ...h, stops: parsed };
}
} catch (e) {}
return h;
});
if (mounted) setAllHoraires(parsedHoraires || []);
// Récupérer le type de gare depuis la table `gares` si possible
try {
let fetchedType = null;
if (gareIdParam) {
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).limit(1);
if (!gerr && gdata && gdata.length) fetchedType = gdata[0] && gdata[0].type;
} else if (gareDisplay) {
const needle = `%${gareDisplay}%`;
try {
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type').or(`nom.ilike.${needle},name.ilike.${needle}`).limit(1);
if (!gerr && gdata && gdata.length) fetchedType = gdata[0] && gdata[0].type;
} catch (e) {
try {
const { data: gdata } = await supabase.from('gares').select('id,type').ilike('nom', needle).limit(1);
if (gdata && gdata.length) fetchedType = gdata[0] && gdata[0].type;
} catch (err) { /* ignore */ }
}
}
if (mounted) setStationType(fetchedType ? String(fetchedType).toLowerCase() : null);
} catch (e) { /* ignore errors reading gares table */ }
// Filtrer par gare : garder uniquement les horaires qui DESSERVENT la gare ciblée en tant qu'ARRIVÉE
let displayHoraires = parsedHoraires.slice();
if (gareDisplay) {
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
displayHoraires = displayHoraires.filter(h => {
// Exclure si la gare ciblée est la gare de départ (on veut les arrivées)
const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString());
if (departName && (departName === normalized || departName.includes(normalized) || normalized.includes(departName))) return false;
// Inclure si gare d'arrivée correspond
const arriveeName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
if (arriveeName && (arriveeName === normalized || arriveeName.includes(normalized) || normalized.includes(arriveeName))) return true;
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus d'origine (index==0)
const stopsList = getStops(h);
if (Array.isArray(stopsList) && stopsList.length) {
// compute max index to detect terminus positions
let maxIdx = null;
for (const st of stopsList) {
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);
}
}
for (const s of stopsList) {
const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
if (!stopName) continue;
const isOrigin = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && Number(s.index) === 0));
if (isOrigin) continue;
if (stopName === normalized || stopName.includes(normalized) || normalized.includes(stopName)) return true;
}
}
return false;
});
} else if (gareIdParam) {
const gid = String(gareIdParam);
displayHoraires = displayHoraires.filter(h => {
const departId = (h.gare_depart_id || h.gare_depart || null);
if (departId != null && String(departId) === gid) return false;
const arriveeId = h.gare_arrivee_id || h.gare_arrivee || null;
if (arriveeId != null && String(arriveeId) === gid) return true;
const stopsList = getStops(h);
if (Array.isArray(stopsList) && stopsList.length) {
for (const s of stopsList) {
const stopId = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
if (stopId == null) continue;
const isOrigin = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && Number(s.index) === 0));
if (isOrigin) continue;
if (String(stopId) === gid) return true;
}
}
return false;
});
}
const dateKeys = ['date_depart','date','jour','service_date','date_service','date_dep'];
const normalizeDate = (o) => {
for (const k of dateKeys) {
if (!o) continue;
const v = o[k];
if (!v) continue;
const d = new Date(v);
if (!isNaN(d.getTime())) return d.toISOString().slice(0,10);
}
return null;
};
const withDates = displayHoraires.map(h => ({ ...h, _date: normalizeDate(h) }));
const now = new Date();
const todayHM = now.getHours()*60 + now.getMinutes();
const enrich = withDates.map(h => {
let raw = h.heure_arrivee || h.arrival_time || h.time || h.heure || '';
const stopsListForH = getStops(h);
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null;
const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null;
const stop = byId || byName;
if (stop && (stop.heure_arrivee || stop.time || stop.heure)) raw = stop.heure_arrivee || stop.time || stop.heure;
}
const arrHM = parseTimeHM(raw);
const displayTime = formatTimeNoSeconds(raw);
return { ...h, _rawArrTime: raw, _arrHM: arrHM, _displayTime: displayTime };
});
// travaux processing (copié et adapté)
const pad = (n) => String(n).padStart(2,'0');
const localDateStr = (d) => `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`;
const today = new Date();
const todayStr = localDateStr(today);
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate()+1);
const tomorrowStr = localDateStr(tomorrow);
const weekdayIndex = (d) => d.getDay();
const weekdayNameForColumn = (d) => {
const i = weekdayIndex(d);
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 circulatesOn = (h, d) => {
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);
};
const travauxList = travauxData || [];
const parseDateOnly = (v) => { if (!v) return null; const d = new Date(v); if (isNaN(d.getTime())) return null; return new Date(d.getFullYear(), d.getMonth(), d.getDate()); };
const parseJoursTravaux = (val) => { if (val == null) return null; if (Array.isArray(val)) return new Set(val.map(x => String(x).toLowerCase())); const s = String(val || ''); const parts = s.split(/[,;\s]+/).map(p => p.trim()).filter(Boolean); return parts.length ? new Set(parts.map(p => p.toLowerCase())) : null; };
const weekdayShort = (i) => { switch(i) { case 0: return 'dim'; case 1: return 'lun'; case 2: return 'mar'; case 3: return 'mer'; case 4: return 'jeu'; case 5: return 'ven'; case 6: return 'sam'; default: return null; } };
const travauxApplies = (t, h, date) => {
if (!t) return false;
const db = parseDateOnly(t.date_debut || t.dateDebut || t.start_date);
const df = parseDateOnly(t.date_fin || t.dateFin || t.end_date);
if (!db || !df) return false;
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
if (d < db || d > df) return false;
const joursSet = parseJoursTravaux(t.jours_travaux || t.jours || t.days);
if (joursSet) {
const name = weekdayShort(d.getDay());
if (!name) return false;
let ok = false; for (const v of joursSet) { if (v.includes(name) || name.includes(v)) { ok = true; break; } }
if (!ok) return false;
}
const startHM = parseTimeHM(t.heure_debut || t.heureDebut || t.start_time);
const endHM = parseTimeHM(t.heure_fin || t.heureFin || t.end_time);
if (startHM == null || endHM == null) return true;
if (typeof h._arrHM === 'number') return h._arrHM >= startHM && h._arrHM <= endHM;
return false;
};
const isSubstitution = (h) => {
if (!h) return false;
if (h.substitution_disponible === true) return true;
const subFlag = h.substitution === true || h.is_substitution === true || h.isSubstitution === true;
const subStr = (typeof h.substitution === 'string' && ['true','1','yes','y','t'].includes(h.substitution.toLowerCase()));
const typeMatch = h.type_train && /subst|substitut|remplac/i.test(String(h.type_train));
const commentMatch = h.comment && /subst|substitut|remplac/i.test(String(h.comment));
return Boolean(subFlag || subStr || typeMatch || commentMatch);
};
const enriched = enrich.map(h => {
let linked = false; let appliesToday = false; let appliesTomorrow = false;
for (const t of travauxList) {
const tLine = t.ligne_id || t.ligne || t.line_id || t.line || t.ligneId || t.lineId;
const hLine = h.ligne_id || h.ligne || h.line_id || h.line || h.ligneId || h.lineId;
if (typeof tLine !== 'undefined' && tLine !== null) {
if (typeof hLine === 'undefined' || hLine === null) continue;
if (String(tLine) !== String(hLine)) continue;
}
const ref = t.horaire_id || t.horaire || t.horaireId || t.numero_train || t.train_number || t.numero;
if (ref && (String(ref) === String(h.id) || String(ref) === String(h.numero_train) || String(ref) === String(h.numero))) linked = true;
if (!appliesToday && travauxApplies(t,h,today)) appliesToday = true;
if (!appliesTomorrow && travauxApplies(t,h,tomorrow)) appliesTomorrow = true;
if (linked && appliesToday && appliesTomorrow) break;
}
return { ...h, _linkedToTravaux: linked, _travauxAppliesToday: appliesToday, _travauxAppliesTomorrow: appliesTomorrow };
});
const todayCandidates = enriched.filter(h => {
if (h._date && h._date !== todayStr) return false;
if (!circulatesOn(h, today)) return false;
if (typeof h._arrHM !== 'number') return false;
if (h._arrHM <= todayHM) return false;
if (h._travauxAppliesToday && !isSubstitution(h)) return false;
if (isSubstitution(h)) return Boolean(h.substitution_disponible === true && h._linkedToTravaux && h._travauxAppliesToday);
return true;
});
const tomorrowCandidates = enriched.filter(h => {
if (h._date && h._date !== tomorrowStr) return false;
if (!circulatesOn(h, tomorrow)) return false;
if (h._travauxAppliesTomorrow && !isSubstitution(h)) return false;
if (isSubstitution(h)) return Boolean(h.substitution_disponible === true && h._linkedToTravaux && h._travauxAppliesTomorrow);
return true;
});
let finalList; let serviceDateStr;
if (todayCandidates && todayCandidates.length > 0) { finalList = todayCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999)); serviceDateStr = todayStr; }
else { finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999)); serviceDateStr = tomorrowStr; }
const localPerturbMap = buildPerturbMap(perturbData || [], parsedHoraires || []);
debug('localPerturbMap summary', {
horaireCount: Object.keys(localPerturbMap.byHoraire || {}).length,
numberCount: Object.keys(localPerturbMap.byNumber || {}).length,
});
const annotated = applyPerturbationsToHoraires(finalList, perturbData || [], null, null, serviceDateStr);
try { debug('annotated horaires sample', annotated.slice(0,6).map(a => ({ id: a.id, numero: a.numero_train || a.numero, _status: a._status, _displayTime: a._displayTime }))); } catch (e) {}
setHoraires((annotated || []).slice(0, 9));
setPerturbations(perturbData || []);
setTravaux(travauxData || []);
} catch (err) {
console.error('Erreur Supabase horaires (arrivees)', err);
if (mounted) setError(err.message || String(err));
} finally {
if (mounted) setLoading(false);
inFlight = false;
}
};
fetchData();
const intervalMs = 15000;
const intervalId = setInterval(fetchData, intervalMs);
return () => { mounted = false; clearInterval(intervalId); };
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, applyPerturbationsToHoraires, debug, getStops, parseTimeHM]);
return (
<div className="classiques-arrivees-container">
<div className="watermark-arrivees">arrivées</div>
<ul className="arrivees-list">
{horaires.length === 0 ? (
<li className="arrivee-row" key="empty">
<div>Aucun horaire disponible.</div>
<div style={{fontSize:'0.8rem', opacity:0.8}}>lignes brutes renvoyées : {rawDebug.count}</div>
{rawDebug.count > 0 && rawDebug.sample && rawDebug.sample.length > 0 ? (
<pre style={{maxHeight: 160, overflow: 'auto', fontSize: '0.75rem', marginTop: 8, background: '#111', color:'#fff', padding:8}}>{JSON.stringify(rawDebug.sample.slice(0,2), null, 2).slice(0,1000)}{JSON.stringify(rawDebug.sample.slice(0,2), null, 2).length > 1000 ? '…(truncated)' : ''}</pre>
) : null}
</li>
) : horaires.map((h, idx) => {
const horaire = h || {};
const op = horaire.type_train || horaire.op || '';
const number = horaire.numero_train || horaire.number || horaire.numero || '';
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.arrival_time || horaire.time || horaire.heure || '');
// afficher la gare de départ à la place de la destination
const departure = horaire.gare_depart_nom || horaire.gare_depart || '';
const platform = (horaire._displayPlatform || horaire.arrival_platform || horaire.platform || '').toString().trim();
const stationId = horaire.gare_arrivee_id || horaire.gare_arrivee || (gareIdParam || null);
const stationName = (gareDisplay) ? gareDisplay : (horaire.gare_arrivee_nom || horaire.gare_arrivee || null);
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
if (status && status.label === 'supprimé' && showStatus) {
time = '';
} else if (!horaire._displayTime && status && typeof status.delayMinutes === 'number' && !isNaN(status.delayMinutes)) {
if (typeof horaire._arrHM === 'number') {
time = formatTimeFromHM(horaire._arrHM + status.delayMinutes);
} else {
const tryParse = (s) => {
if (!s) return null;
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 base = tryParse(horaire.heure_arrivee || horaire.arrival_time || horaire.time || horaire._rawArrTime || '');
if (typeof base === 'number') time = formatTimeFromHM(base + status.delayMinutes);
}
}
const statusClass = status && status.label === 'supprimé' ? 'status-suppressed-arrivees' : (status && status.label && String(status.label).startsWith('retardé') ? 'status-delayed-arrivees' : '');
// Ne pas afficher le quai si le train est supprimé ou si aucun quai n'est défini
let showPlatform = !(status && status.label === 'supprimé') && Boolean(platform);
if (showPlatform) {
const now = new Date();
const nowHM = now.getHours()*60 + now.getMinutes();
const sched = (typeof horaire._arrHM === 'number') ? horaire._arrHM : parseTimeHM(horaire.heure_arrivee || horaire.arrival_time || horaire.time || horaire._rawArrTime || '');
const threshold = thresholdForStationType(stationType);
if (typeof sched === 'number' && typeof threshold === 'number' && threshold > 0) {
// n'afficher le quai que si l'heure courante est au moins (scheduled - threshold)
showPlatform = (nowHM >= (sched - threshold));
}
}
return (
<li className="arrivee-row" key={idx}>
<div className="col-logo-arrivees"><Logo op={op} /></div>
<div className="col-train-type-arrivees">
{showStatus ? (
<div className={`train-meta-arrivees ${statusClass}`} aria-live="polite">
<div className="op-name-arrivees">{status ? status.label : ''}</div>
<div className="op-number-arrivees">{status ? status.detail : ''}</div>
</div>
) : (
<div className="train-meta-arrivees">
<div className="op-name-arrivees">{op}</div>
<div className="op-number-arrivees">{number}</div>
</div>
)}
</div>
<div className="col-time-arrivees"><div className="time-arrivees">{time}</div></div>
<div className="col-destination-arrivees"><div className="destination-arrivees">{departure}</div></div>
{showPlatform ? (
<div className="col-platform-arrivees"><div className="platform-box-arrivees"><div className="platform-text-arrivees">{platform}</div></div></div>
) : null}
</li>
);
})}
</ul>
<div className="afficheur-footer-arrivees" aria-hidden>
<div className="footer-inner-arrivees"><Clock /></div>
</div>
</div>
);
}