V2.0.0 : VOICI LES MODIFICATIONS MAJEURS APPRTÉES DANS CETTE VERSION :
-> REFONTE DU DESIGN DES AFFICHEURS CLASSIQUES (GÉNÉRAL + QUAIS) -> IMPLÉMENTATION DU DESIGN AFL : GÉNÉRAL ET QUAIS -> CHOIX DE LA SÉLECTION DES TYPES D'AFFICHEURS (GÉNÉRAL / QUAIS) UNE FOIS LE TYPE D'AFFICHEUR SÉLECTIONNÉS EN COURS DE DÉVELOPPEMENT : -> IMPLÉMENTATION DES PERTURBATION DU LANDEMAIN POUR LES HORAIRES DU LANDEMAIN AFFICHÉS AVANT MINUIT. -> IMPLÉMENTATION DES AFFICHEURS D'INFORMATIONS / INFOS TRAVAUX, etc... (AFFICHEURS AFL)
This commit is contained in:
1 parent
f0d9ed9ee7
commit
f06ebbaf48
43 files changed
+10519
-3813
No files matched your search
@@ -1,7 +1,8 @@
|
||||
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import './ClassiquesDeparts.css';
|
||||
import supabase from '../../../lib/supabase';
|
||||
import Logo from '../../../components/Logo';
|
||||
import MarqueeText from '../../../components/MarqueeText';
|
||||
|
||||
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
||||
const formatTimeNoSeconds = (v) => {
|
||||
@@ -22,46 +23,15 @@ const formatTimeNoSeconds = (v) => {
|
||||
return String(v).slice(0,5).replace(':','h');
|
||||
};
|
||||
|
||||
// Données proviennent exclusivement de la table `horaires`.
|
||||
function Logo({op}){
|
||||
// Tente de charger un logo SVG depuis /type/ en fonction de l'opérateur.
|
||||
// mapping pour opérateurs connus -> noms de fichiers dans public/type
|
||||
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;
|
||||
// nettoyer et fabriquer un nom plausible : "logo-<slug>.svg"
|
||||
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
return `logo-${slug}.svg`;
|
||||
};
|
||||
|
||||
const fileName = mapping[op] || mapping[op.split(' ')[0]] || defaultFileFromOp(op);
|
||||
|
||||
const [hasImg, setHasImg] = React.useState(true);
|
||||
|
||||
return (
|
||||
<div className="aff-classiques-departs__logo-box" aria-hidden>
|
||||
{hasImg && fileName ? (
|
||||
<img
|
||||
src={`/type/${fileName}`}
|
||||
alt={op}
|
||||
className="aff-classiques-departs__op-logo"
|
||||
onError={()=> setHasImg(false)}
|
||||
/>
|
||||
) : (
|
||||
<div className="aff-classiques-departs__logo-text" aria-hidden>{op.split(' ')[0]}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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);
|
||||
};
|
||||
|
||||
// (La fonction TrainType a été retirée car la colonne affiche désormais directement les
|
||||
// informations via .train-meta dans la colonne .col-train-type)
|
||||
@@ -95,13 +65,17 @@ export default function ClassiquesDeparts(){
|
||||
const gareIdParam = params.get('gare_id') || params.get('gareId');
|
||||
// gérer les +, décoder et remplacer les tirets par des espaces
|
||||
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
||||
// Mode défilement (marquee vertical infini) si ?defilement=true dans l'URL
|
||||
const defilementMode = params.get('defilement') === 'true';
|
||||
|
||||
// debug flag activable via ?debug=1 or ?debug=true
|
||||
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
|
||||
// Alterner toutes les 2 secondes entre type+numéro et statut
|
||||
const [showStatus, setShowStatus] = useState(false);
|
||||
// Afficher la cause de perturbation 5s, cacher 5s (cycle indépendant)
|
||||
const [showInfoCause, setShowInfoCause] = useState(true);
|
||||
// État pour l'heure actuelle, mise à jour chaque seconde pour la logique d'affichage du quai
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
|
||||
@@ -123,6 +97,38 @@ export default function ClassiquesDeparts(){
|
||||
// debug des données brutes renvoyées par Supabase
|
||||
const [rawDebug, setRawDebug] = useState({ count: 0, sample: [] });
|
||||
|
||||
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
const fetchStationType = async () => {
|
||||
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 flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
||||
if (!gerr && 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(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
return n1 === n2;
|
||||
});
|
||||
fetchedType = exactMatch ? exactMatch.type : gdata[0].type;
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
setStationType(fetchedType ? String(fetchedType).toLowerCase() : null);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
};
|
||||
if (supabase) {
|
||||
fetchStationType();
|
||||
}
|
||||
return () => { mounted = false; };
|
||||
}, [gareIdParam, gareDisplay]);
|
||||
|
||||
// --- Helpers pour perturbations (définis ici pour éviter use-before-define dans useEffect) ---
|
||||
const normalizeName = useCallback((s) => {
|
||||
if (!s) return '';
|
||||
@@ -252,12 +258,19 @@ export default function ClassiquesDeparts(){
|
||||
}, []);
|
||||
|
||||
// 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).trim().toLowerCase();
|
||||
if (t === 'interurbaine' || t.includes('interurb')) return 360; // 6 heures
|
||||
if (t === 'ville' || t.includes('ville')) return 30; // 30 minutes
|
||||
return 0;
|
||||
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) => {
|
||||
@@ -354,81 +367,172 @@ export default function ClassiquesDeparts(){
|
||||
}, [buildPerturbMap, normalizeName, debug]);
|
||||
|
||||
|
||||
// serviceDate (YYYY-MM-DD) optional: forces the evaluation date for perturbations when horaire._date is missing
|
||||
// serviceDate (YYYY-MM-DD) optional: forces the evaluation date for perturbations when horaire._date is missing
|
||||
const computeStatusForHoraireAtStop = useCallback((horaire, stationId, stationName, idx, serviceDate = null) => {
|
||||
const found = findPerturbsForHoraireAndStop(horaire, stationId, stationName, idx) || [];
|
||||
try { debug('computeStatusForHoraireAtStop', { id: horaire && horaire.id, numero: horaire && (horaire.numero_train || horaire.numero), stationId, stationName, found: found.length }); } catch (e) {}
|
||||
const map = idx || buildPerturbMap();
|
||||
const allPerturbs = [];
|
||||
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 (map) {
|
||||
const addedIds = new Set();
|
||||
const addP = (p) => {
|
||||
if (p && !addedIds.has(p.id)) {
|
||||
addedIds.add(p.id);
|
||||
allPerturbs.push(p);
|
||||
}
|
||||
};
|
||||
if (hId != null && map.byHoraire && map.byHoraire[String(hId)]) {
|
||||
map.byHoraire[String(hId)].forEach(addP);
|
||||
}
|
||||
if (number != null && map.byNumber && map.byNumber[String(number)]) {
|
||||
map.byNumber[String(number)].forEach(addP);
|
||||
}
|
||||
if (stationId != null && map.byStopId && map.byStopId[String(stationId)]) {
|
||||
map.byStopId[String(stationId)].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
if (stationName && map.byStopName) {
|
||||
const key = normalizeName(stationName);
|
||||
if (map.byStopName[key]) map.byStopName[key].forEach(x => addP(x.perturbation));
|
||||
for (const existingKey of Object.keys(map.byStopName)) {
|
||||
if (existingKey.includes(key) || key.includes(existingKey)) {
|
||||
map.byStopName[existingKey].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
}
|
||||
}
|
||||
const trainStops = getStops(horaire);
|
||||
if (Array.isArray(trainStops)) {
|
||||
trainStops.forEach(s => {
|
||||
const sId = s.gare_id || s.gareId || s.id || s.code;
|
||||
const sName = s.gare_nom || s.nom || s.name || s.stop_name;
|
||||
if (sId != null && map.byStopId && map.byStopId[String(sId)]) {
|
||||
map.byStopId[String(sId)].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
if (sName && map.byStopName) {
|
||||
const skey = normalizeName(sName);
|
||||
if (map.byStopName[skey]) map.byStopName[skey].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (map.global) {
|
||||
map.global.forEach(addP);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
let maxDelay = null;
|
||||
let isSuppressed = false;
|
||||
let causeDetails = '';
|
||||
|
||||
for (const p of found) {
|
||||
// date filter: if perturbation carries explicit dates or jours_perturbation, check they match horaireYmd
|
||||
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) { debug('perturbation date mismatch', p.id); continue; }
|
||||
if (db && df && !(horaireYmd >= db && horaireYmd <= df)) { debug('perturbation date-range mismatch', p.id); continue; }
|
||||
}
|
||||
} catch (e) { /* ignore date parse errors -> assume applies */ }
|
||||
for (const p of allPerturbs) {
|
||||
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 && !joursSet.has(horaireYmd)) continue;
|
||||
} catch (e) {}
|
||||
|
||||
// also check jours_perturbation (may be inside perturbations_arrets wrapper)
|
||||
try {
|
||||
const joursSet = getPerturbationJours(p);
|
||||
if (joursSet && horaireYmd) {
|
||||
if (!joursSet.has(horaireYmd)) { debug('perturbation jours mismatch', p.id); continue; }
|
||||
}
|
||||
} catch (e) {}
|
||||
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 parsedObj = parsePerturbArretsObject(p) || { arrets: [], jours: null };
|
||||
const arrets = parsedObj.arrets || [];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
const trainStops = getStops(horaire);
|
||||
let trainStopMatch = false;
|
||||
|
||||
if (!matched) {
|
||||
// top-level perturbation that references the horaire/numero
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const matchesTarget = (a) => {
|
||||
if (!stationName && stationId == null) return false;
|
||||
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;
|
||||
if (stationId != null && String(aId) === String(stationId)) return true;
|
||||
if (stationName && aName) {
|
||||
const na = normalizeName(aName);
|
||||
const ns = normalizeName(stationName);
|
||||
return na === ns;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay };
|
||||
return { label: "à l'heure", detail: '', delayMinutes: null };
|
||||
}, [findPerturbsForHoraireAndStop, parsePerturbArretsObject, getPerturbationJours, parseDelayMinutesFromArret, normalizeName, debug]);
|
||||
for (const a of arrets) {
|
||||
const aId = a.gare_id || a.gareId || a.id || a.uic || a.code;
|
||||
const aName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom;
|
||||
const na = normalizeName(aName || '');
|
||||
|
||||
const isTarget = matchesTarget(a);
|
||||
let isTrainStop = false;
|
||||
if (trainStops && Array.isArray(trainStops)) {
|
||||
isTrainStop = trainStops.some(s => {
|
||||
const sId = s.gare_id || s.gareId || s.id || s.code;
|
||||
const sName = s.gare_nom || s.nom || s.name || s.stop_name;
|
||||
if (sId != null && aId != null && String(sId) === String(aId)) return true;
|
||||
if (sName && aName) {
|
||||
const ns = normalizeName(sName);
|
||||
return ns === na;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
const originName = normalizeName(horaire.gare_depart_nom || horaire.gare_depart || '');
|
||||
const destName = normalizeName(horaire.gare_arrivee_nom || horaire.gare_arrivee || '');
|
||||
if (originName === na || destName === na) isTrainStop = true;
|
||||
|
||||
if (isTrainStop) trainStopMatch = true;
|
||||
|
||||
if (isTarget) {
|
||||
const t = (a.type_perturbation || a.type || '').toString().toLowerCase();
|
||||
if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) {
|
||||
isSuppressed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isGlobal = (arrets.length === 0) && !ref && !numberRef;
|
||||
const applies = appliesByRef || appliesByNumber || trainStopMatch || isGlobal;
|
||||
|
||||
if (applies) {
|
||||
const t = (p.type_perturbation || p.type || '').toString().toLowerCase();
|
||||
if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) {
|
||||
isSuppressed = true;
|
||||
}
|
||||
|
||||
for (const a of arrets) {
|
||||
const tType = (a.type_perturbation || a.type || '').toString().toLowerCase();
|
||||
if (tType.includes('retard') || tType.includes('delay') || /retard/.test(tType)) {
|
||||
const d = parseDelayMinutesFromArret(a) || parseDelayMinutesFromArret(p) || 0;
|
||||
if (maxDelay == null || d > maxDelay) maxDelay = d;
|
||||
}
|
||||
}
|
||||
if (t.includes('retard') || t.includes('delay') || /retard/.test(t)) {
|
||||
const d = parseDelayMinutesFromArret(p) || 0;
|
||||
if (maxDelay == null || d > maxDelay) maxDelay = d;
|
||||
}
|
||||
if (p.cause_details) {
|
||||
causeDetails = p.cause_details;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null, causeDetails };
|
||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay, causeDetails };
|
||||
return { label: "à l'heure", detail: '', delayMinutes: null, causeDetails };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
// Applique les perturbations à une liste d'horaires : calcule status par arrêt et met à jour l'heure affichée
|
||||
const applyPerturbationsToHoraires = useCallback((list = [], perturbList = perturbations, displayStationId = null, displayStationName = null, serviceDate = null) => {
|
||||
const applyPerturbationsToHoraires = useCallback((list = [], perturbList = [], displayStationId = null, displayStationName = null, serviceDate = null) => {
|
||||
const map = buildPerturbMap(perturbList || []);
|
||||
// fallback to global display station context
|
||||
const targetStationId = displayStationId || gareIdParam || null;
|
||||
@@ -449,22 +553,6 @@ export default function ClassiquesDeparts(){
|
||||
// compute display time using delay at the display station if any
|
||||
const displayStatus = computeStatusForHoraireAtStop(horaire, targetStationId, targetStationName, map, serviceDate);
|
||||
let displayTime = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
|
||||
if (displayStatus && typeof displayStatus.delayMinutes === 'number' && !isNaN(displayStatus.delayMinutes)) {
|
||||
if (typeof horaire._depHM === 'number') {
|
||||
displayTime = formatTimeFromHM(horaire._depHM + 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._rawTime || horaire.heure_depart || horaire.time || horaire.heure || '');
|
||||
if (typeof base === 'number') displayTime = formatTimeFromHM(base + displayStatus.delayMinutes);
|
||||
}
|
||||
}
|
||||
horaire._stopsStatus = stopsStatus;
|
||||
// prefer platform from stop data (quai) if available
|
||||
const stopForStation = (stops || []).find(s => {
|
||||
@@ -472,20 +560,36 @@ export default function ClassiquesDeparts(){
|
||||
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)));
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
|
||||
}) || null;
|
||||
horaire._status = displayStatus;
|
||||
horaire._displayTime = displayTime;
|
||||
horaire._displayPlatform = (stopForStation && (stopForStation.quai || stopForStation.platform || stopForStation.quay)) || horaire.platform || '';
|
||||
// utiliser quai en priorité
|
||||
horaire._displayPlatform = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay)) || horaire.quai || horaire.quai_reel || horaire.platform || '';
|
||||
return horaire;
|
||||
});
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, formatTimeFromHM, perturbations, gareDisplay, gareIdParam, getStops, normalizeName]);
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setShowStatus(s => !s), 3000);
|
||||
// Alternance toutes les 4 secondes (type+numéro ↔ statut)
|
||||
const id = setInterval(() => setShowStatus(s => !s), 4000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
// Cycle 5s visible / 5s caché pour la cause de perturbation
|
||||
useEffect(() => {
|
||||
let timeoutId;
|
||||
const scheduleCycle = (currentlyVisible) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
setShowInfoCause(!currentlyVisible);
|
||||
scheduleCycle(!currentlyVisible);
|
||||
}, 5000);
|
||||
};
|
||||
setShowInfoCause(true);
|
||||
scheduleCycle(true);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, []);
|
||||
|
||||
// Récupération Supabase — maintenant avec polling pour synchroniser sans recharger la page
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
@@ -554,29 +658,6 @@ export default function ClassiquesDeparts(){
|
||||
|
||||
// stocker la liste complète (non filtrée) pour associer horaire_id -> numero_train
|
||||
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
|
||||
// (soit comme gare_depart, soit comme un arrêt intermédiaire dans `stops` / `gares_desservies`)
|
||||
// Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus)
|
||||
@@ -586,11 +667,11 @@ export default function ClassiquesDeparts(){
|
||||
displayHoraires = displayHoraires.filter(h => {
|
||||
// Exclure si la gare ciblée est la gare d'arrivée finale
|
||||
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (destName && (destName === normalized || destName.includes(normalized) || normalized.includes(destName))) return false;
|
||||
if (destName === normalized) return false;
|
||||
|
||||
// Inclure si gare de départ correspond
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString());
|
||||
if (departName && (departName === normalized || departName.includes(normalized) || normalized.includes(departName))) return true;
|
||||
if (departName === normalized) return true;
|
||||
|
||||
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max)
|
||||
const stopsList = getStops(h);
|
||||
@@ -607,7 +688,7 @@ export default function ClassiquesDeparts(){
|
||||
if (!stopName) continue;
|
||||
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
|
||||
if (isTerminus) continue;
|
||||
if (stopName === normalized || stopName.includes(normalized) || normalized.includes(stopName)) return true;
|
||||
if (stopName === normalized) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -820,23 +901,14 @@ export default function ClassiquesDeparts(){
|
||||
return false;
|
||||
};
|
||||
|
||||
const isSubstitution = (h) => {
|
||||
if (!h) return false;
|
||||
// nouvelle colonne spécifique
|
||||
if (h.substitution_disponible === true) return true;
|
||||
// anciennes variantes
|
||||
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);
|
||||
};
|
||||
|
||||
|
||||
// compute linkage and travaux applicability per horaire
|
||||
const enriched = enrich.map(h => {
|
||||
let linked = false;
|
||||
let appliesToday = false;
|
||||
let appliesTomorrow = false;
|
||||
let linkedTravaux = null;
|
||||
for (const t of travauxList) {
|
||||
// Extraire identifiants de ligne (différentes variantes possibles)
|
||||
const tLine = t.ligne_id || t.ligne || t.line_id || t.line || t.ligneId || t.lineId;
|
||||
@@ -856,12 +928,19 @@ export default function ClassiquesDeparts(){
|
||||
}
|
||||
|
||||
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;
|
||||
let matchRef = ref && (String(ref) === String(h.id) || String(ref) === String(h.numero_train) || String(ref) === String(h.numero));
|
||||
if (matchRef) linked = true;
|
||||
|
||||
const appT = travauxApplies(t,h,today);
|
||||
const appY = travauxApplies(t,h,tomorrow);
|
||||
if (appT) appliesToday = true;
|
||||
if (appY) appliesTomorrow = true;
|
||||
|
||||
if (matchRef || appT || appY) {
|
||||
linkedTravaux = t;
|
||||
}
|
||||
}
|
||||
return { ...h, _linkedToTravaux: linked, _travauxAppliesToday: appliesToday, _travauxAppliesTomorrow: appliesTomorrow };
|
||||
return { ...h, _linkedToTravaux: linked, _travauxAppliesToday: appliesToday, _travauxAppliesTomorrow: appliesTomorrow, _linkedTravauxObj: linkedTravaux };
|
||||
});
|
||||
|
||||
// Build today and tomorrow candidate lists
|
||||
@@ -869,7 +948,7 @@ export default function ClassiquesDeparts(){
|
||||
if (h._date && h._date !== todayStr) return false;
|
||||
if (!circulatesOn(h, today)) return false;
|
||||
if (typeof h._depHM !== 'number') return false;
|
||||
if (h._depHM <= todayHM) return false;
|
||||
if (h._depHM < todayHM - 1) return false;
|
||||
|
||||
// if travaux apply at this horaire today, hide regular horaires
|
||||
if (h._travauxAppliesToday && !isSubstitution(h)) return false;
|
||||
@@ -910,21 +989,21 @@ export default function ClassiquesDeparts(){
|
||||
|
||||
// Construire un index local des perturbations à partir des données fraîchement récupérées
|
||||
// Filtrer pour ne garder que les perturbations applicables aujourd'hui (éviter d'appliquer les perturbations d'hier)
|
||||
const todayYmd = new Date().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 || p.start_date || p.date_debut);
|
||||
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);
|
||||
// si pDate fourni et ne correspond pas à aujourd'hui -> ignorer
|
||||
if (pDate && pDate !== todayYmd) return false;
|
||||
// si fourche de dates fournie et aujourd'hui n'est pas dans l'intervalle -> ignorer
|
||||
if (db && df && !(todayYmd >= db && todayYmd <= df)) return false;
|
||||
// si jours_perturbation fourni -> vérifier qu'aujourd'hui en fait partie
|
||||
// si pDate fourni et ne correspond pas à la date de service -> ignorer
|
||||
if (pDate && pDate !== serviceDateStr) return false;
|
||||
// si fourche de dates fournie et la date de service n'est pas dans l'intervalle -> ignorer
|
||||
if (db && df && !(serviceDateStr >= db && serviceDateStr <= df)) return false;
|
||||
// si jours_perturbation fourni -> vérifier que la date de service en fait partie
|
||||
try {
|
||||
const joursSet = getPerturbationJours(p);
|
||||
if (joursSet && !joursSet.has(todayYmd)) return false;
|
||||
if (joursSet && !joursSet.has(serviceDateStr)) return false;
|
||||
} catch (e) {
|
||||
// ignore parse errors and keep perturbation
|
||||
}
|
||||
@@ -952,8 +1031,8 @@ export default function ClassiquesDeparts(){
|
||||
// Debug: log first few annotated horaires
|
||||
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) {}
|
||||
|
||||
// afficher seulement les 9 premières lignes sur l'afficheur
|
||||
setHoraires((annotated || []).slice(0, 9));
|
||||
// 10 lignes en mode normal, 20 en mode défilement
|
||||
setHoraires((annotated || []).slice(0, defilementMode ? 20 : 10));
|
||||
setPerturbations(filteredPerturb || []);
|
||||
setTravaux(travauxData || []);
|
||||
} catch (err) {
|
||||
@@ -972,124 +1051,223 @@ 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]);
|
||||
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
|
||||
|
||||
// Helpers pour perturbations (memoized plus haut)
|
||||
|
||||
// Création des lignes vides pour créer l'espace
|
||||
const emptyRows = Array.from({ length: 10 }).map((_, i) => ({ _isEmpty: true, id: `empty-${i}` }));
|
||||
|
||||
// Liste de lignes à rendre (horaires + espace + horaires + espace en mode défilement pour boucle infinie)
|
||||
const rowsToRender = defilementMode
|
||||
? [...horaires, ...emptyRows, ...horaires, ...emptyRows]
|
||||
: horaires;
|
||||
|
||||
// Calcul dynamique de l'animation CSS
|
||||
useEffect(() => {
|
||||
if (defilementMode && horaires.length > 0) {
|
||||
// Nombre de lignes de données
|
||||
const dataRows = horaires.length;
|
||||
// 10 lignes vides
|
||||
const emptyRowsCount = 10;
|
||||
|
||||
// Temps pour défiler les données (2s par ligne au lieu de 1s)
|
||||
const dataScrollTime = dataRows * 2;
|
||||
// Temps pour défiler l'espace vide (2s par ligne)
|
||||
const emptyScrollTime = emptyRowsCount * 2;
|
||||
// Temps de pause de 10 secondes
|
||||
const pauseTime = 10;
|
||||
|
||||
// Durée totale d'un cycle (défilement + pause)
|
||||
const totalAnimationTime = dataScrollTime + emptyScrollTime + pauseTime;
|
||||
|
||||
// Calcul des pourcentages pour l'animation
|
||||
// On commence à 0
|
||||
// À la fin du défilement des données, on a défilé de (dataRows / (totalCycleRows * 2)) * 100% de la hauteur totale
|
||||
// À la fin du défilement de l'espace, on a défilé de (totalCycleRows / (totalCycleRows * 2)) * 100% (soit 50%)
|
||||
// Ensuite on fait une pause pendant 10 secondes (donc le % de temps avance, mais pas la translation)
|
||||
|
||||
const scrollEndPercent = ((dataScrollTime + emptyScrollTime) / totalAnimationTime) * 100;
|
||||
|
||||
const styleSheet = document.createElement("style");
|
||||
styleSheet.innerText = `
|
||||
@keyframes customScrollMarquee {
|
||||
0% { transform: translateY(0); }
|
||||
${scrollEndPercent}% { transform: translateY(-50%); }
|
||||
100% { transform: translateY(-50%); }
|
||||
}
|
||||
.aff-classiques-departs__scroll-track {
|
||||
animation: customScrollMarquee ${totalAnimationTime}s linear infinite !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleSheet);
|
||||
|
||||
return () => {
|
||||
document.head.removeChild(styleSheet);
|
||||
};
|
||||
}
|
||||
}, [defilementMode, horaires]);
|
||||
|
||||
|
||||
const renderRow = (h, idx) => {
|
||||
if (h._isEmpty) {
|
||||
return <li className="aff-classiques-departs__row" key={`empty-${idx}`} style={{ background: 'transparent' }}></li>;
|
||||
}
|
||||
|
||||
const horaire = h || {};
|
||||
const op = horaire.type_train || horaire.op || '';
|
||||
const isCarTER = (op === 'Car TER' || op === 'Autocar');
|
||||
const time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
|
||||
const destination = horaire.gare_arrivee_nom || '';
|
||||
const platform = (horaire._displayPlatform || '').toString().trim();
|
||||
const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null);
|
||||
const stationName = gareDisplay || horaire.gare_depart_nom || horaire.gare_depart || horaire.origin_name || null;
|
||||
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
||||
const isSuppressed = status && status.label === 'supprimé';
|
||||
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
|
||||
? (horaire.motif_substitution && horaire.motif_substitution !== 'TRAVAUX' && horaire.motif_substitution !== 'Travaux.' ? horaire.motif_substitution : 'Autocar cour de la gare Plateforme 14')
|
||||
: (status && status.causeDetails ? status.causeDetails : '');
|
||||
const trainNumber = horaire.numero_train || horaire.numero || horaire.number || '';
|
||||
|
||||
return (
|
||||
<li className="aff-classiques-departs__row" key={idx}>
|
||||
|
||||
{/* COL 1 : Logo opérateur */}
|
||||
<div className="aff-classiques-departs__col-logo">
|
||||
{isCarTER ? (
|
||||
<div className="aff-classiques-departs__logo-box">
|
||||
<img src="/type/sncf-voyageurs-logo.png" alt="SNCF Voyageurs" className="aff-classiques-departs__op-logo" />
|
||||
</div>
|
||||
) : (
|
||||
<Logo
|
||||
op={op}
|
||||
classNameBox="aff-classiques-departs__logo-box"
|
||||
classNameImg="aff-classiques-departs__op-logo"
|
||||
classNameText="aff-classiques-departs__logo-text"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* COL 2 : Type + Numéro (4s) OU Statut (4s) */}
|
||||
<div className="aff-classiques-departs__col-meta">
|
||||
{showStatus ? (
|
||||
isSuppressed ? (
|
||||
<div className="aff-classiques-departs__status-suppressed">supprimé</div>
|
||||
) : isDelayed ? (
|
||||
<div className="aff-classiques-departs__status-delayed">
|
||||
<div className="aff-classiques-departs__status-label">retard</div>
|
||||
<div className="aff-classiques-departs__status-value">{status.delayMinutes} min.</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="aff-classiques-departs__status-ontime">à l'heure</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div className="aff-classiques-departs__train-type">{op}</div>
|
||||
<div className="aff-classiques-departs__train-number">{trainNumber}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* COL 3 : Heure (grande, jaune) */}
|
||||
<div className="aff-classiques-departs__col-time">
|
||||
<div className="aff-classiques-departs__time">{time}</div>
|
||||
</div>
|
||||
|
||||
{/* COL 4 : Destination + Info cause (slide-in 5s/5s) */}
|
||||
<div className="aff-classiques-departs__col-destination">
|
||||
<MarqueeText text={destination} className="aff-classiques-departs__destination" />
|
||||
{showInfoCause && infoText ? (
|
||||
<div className="aff-classiques-departs__info-wrapper">
|
||||
<div className="aff-classiques-departs__info-inner">
|
||||
<div className="aff-classiques-departs__separator">|</div>
|
||||
<div className="aff-classiques-departs__info-text">{infoText}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* COL 5 : Quai OU icône bus */}
|
||||
<div className="aff-classiques-departs__col-platform">
|
||||
{isSub || isCarTER ? (
|
||||
<img src="/type/track-car.svg" alt="Car" style={{ width: '6.5vh', height: '6.5vh' }} />
|
||||
) : showPlatform ? (
|
||||
<div className="aff-classiques-departs__platform-box">
|
||||
<div className="aff-classiques-departs__platform-text">{platform}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="aff-classiques-departs__container">
|
||||
{/* nom de la gare configurable via ?gare=Nom-de-la-gare */}
|
||||
|
||||
{/* Watermark "départs" — diagonal droite */}
|
||||
<div className="aff-classiques-departs__watermark">départs</div>
|
||||
<ul className="aff-classiques-departs__list">
|
||||
{horaires.length === 0 ? (
|
||||
<li className="aff-classiques-departs__row aff-classiques-departs__row--empty" key="empty">
|
||||
<div>Aucun horaire disponible.</div>
|
||||
|
||||
{defilementMode ? (
|
||||
/* ── MODE DÉFILEMENT : marquee vertical infini ── */
|
||||
<div className="aff-classiques-departs__scroll-container">
|
||||
{horaires.length === 0 ? (
|
||||
<ul className="aff-classiques-departs__list">
|
||||
<li className="aff-classiques-departs__row aff-classiques-departs__row--empty">
|
||||
<div>Aucun horaire disponible.</div>
|
||||
</li>
|
||||
</ul>
|
||||
) : (
|
||||
<ul
|
||||
className="aff-classiques-departs__scroll-track"
|
||||
>
|
||||
{rowsToRender.map((h, idx) => renderRow(h, idx))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* ── MODE NORMAL : 10 lignes statiques ── */
|
||||
<ul className="aff-classiques-departs__list">
|
||||
{horaires.length === 0 ? (
|
||||
<li className="aff-classiques-departs__row aff-classiques-departs__row--empty" 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 || '';
|
||||
// Use enriched display time (computed during fetch) — fallback to formatting raw fields
|
||||
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
|
||||
|
||||
const destination = horaire.gare_arrivee_nom || '';
|
||||
const platform = (horaire._displayPlatform || horaire.platform || '').toString().trim();
|
||||
// attachedPerturbs/rowObj previously used for mocked status; status is now computed from perturbations per stop
|
||||
|
||||
// determine station context (the display station) — prefer explicit gareIdParam/gareDisplay if provided
|
||||
const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null);
|
||||
const stationName = (gareDisplay) ? gareDisplay : (horaire.gare_depart_nom || horaire.gare_depart || horaire.origin_name || null);
|
||||
|
||||
// prefer precomputed status and display time (synchronisé depuis la BDD) — sinon calculer à la volée
|
||||
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
||||
|
||||
if (status && status.label === 'supprimé' && showStatus) {
|
||||
time = '';
|
||||
}
|
||||
// if the status is 'supprimé' and the component is currently showing the status (showStatus toggles every 3s), hide the time
|
||||
if (status && status.label === 'supprimé' && showStatus) {
|
||||
time = '';
|
||||
} else if (!horaire._displayTime && status && typeof status.delayMinutes === 'number' && !isNaN(status.delayMinutes)) {
|
||||
// if we have _depHM (minutes since midnight) use it
|
||||
if (typeof horaire._depHM === 'number') {
|
||||
time = formatTimeFromHM(horaire._depHM + status.delayMinutes);
|
||||
} else {
|
||||
// fallback: try to parse original time string
|
||||
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_depart || horaire.time || horaire.heure || horaire._rawTime || '');
|
||||
if (typeof base === 'number') time = formatTimeFromHM(base + status.delayMinutes);
|
||||
}
|
||||
}
|
||||
const statusClass = status.label === 'supprimé' ? 'aff-classiques-departs__train-meta--suppressed' : (status.label && String(status.label).startsWith('retardé') ? 'aff-classiques-departs__train-meta--delayed' : '');
|
||||
|
||||
// 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 nowHM = currentTime.getHours()*60 + currentTime.getMinutes(); // Use currentTime
|
||||
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' && 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="aff-classiques-departs__row" key={idx}>
|
||||
<div className="aff-classiques-departs__col-logo">
|
||||
<Logo op={op} />
|
||||
</div>
|
||||
<div className="aff-classiques-departs__col-train-type">
|
||||
{showStatus ? (
|
||||
<div className={`aff-classiques-departs__train-meta aff-classiques-departs__train-status ${statusClass}`} aria-live="polite">
|
||||
<div className="aff-classiques-departs__op-name">{status.label}</div>
|
||||
<div className="aff-classiques-departs__op-number">{status.detail}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="aff-classiques-departs__train-meta">
|
||||
<div className="aff-classiques-departs__op-name">{op}</div>
|
||||
<div className="aff-classiques-departs__op-number">{number}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="aff-classiques-departs__col-time">
|
||||
<div className="aff-classiques-departs__time">{time}</div>
|
||||
</div>
|
||||
<div className="aff-classiques-departs__col-destination">
|
||||
<div className="aff-classiques-departs__destination">{destination}</div>
|
||||
</div>
|
||||
{showPlatform ? (
|
||||
<div className="aff-classiques-departs__col-platform">
|
||||
<div className="aff-classiques-departs__platform-box">
|
||||
<div className="aff-classiques-departs__platform-text">{platform}</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{/* Footer visuel de l'afficheur — barre foncée en bas */}
|
||||
<div className="aff-classiques-departs__footer" aria-hidden>
|
||||
<div className="aff-classiques-departs__footer-inner">
|
||||
<Clock />
|
||||
) : horaires.map((h, idx) => renderRow(h, idx))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{/* Horloge — superposée en bas à droite */}
|
||||
<div className="aff-classiques-departs__footer" aria-hidden>
|
||||
<Clock />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user