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,70 +1,6 @@
|
||||
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import './ClassiquesDepartsQuais.css';
|
||||
import supabase from '../../../../lib/supabase';
|
||||
|
||||
// Hook pour récupérer la map des logos depuis /type/data.json
|
||||
function useTypeLogos(){
|
||||
const [map, setMap] = useState({ default: '/type/quais/logo-sncf-color.svg' });
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
const load = async () => {
|
||||
const candidates = ['/type/quais/data-color.json', '/type/data.json'];
|
||||
for(const url of candidates){
|
||||
try{
|
||||
const res = await fetch(url);
|
||||
if(!res.ok) continue;
|
||||
const data = await res.json();
|
||||
if(!mounted) return;
|
||||
const logos = {};
|
||||
if(Array.isArray(data.logos)){
|
||||
data.logos.forEach(item => {
|
||||
// item.file is expected (ex: logo-tgv.svg). If absent, try to extract from path
|
||||
let file = item.file;
|
||||
if(!file && item.path){
|
||||
const parts = String(item.path).split('/');
|
||||
file = parts[parts.length-1];
|
||||
}
|
||||
if(!file) return;
|
||||
const basename = file.replace(/\.[^/.]+$/, '');
|
||||
const semantic = basename.replace(/^logo-/, '');
|
||||
const key = semantic.toUpperCase().replace(/[^A-Z0-9]+/g, '-');
|
||||
// Prefer color logos in the 'quais' subfolder if data came from the quais file
|
||||
if(url.includes('/quais/')){
|
||||
logos[key] = `/type/quais/${file}`;
|
||||
} else {
|
||||
logos[key] = `/type/${file}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
logos.default = logos['SNCF-VOYAGEURS-LOGO'] || logos['SNFC-VOYAGEURS-LOGO'] || logos['SNCF'] || '/type/sncf-voyageurs-logo.png';
|
||||
setMap(logos);
|
||||
return;
|
||||
}catch(e){
|
||||
// try next candidate (ignore error and continue loop)
|
||||
}
|
||||
}
|
||||
// no candidate succeeded — keep fallback
|
||||
};
|
||||
|
||||
load();
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
return map;
|
||||
}
|
||||
|
||||
function getTypeLogo(type, TYPE_LOGOS_MAP){
|
||||
const map = TYPE_LOGOS_MAP || { default: '/type/sncf-voyageurs-logo.png' };
|
||||
if(!type) return map.default;
|
||||
const norm = String(type).toUpperCase().replace(/[^A-Z0-9]+/g, '-');
|
||||
if(map[norm]) return map[norm];
|
||||
const keys = Object.keys(map).filter(k => k !== 'default');
|
||||
for(const k of keys){
|
||||
if(k.includes(norm) || norm.includes(k)) return map[k];
|
||||
}
|
||||
return map.default;
|
||||
}
|
||||
|
||||
function Clock() {
|
||||
const [now, setNow] = useState(new Date());
|
||||
useEffect(() => {
|
||||
@@ -75,20 +11,15 @@ function Clock() {
|
||||
const mm = String(now.getMinutes()).padStart(2,'0');
|
||||
const ss = String(now.getSeconds()).padStart(2,'0');
|
||||
return (
|
||||
<div className="aff-quais__clock">
|
||||
<div className="aff-quais__clock-box"><span className="aff-quais__clock-main">{hh}:{mm}:{ss}</span></div>
|
||||
<div className="aff-quais-departs-redesign__clock">
|
||||
<span>{hh}</span>
|
||||
<span>{mm}</span>
|
||||
<span className="aff-quais-departs-redesign__clock-s">{ss}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ClassiquesDepartsQuais(){
|
||||
const typeLogos = useTypeLogos();
|
||||
|
||||
const rightRef = useRef(null);
|
||||
const stopsListRef = useRef(null);
|
||||
const railRef = useRef(null);
|
||||
|
||||
// fallback mock strict (utilisé uniquement si aucune donnée réelle n'est trouvée)
|
||||
export default function ClassiquesDepartsQuais() {
|
||||
const mock = {
|
||||
time: '16h44',
|
||||
status: "à l'heure",
|
||||
@@ -101,26 +32,37 @@ export default function ClassiquesDepartsQuais(){
|
||||
],
|
||||
};
|
||||
|
||||
// état pour l'horaire sélectionné
|
||||
const [selectedHoraire, setSelectedHoraire] = useState(null);
|
||||
const [loadingHoraire, setLoadingHoraire] = useState(false);
|
||||
const [horaireError, setHoraireError] = useState(null);
|
||||
const [candidatesCount, setCandidatesCount] = useState(0);
|
||||
// type de la gare (récupéré depuis la table gares)
|
||||
const [, setHoraireError] = useState(null);
|
||||
const [, setCandidatesCount] = useState(0);
|
||||
const [stationType, setStationType] = useState(null);
|
||||
// Heure actuelle mise à jour chaque seconde pour la logique de seuil
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const [alternateDelayPhase, setAlternateDelayPhase] = useState(true);
|
||||
const [alternateCancelPhase, setAlternateCancelPhase] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setCurrentTime(new Date()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
// perturbations/travaux (récupérées depuis Supabase)
|
||||
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([]);
|
||||
|
||||
// helpers locaux (simples) — similaires à ceux du composant principal
|
||||
const weekdayNameForColumn = useCallback((d) => {
|
||||
const i = d.getDay();
|
||||
switch(i) {
|
||||
@@ -141,20 +83,19 @@ export default function ClassiquesDepartsQuais(){
|
||||
const day = d.getDate();
|
||||
const s = `${y}-${String(m).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
const holidays = [
|
||||
`${y}-01-01`, // Jour de l'an
|
||||
`${y}-05-01`, // Fête du Travail
|
||||
`${y}-05-08`, // Victoire 1945
|
||||
`${y}-07-14`, // Fête nationale
|
||||
`${y}-08-15`, // Assomption
|
||||
`${y}-11-01`, // Toussaint
|
||||
`${y}-11-11`, // Armistice 1918
|
||||
`${y}-12-25`, // Noël
|
||||
`${y}-01-01`,
|
||||
`${y}-05-01`,
|
||||
`${y}-05-08`,
|
||||
`${y}-07-14`,
|
||||
`${y}-08-15`,
|
||||
`${y}-11-01`,
|
||||
`${y}-11-11`,
|
||||
`${y}-12-25`,
|
||||
];
|
||||
// Dates variables pour 2026
|
||||
if (y === 2026) {
|
||||
holidays.push('2026-04-06'); // Lundi de Pâques
|
||||
holidays.push('2026-05-14'); // Ascension
|
||||
holidays.push('2026-05-25'); // Lundi de Pentecôte
|
||||
holidays.push('2026-04-06');
|
||||
holidays.push('2026-05-14');
|
||||
holidays.push('2026-05-25');
|
||||
}
|
||||
return holidays.includes(s);
|
||||
}, []);
|
||||
@@ -215,8 +156,6 @@ export default function ClassiquesDepartsQuais(){
|
||||
return String(v).slice(0,5).replace(':','h');
|
||||
}, []);
|
||||
|
||||
// ...existing code...
|
||||
|
||||
const formatTimeFromHM = useCallback((totalMinutes) => {
|
||||
if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return '';
|
||||
const m = ((totalMinutes % 1440) + 1440) % 1440;
|
||||
@@ -225,24 +164,21 @@ export default function ClassiquesDepartsQuais(){
|
||||
return `${hh}h${mm}`;
|
||||
}, []);
|
||||
|
||||
// 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
|
||||
if (t === 'interurbaine' || t.includes('interurb')) return 360;
|
||||
if (t === 'ville' || t.includes('ville')) return 30;
|
||||
return 0;
|
||||
}, []);
|
||||
|
||||
// lecture des query params pour gare & quai
|
||||
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 debugEnabled = (paramsForThis.get('debug') === '1' || String(paramsForThis.get('debug')).toLowerCase() === 'true');
|
||||
|
||||
const gareDisplayParam = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g,' ')).replace(/-/g,' ') : null;
|
||||
|
||||
// helper: obtenir la liste d'arrêts d'un horaire (supporte plusieurs noms de champs)
|
||||
const getStops = (h) => {
|
||||
if (!h) return [];
|
||||
if (Array.isArray(h.stops) && h.stops.length) return h.stops;
|
||||
@@ -255,13 +191,12 @@ export default function ClassiquesDepartsQuais(){
|
||||
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 */ }
|
||||
try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) return parsed; } catch (e) {}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// --- Helpers pour perturbations (copiées/adaptées depuis ClassiquesDeparts) ---
|
||||
const safeParseArrets = (p) => {
|
||||
const raw = p && (p.perturbations_arrets || p.arrets || p.arrets_perturbations);
|
||||
if (!raw) return [];
|
||||
@@ -386,7 +321,7 @@ export default function ClassiquesDepartsQuais(){
|
||||
for (const p of found) {
|
||||
try {
|
||||
if (horaireYmd) {
|
||||
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);
|
||||
if (pDate && pDate !== horaireYmd) continue;
|
||||
@@ -436,7 +371,15 @@ export default function ClassiquesDepartsQuais(){
|
||||
return { label: "à l'heure", detail: '', delayMinutes: null };
|
||||
}, [findPerturbsForHoraireAndStop, parseDelayMinutesFromArret, getPerturbationJours, normalizeName, parsePerturbArretsObject]);
|
||||
|
||||
// Fetch perturbations & travaux from Supabase periodically
|
||||
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;
|
||||
@@ -452,19 +395,32 @@ export default function ClassiquesDepartsQuais(){
|
||||
]);
|
||||
if (!mounted) return;
|
||||
if (pError || tError) {
|
||||
// ignore quietly — keep previous data
|
||||
} else {
|
||||
// Filter perturbations to those applicable today
|
||||
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 || 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);
|
||||
if (pDate && pDate !== todayYmd) return false;
|
||||
if (db && df && !(todayYmd >= db && todayYmd <= df)) return false;
|
||||
try { const joursSet = getPerturbationJours(p); if (joursSet && !joursSet.has(todayYmd)) return false; } catch (e) {}
|
||||
|
||||
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; }
|
||||
});
|
||||
@@ -472,7 +428,6 @@ export default function ClassiquesDepartsQuais(){
|
||||
setTravaux(travauxData || []);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
inFlight = false;
|
||||
}
|
||||
@@ -482,7 +437,6 @@ export default function ClassiquesDepartsQuais(){
|
||||
return () => { mounted = false; clearInterval(id); };
|
||||
}, [getPerturbationJours]);
|
||||
|
||||
// Annotate selectedHoraire with computed status when perturbations change
|
||||
useEffect(() => {
|
||||
if (!selectedHoraire || !selectedHoraire.horaire) return;
|
||||
const horaire = selectedHoraire.horaire;
|
||||
@@ -491,20 +445,36 @@ export default function ClassiquesDepartsQuais(){
|
||||
const stationName = stop ? (typeof stop === 'string' ? stop : (stop.gare_nom || stop.nom || stop.name || stop.stop_name || '')) : null;
|
||||
try {
|
||||
const map = buildPerturbMap(perturbations || []);
|
||||
const status = computeStatusForHoraireAtStop(horaire, stationId, stationName, map, null);
|
||||
// keep original scheduled time for display (_displayTime)
|
||||
|
||||
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_depart || horaire.time || horaire.heure);
|
||||
const displayTimeOriginal = (typeof originalBase === 'number') ? formatTimeFromHM(originalBase) : (selectedHoraire.rawTime ? formatTimeNoSeconds(selectedHoraire.rawTime) : (horaire.heure_depart || horaire.time || horaire.heure ? formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure) : null));
|
||||
// compute updated time (delayed) but store separately — do not override the original displayed time
|
||||
let updatedTime = null;
|
||||
if (status && typeof status.delayMinutes === 'number' && !Number.isNaN(status.delayMinutes) && typeof originalBase === 'number') {
|
||||
updatedTime = formatTimeFromHM(originalBase + status.delayMinutes);
|
||||
}
|
||||
setSelectedHoraire(prev => ({ ...(prev||{}), horaire: { ...(prev && prev.horaire ? prev.horaire : {}), _status: status }, _displayTime: displayTimeOriginal, _updatedTime: updatedTime }));
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
// 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]);
|
||||
|
||||
// récupérer depuis Supabase le prochain départ correspondant au couple (gare, quai)
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
async function loadNextForQuai() {
|
||||
@@ -517,17 +487,23 @@ export default function ClassiquesDepartsQuais(){
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Récupérer le type de la gare
|
||||
let fetchedType = null;
|
||||
try {
|
||||
if (gareIdParam) {
|
||||
const { data: g } = await supabase.from('gares').select('type').eq('gare_id', gareIdParam).single();
|
||||
if (g) fetchedType = g.type;
|
||||
} else if (gareDisplayParam) {
|
||||
const { data: g } = await supabase.from('gares').select('type').ilike('nom', `%${gareDisplayParam}%`).limit(1).single();
|
||||
if (g) fetchedType = g.type;
|
||||
const { data: gdata } = await supabase.from('gares').select('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;
|
||||
});
|
||||
fetchedType = exactMatch ? exactMatch.type : gdata[0].type;
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
} catch (e) {}
|
||||
if (mounted) setStationType(fetchedType);
|
||||
|
||||
const { data: all, error } = await supabase.from('horaires').select('id, *');
|
||||
@@ -555,15 +531,12 @@ export default function ClassiquesDepartsQuais(){
|
||||
for (const h of all) {
|
||||
if (!h) continue;
|
||||
|
||||
// Filtrage par jour de circulation
|
||||
const circulatesToday = circulatesOn(h, now);
|
||||
const circulatesTomorrow = circulatesOn(h, tomorrow);
|
||||
if (!circulatesToday && !circulatesTomorrow) continue;
|
||||
|
||||
// build stops list (support multiple field names)
|
||||
const stops = getStops(h);
|
||||
|
||||
// compute max index for terminus detection if stops contain index
|
||||
let maxIdx = null;
|
||||
if (Array.isArray(stops) && stops.length) {
|
||||
for (const st of stops) {
|
||||
@@ -574,34 +547,24 @@ export default function ClassiquesDepartsQuais(){
|
||||
}
|
||||
}
|
||||
|
||||
// find matching stop (by name or by gare_id)
|
||||
let stopMatch = null;
|
||||
// check horaire-level depart name first
|
||||
if (normalizedTarget) {
|
||||
// exclude horaires where the target gare is the final destination (terminus)
|
||||
const destNameNorm = normalizeName(h.gare_arrivee_nom || h.gare_arrivee || '');
|
||||
if (destNameNorm && (destNameNorm === normalizedTarget || destNameNorm.includes(normalizedTarget) || normalizedTarget.includes(destNameNorm))) {
|
||||
// gare ciblée est la gare d'arrivée finale -> ne pas considérer cet horaire
|
||||
// skip to next horaire
|
||||
} else {
|
||||
const departName = normalizeName(h.gare_depart_nom || h.gare_depart || h.origin_name || '');
|
||||
if (departName && (departName === normalizedTarget || departName.includes(normalizedTarget) || normalizedTarget.includes(departName))) {
|
||||
// créer un stopMatch initial à partir du niveau horaire
|
||||
let initialStop = { heure_depart: h.heure_depart || h.time || h.heure, gare_nom: h.gare_depart_nom || h.gare_depart };
|
||||
// si la liste stops contient un objet correspondant à la gare de départ, préférer cet objet
|
||||
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)) {
|
||||
// prefer the stops entry (it may contain platform/quai information)
|
||||
// but preserve a horaire-level time if the stop entry lacks heure_depart
|
||||
if (typeof s === 'object') {
|
||||
if (!s.heure_depart && initialStop.heure_depart) s.heure_depart = initialStop.heure_depart;
|
||||
initialStop = s;
|
||||
} else {
|
||||
// s is string name — keep initialStop but set gare_nom
|
||||
initialStop.gare_nom = s;
|
||||
}
|
||||
break;
|
||||
@@ -613,15 +576,12 @@ export default function ClassiquesDepartsQuais(){
|
||||
}
|
||||
}
|
||||
|
||||
// if gare_id param present, try to match by id as well
|
||||
if (!stopMatch && gareIdParam) {
|
||||
const gid = String(gareIdParam);
|
||||
// check horaire-level id
|
||||
const departId = h.gare_depart_id || h.gare_id || h.origin_id || null;
|
||||
if (departId != null && String(departId) === gid) {
|
||||
stopMatch = { heure_depart: h.heure_depart || h.time || h.heure, gare_id: departId, gare_nom: h.gare_depart_nom || h.gare_depart };
|
||||
}
|
||||
// check stops list ids
|
||||
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;
|
||||
@@ -630,35 +590,29 @@ export default function ClassiquesDepartsQuais(){
|
||||
}
|
||||
}
|
||||
|
||||
// if still no match, try match by normalized name inside stops
|
||||
if (!stopMatch && normalizedTarget && Array.isArray(stops) && stops.length) {
|
||||
for (const s of stops) {
|
||||
// support both string entries and object entries
|
||||
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)) {
|
||||
// detect if this stop is a terminus (isLast / is_last / index == maxIdx)
|
||||
const isTerminus = Boolean((typeof s === 'object') && (s.isLast || s.is_last || s.isTerminus || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx)));
|
||||
if (isTerminus) continue; // ne pas considérer les terminus
|
||||
if (isTerminus) continue;
|
||||
stopMatch = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!stopMatch) continue; // this horaire does not serve the target gare (or match failed)
|
||||
if (!stopMatch) continue;
|
||||
|
||||
// determine platform/quai for this stop/horaire
|
||||
const platform = (stopMatch && (stopMatch.quai || stopMatch.platform || stopMatch.quay)) || h.platform || h.quai || '';
|
||||
const normPlatform = String(platform || '').toLowerCase().replace(/\s+/g,'');
|
||||
|
||||
// if targetQuai is set, ensure it matches (loose compare: contains or equals)
|
||||
if (targetQuai) {
|
||||
if (!normPlatform) continue; // no platform info -> skip
|
||||
if (!normPlatform) continue;
|
||||
if (!(normPlatform === targetQuai || normPlatform.includes(targetQuai) || targetQuai.includes(normPlatform))) continue;
|
||||
}
|
||||
|
||||
// Ne garder que les horaires qui ont un départ au stop (on ne veut pas afficher les arrivées)
|
||||
const stopIsObject = stopMatch && typeof stopMatch === 'object';
|
||||
const hasDeparture = stopIsObject && (
|
||||
typeof stopMatch.heure_depart !== 'undefined' || typeof stopMatch.departure_time !== 'undefined' || typeof stopMatch.departure !== 'undefined' || typeof stopMatch.time !== 'undefined' || typeof stopMatch.departureTime !== 'undefined' || typeof stopMatch.horaire_time !== 'undefined'
|
||||
@@ -666,15 +620,12 @@ export default function ClassiquesDepartsQuais(){
|
||||
const hasArrivalOnly = stopIsObject && !hasDeparture && (
|
||||
typeof stopMatch.heure_arrivee !== 'undefined' || typeof stopMatch.arrival_time !== 'undefined' || typeof stopMatch.arrivee !== 'undefined' || typeof stopMatch.arrival !== 'undefined'
|
||||
);
|
||||
// si le stop correspond mais n'a que l'heure d'arrivée (pas de départ), ignorer cet horaire — on veut uniquement des départs
|
||||
if (stopIsObject && hasArrivalOnly) continue;
|
||||
|
||||
// pick time from stopMatch (si on a une heure de départ) ou fallback au niveau horaire
|
||||
const rawTime = (hasDeparture ? (stopMatch.heure_depart || stopMatch.time || stopMatch.heure || stopMatch.departure_time || stopMatch.departure || stopMatch.departureTime || stopMatch.horaire_time) : (h.heure_depart || h.time || h.heure || h.departure_time || h.departure || h.departureTime || h.horaire_time));
|
||||
const depHM = parseTimeHM(rawTime);
|
||||
if (depHM == null) continue;
|
||||
|
||||
// compute minutes until departure (allow next-day wrap)
|
||||
let minutesUntil = depHM - nowHM;
|
||||
if (minutesUntil < 0) minutesUntil = depHM + 24*60 - nowHM;
|
||||
|
||||
@@ -688,13 +639,15 @@ export default function ClassiquesDepartsQuais(){
|
||||
return;
|
||||
}
|
||||
|
||||
// split into today (circulatesToday AND depHM > nowHM) and tomorrow (circulatesTomorrow AND (depHM <= nowHM OR not circulatesToday))
|
||||
const todayCandidates = candidates.filter(c => c.circulatesToday && c.depHM > nowHM).sort((a,b) => a.depHM - b.depHM);
|
||||
const tomorrowCandidates = candidates.filter(c => c.circulatesTomorrow && (c.depHM <= nowHM || !c.circulatesToday)).sort((a,b) => a.depHM - b.depHM);
|
||||
|
||||
let chosen = null;
|
||||
if (todayCandidates.length > 0) chosen = todayCandidates[0];
|
||||
else if (tomorrowCandidates.length > 0) chosen = tomorrowCandidates[0];
|
||||
if (todayCandidates.length > 0) {
|
||||
chosen = { ...todayCandidates[0], isTomorrow: false };
|
||||
} else if (tomorrowCandidates.length > 0) {
|
||||
chosen = { ...tomorrowCandidates[0], isTomorrow: true };
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setCandidatesCount(candidates.length);
|
||||
@@ -712,232 +665,149 @@ export default function ClassiquesDepartsQuais(){
|
||||
return () => { mounted = false; };
|
||||
}, [gareDisplayParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]);
|
||||
|
||||
// toggle flag to alternate time/status when needed (observe selectedHoraire.status when present)
|
||||
const [showTime, setShowTime] = useState(true);
|
||||
// derive a simple status label string to use in the dependency array (avoids complex expressions)
|
||||
const _selectedStatusLabel = (() => {
|
||||
const horaireObj = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
|
||||
const computedLabel = horaireObj && horaireObj._status && horaireObj._status.label ? String(horaireObj._status.label) : null;
|
||||
const rawStatus = horaireObj && horaireObj.status ? (typeof horaireObj.status === 'string' ? horaireObj.status : (horaireObj.status && horaireObj.status.label ? String(horaireObj.status.label) : null)) : null;
|
||||
return String(computedLabel || rawStatus || mock.status || '').toLowerCase();
|
||||
// Rail is handled purely in CSS for better performance and alignment
|
||||
const showPlatform = (() => {
|
||||
const sched = selectedHoraire ? selectedHoraire.depHM : null;
|
||||
if (sched === null || !stationType) return true;
|
||||
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
|
||||
const threshold = thresholdForStationType(stationType);
|
||||
return nowHM >= (sched - threshold);
|
||||
})();
|
||||
|
||||
useEffect(()=>{
|
||||
let id = null;
|
||||
if (/retard|supprim/.test(_selectedStatusLabel)) {
|
||||
// ensure we start by showing the time
|
||||
setShowTime(true);
|
||||
id = setInterval(() => setShowTime(prev => !prev), 3000);
|
||||
} else {
|
||||
setShowTime(true);
|
||||
}
|
||||
return () => { if (id) clearInterval(id); };
|
||||
}, [_selectedStatusLabel, mock.status]);
|
||||
|
||||
// compute and position the rail so it covers from first stop center to last stop center
|
||||
useEffect(() => {
|
||||
function updateRail(){
|
||||
const rail = railRef.current;
|
||||
const list = stopsListRef.current;
|
||||
const right = rightRef.current;
|
||||
if(!rail || !list || !right) return;
|
||||
const items = Array.from(list.querySelectorAll('li'));
|
||||
if(items.length === 0) return;
|
||||
const first = items[0];
|
||||
// find destination (last with dest class) or fallback to last item
|
||||
const last = items.find(el => el.classList.contains('aff-quais__stop--strict-dest')) || items[items.length-1];
|
||||
|
||||
const rightRect = right.getBoundingClientRect();
|
||||
const firstRect = first.getBoundingClientRect();
|
||||
const lastRect = last.getBoundingClientRect();
|
||||
|
||||
const firstCenterY = firstRect.top + firstRect.height/4 - rightRect.top;
|
||||
const lastCenterY = lastRect.top + lastRect.height/2 - rightRect.top;
|
||||
|
||||
// add small padding so rail extends a bit beyond centers
|
||||
const pad = 9;
|
||||
const top = Math.max(0, firstCenterY - pad);
|
||||
const height = Math.max(8, (lastCenterY - firstCenterY) + pad*2);
|
||||
|
||||
// compute horizontal position so dots are centered next to the text
|
||||
const firstText = first.querySelector('.aff-quais__stop-text--strict') || first.querySelector('.aff-quais__stop-text');
|
||||
const textRect = firstText ? firstText.getBoundingClientRect() : firstRect;
|
||||
const centerX = (textRect.left - rightRect.left) - 28; // place rail left of text by ~20px
|
||||
const railWidth = 35;
|
||||
const left = centerX - railWidth/2;
|
||||
|
||||
// apply styles for the rail rectangle
|
||||
rail.style.top = `${Math.round(top)}px`;
|
||||
rail.style.height = `${Math.round(height)}px`;
|
||||
rail.style.left = `${Math.round(left)}px`;
|
||||
rail.style.width = `${railWidth}px`;
|
||||
// remove background-image reliance and render individual dots as DOM children
|
||||
// clear existing dots
|
||||
while (rail.firstChild) rail.removeChild(rail.firstChild);
|
||||
|
||||
// create a dot element for each item and position it exactly at the item's vertical center
|
||||
const railWidthComputed = parseInt(rail.style.width, 8) || railWidth;
|
||||
items.forEach(it => {
|
||||
const r = it.getBoundingClientRect();
|
||||
const centerY = r.top + r.height/2 - (rightRect.top + top);
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'aff-quais__rail-dot';
|
||||
dot.style.position = 'absolute';
|
||||
dot.style.left = '50%';
|
||||
dot.style.transform = 'translate(-50%, -50%)';
|
||||
dot.style.top = `${Math.round(centerY)}px`;
|
||||
// set dot diameter to rail width
|
||||
dot.style.width = `${railWidthComputed}px`;
|
||||
dot.style.height = `${railWidthComputed}px`;
|
||||
dot.style.borderRadius = '50%';
|
||||
dot.style.background = '#ffe600';
|
||||
dot.style.boxShadow = '0 0 0 4px rgba(255,230,102,0.06) inset';
|
||||
rail.appendChild(dot);
|
||||
});
|
||||
}
|
||||
|
||||
updateRail();
|
||||
window.addEventListener('resize', updateRail);
|
||||
const obs = new MutationObserver(updateRail);
|
||||
const listNode = stopsListRef.current;
|
||||
if(listNode) obs.observe(listNode, { childList:true, subtree:true });
|
||||
return ()=>{
|
||||
window.removeEventListener('resize', updateRail);
|
||||
if(obs) obs.disconnect();
|
||||
};
|
||||
}, [selectedHoraire]);
|
||||
// Observer changement des stops via selectedHoraire
|
||||
// Re-run rail effect lorsque selectedHoraire change (les stops affichés changent)
|
||||
|
||||
const displayTimeOrStatus = () => {
|
||||
const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
|
||||
const statusStrFromField = horaire ? (horaire.status || mock.status) : mock.status;
|
||||
const statusObj = horaire && horaire._status ? horaire._status : null;
|
||||
// prefer precomputed _displayTime if available
|
||||
const time = selectedHoraire && selectedHoraire._displayTime ? selectedHoraire._displayTime : (horaire ? formatTimeNoSeconds(selectedHoraire.rawTime || horaire.heure_depart || horaire.time || horaire.heure) : mock.time);
|
||||
|
||||
// build canonical status label when we have computed status object
|
||||
let canonicalStatus = null;
|
||||
if (statusObj) {
|
||||
if (typeof statusObj.delayMinutes === 'number' && !Number.isNaN(statusObj.delayMinutes)) {
|
||||
canonicalStatus = `retardé +${Math.round(statusObj.delayMinutes)} min.`;
|
||||
} else if (statusObj.label && String(statusObj.label).toLowerCase().includes('supprim')) {
|
||||
canonicalStatus = 'supprimé';
|
||||
} else if (statusObj.label) {
|
||||
canonicalStatus = String(statusObj.label);
|
||||
}
|
||||
}
|
||||
|
||||
const s = (canonicalStatus || statusStrFromField || '').toLowerCase();
|
||||
const isAlert = s.includes('retard') || s.includes('supprim');
|
||||
const label = canonicalStatus || statusStrFromField || '';
|
||||
|
||||
// Logique de seuil d'affichage du quai
|
||||
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
|
||||
const sched = selectedHoraire && selectedHoraire.depHM ? selectedHoraire.depHM : null;
|
||||
const threshold = thresholdForStationType(stationType);
|
||||
const showPlatform = (sched !== null) ? (nowHM >= (sched - threshold)) : true;
|
||||
|
||||
if (!showPlatform) {
|
||||
return (
|
||||
<span className="aff-quais__time-status-wrapper" aria-live="polite">
|
||||
<span className="aff-quais__time aff-quais__time--strict visible">--h--</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Always render both time and status inside a fixed-size wrapper to avoid layout shift
|
||||
if (!loadingHoraire && !selectedHoraire) {
|
||||
return (
|
||||
<span className="aff-quais__time-status-wrapper" aria-live="polite">
|
||||
<span className={`aff-quais__time aff-quais__time--strict ${showTime ? 'visible' : 'hidden'}`}>{time}</span>
|
||||
<span className={`aff-quais__time-status aff-quais__time-status--strict aff-quais__status--alert ${(!showTime && isAlert) ? 'visible' : 'hidden'}`}>{label}</span>
|
||||
</span>
|
||||
<div className="aff-quais-departs-redesign__container aff-quais-departs-redesign__empty">
|
||||
<div className="aff-quais-departs-redesign__empty-text">Aucun Départ Prévu</div>
|
||||
<div className="aff-quais-departs-redesign__footer">
|
||||
<div className="aff-quais-departs-redesign__footer-left"></div>
|
||||
<div className="aff-quais-departs-redesign__footer-right">
|
||||
<Clock />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="aff-quais__container aff-quais__container--strict aff-quais__container--depart">
|
||||
<div className="aff-quais__left aff-quais__left--strict">
|
||||
{/* logo (depuis le type de train) */}
|
||||
{(() => {
|
||||
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;
|
||||
return <img src={getTypeLogo(trainType, typeLogos)} alt={trainType || 'train'} className="aff-quais__type-logo--strict" />;
|
||||
})()}
|
||||
return (
|
||||
<div className="aff-quais-departs-redesign__container">
|
||||
{/* LEFT PANEL: White Background */}
|
||||
<div className="aff-quais-departs-redesign__left">
|
||||
{/* SNCF Logo */}
|
||||
<img src="/type/quais/logo-sncf-color.svg" alt="SNCF" className="aff-quais-departs-redesign__sncf-logo" />
|
||||
|
||||
<div className="aff-quais__watermark" aria-hidden>départ</div>
|
||||
{/* affichage des états de chargement / erreur pour debug */}
|
||||
{horaireError ? <div className="aff-quais__error" style={{color:'crimson',fontSize:'0.9rem',marginTop:6}}>{horaireError}</div> : null}
|
||||
{loadingHoraire ? <div className="aff-quais__loading" style={{color:'#666',fontSize:'0.85rem',marginTop:6}}>Chargement...</div> : null}
|
||||
{debugEnabled ? (
|
||||
<div style={{marginTop:8, fontSize:'0.78rem', color:'#333'}}>
|
||||
<div>Candidates: {candidatesCount}</div>
|
||||
{selectedHoraire ? <pre style={{maxHeight:120,overflow:'auto',background:'#f7f7f7',padding:6}}>{JSON.stringify(selectedHoraire, null, 2)}</pre> : null}
|
||||
</div>
|
||||
) : null}
|
||||
{/* logo du type en haut à droite */}
|
||||
<div className="aff-quais__row aff-quais__row--time-status">
|
||||
<div className="aff-quais-departs-redesign__time-status">
|
||||
{(() => {
|
||||
const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
|
||||
const status = horaire ? (horaire.status || (horaire._status && horaire._status.label) || mock.status) : mock.status;
|
||||
const time = horaire ? formatTimeNoSeconds(selectedHoraire.rawTime || horaire.heure_depart || horaire.time || horaire.heure) : mock.time;
|
||||
const isAlert = (status || '').toLowerCase().includes('retard') || (status || '').toLowerCase().includes('supprim');
|
||||
if (isAlert) return displayTimeOrStatus();
|
||||
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_depart || 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-departs-redesign__time--long-text';
|
||||
if (isTimeSuppressed) timeClass += ' aff-quais-departs-redesign__time--suppressed';
|
||||
if (isTimeDelayed) timeClass += ' aff-quais-departs-redesign__time--delayed';
|
||||
|
||||
let statusClass = '';
|
||||
if (isSuppressed) statusClass += ' aff-quais-departs-redesign__status--suppressed';
|
||||
if (isDelayed) statusClass += ' aff-quais-departs-redesign__status--delayed';
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="aff-quais__time aff-quais__time--strict">{time}</span>
|
||||
<span className="aff-quais__time-status aff-quais__time-status--strict">{status}</span>
|
||||
<span className={`aff-quais-departs-redesign__time${timeClass}`}>{timeText}</span>
|
||||
{statusText && <span className={`aff-quais-departs-redesign__status${statusClass}`}>{String(statusText).toLowerCase()}</span>}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="aff-quais__destination aff-quais__destination--strict">
|
||||
|
||||
<div className="aff-quais-departs-redesign__destination">
|
||||
{(() => {
|
||||
const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
|
||||
const stop = selectedHoraire && selectedHoraire.stop ? selectedHoraire.stop : null;
|
||||
// destinationBig is the final destination of the train (gare_arrivee_*). The second line should be the station name of the stop on the route (if present) — avoid duplicating final destination.
|
||||
const destinationBig = horaire ? (horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination || '') : mock.destinationBig;
|
||||
const rawDestExpr = (stop ? ((typeof stop === 'string') ? stop : (stop.gare_nom || stop.nom || stop.name || stop.stop_name || '')) : (horaire ? (horaire.destination_station || '') : mock.destinationStation));
|
||||
return (
|
||||
<>
|
||||
<div className="aff-quais__destination-line1 aff-quais__destination-line1--strict">{destinationBig}</div>
|
||||
|
||||
</>
|
||||
);
|
||||
return showPlatform ? (horaire ? (horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination || '') : mock.destinationBig) : '--';
|
||||
})()}
|
||||
</div>
|
||||
<div className="aff-quais__train-id aff-quais__train-id--strict">{(() => {
|
||||
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 (<>{trainType} <span className="aff-quais__train-number--strict">{trainNumber}</span></>);
|
||||
})()}</div>
|
||||
|
||||
<div className="aff-quais-departs-redesign__train-info">
|
||||
{(() => {
|
||||
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 ? (
|
||||
<>
|
||||
<span className="aff-quais-departs-redesign__train-type">{trainType}</span>
|
||||
<span className="aff-quais-departs-redesign__train-number">{trainNumber}</span>
|
||||
</>
|
||||
) : '';
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Accessibility Icons */}
|
||||
<div className="aff-quais-departs-redesign__icons">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#7e859b" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="4" r="1.5" fill="#7e859b" />
|
||||
<path d="M9 13a4 4 0 0 0 4 4h3" />
|
||||
<path d="M12 7.5v6" />
|
||||
<path d="M8 20a5 5 0 1 1 5-5" />
|
||||
</svg>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#7e859b" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginLeft: '12px' }}>
|
||||
<circle cx="5.5" cy="17.5" r="3.5" />
|
||||
<circle cx="18.5" cy="17.5" r="3.5" />
|
||||
<path d="M15 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm-3 11.5V14l-3-3 4-3 2 3h2" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Watermark */}
|
||||
<div className="aff-quais-departs-redesign__watermark">départ</div>
|
||||
</div>
|
||||
<div className="aff-quais__separator--strict" aria-hidden></div>
|
||||
<div className="aff-quais__right aff-quais__right--strict" ref={rightRef}>
|
||||
{/* barre verticale de points (élément explicite pour fiabilité) */}
|
||||
<div className="aff-quais__stops-rail" aria-hidden ref={railRef} />
|
||||
<ul className="aff-quais__stops-list--strict" ref={stopsListRef}>
|
||||
|
||||
{/* RIGHT PANEL: Blue Background with Stops */}
|
||||
<div className="aff-quais-departs-redesign__right">
|
||||
<ul className="aff-quais-departs-redesign__stops-list">
|
||||
{(() => {
|
||||
if (!showPlatform) return null;
|
||||
const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
|
||||
// utiliser la helper getStops pour supporter plusieurs formats
|
||||
let stops = horaire ? getStops(horaire) : [];
|
||||
if (!Array.isArray(stops) || stops.length === 0) {
|
||||
// fallback parsing si la colonne est une chaîne JSON
|
||||
if (horaire && typeof horaire.stops === 'string') {
|
||||
try { const p = JSON.parse(horaire.stops); if (Array.isArray(p)) stops = p; } catch (e) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(stops) || stops.length === 0) stops = mock.stops || [];
|
||||
|
||||
// construire une liste propre en extrayant un label pour chaque stop
|
||||
const labeled = stops.map((stop, idx) => {
|
||||
let label = '';
|
||||
if (typeof stop === 'string') label = stop;
|
||||
else if (stop && typeof stop === 'object') {
|
||||
// vérifier plusieurs clés possibles
|
||||
label = stop.gare_nom || stop.nom || stop.name || stop.stop_name || stop.stopName || stop.station_name || stop.station || stop.label || stop.stop || stop.name_gare || (stop.properties && (stop.properties.name || stop.properties.stop_name)) || '';
|
||||
// si l'objet possède une clé 'stop' qui est elle-même un objet
|
||||
if (!label && stop.stop && typeof stop.stop === 'object') {
|
||||
label = stop.stop.name || stop.stop.stop_name || '';
|
||||
}
|
||||
@@ -948,7 +818,6 @@ export default function ClassiquesDepartsQuais(){
|
||||
|
||||
if (labeled.length === 0) return null;
|
||||
|
||||
// trouver l'index du stop correspondant à la gare demandée (selectedHoraire.stop)
|
||||
let matchedIndex = -1;
|
||||
try {
|
||||
if (selectedHoraire && selectedHoraire.stop) {
|
||||
@@ -957,7 +826,6 @@ export default function ClassiquesDepartsQuais(){
|
||||
const targetId = (typeof target === 'object') ? (target.gare_id || target.gareId || target.id || target.stop_id || target.stopId || null) : null;
|
||||
for (let i = 0; i < stops.length; i++) {
|
||||
const s = stops[i];
|
||||
// compare by id first if available
|
||||
if (targetId != null) {
|
||||
const sid = (s && typeof s === 'object') ? (s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null) : null;
|
||||
if (sid != null && String(sid) === String(targetId)) { matchedIndex = i; break; }
|
||||
@@ -968,11 +836,8 @@ export default function ClassiquesDepartsQuais(){
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
// filtrer pour ne garder que les prochains arrêts après la gare renseignée
|
||||
let subsequent = labeled.filter(it => (matchedIndex >= 0) ? (it.idx > matchedIndex) : true);
|
||||
// trier par idx pour être sûr
|
||||
let subsequent = labeled.filter(it => (matchedIndex >= 0) ? (it.idx >= matchedIndex) : true);
|
||||
subsequent = subsequent.sort((a,b) => a.idx - b.idx);
|
||||
// Remove consecutive duplicate labels (case-insensitive)
|
||||
if (subsequent.length > 1) {
|
||||
const collapsed = [];
|
||||
for (const it of subsequent) {
|
||||
@@ -985,28 +850,45 @@ export default function ClassiquesDepartsQuais(){
|
||||
}
|
||||
subsequent = collapsed;
|
||||
}
|
||||
// si aucun arrêt après (ou pas de match) on peut retourner null ou le fallback; ici on renvoie le fallback si tout échoue
|
||||
if (subsequent.length === 0) return null;
|
||||
|
||||
return subsequent.map((item, mapIndex) => {
|
||||
const isLast = item.idx === (stops.length - 1);
|
||||
const isLast = mapIndex === subsequent.length - 1;
|
||||
const stop = item.original || {};
|
||||
const stopId = stop.gare_id || stop.gareId || stop.id || stop.stop_id || stop.stopId || null;
|
||||
const stopStatus = computeStatusForHoraireAtStop(horaire, stopId, item.label, perturbMap, serviceDate);
|
||||
const isStopSuppressed = stopStatus && stopStatus.label === 'supprimé';
|
||||
const isStopDelayed = stopStatus && typeof stopStatus.delayMinutes === 'number';
|
||||
const stopDelayMin = stopStatus ? stopStatus.delayMinutes : null;
|
||||
|
||||
let stopTextClass = '';
|
||||
if (isStopSuppressed) stopTextClass += ' aff-quais-departs-redesign__stop-text--suppressed';
|
||||
if (isStopDelayed) stopTextClass += ' aff-quais-departs-redesign__stop-text--delayed';
|
||||
|
||||
return (
|
||||
<li key={`${item.label}-${item.idx}-${mapIndex}`} className={isLast ? 'aff-quais__stop--strict aff-quais__stop--strict-dest' : 'aff-quais__stop--strict'}>
|
||||
<span className="aff-quais__stop-bullet--strict" />
|
||||
<span className="aff-quais__stop-text--strict">{item.label}</span>
|
||||
<li key={`${item.label}-${item.idx}-${mapIndex}`} className={`aff-quais-departs-redesign__stop ${isLast ? 'aff-quais-departs-redesign__stop--dest' : ''}`}>
|
||||
<span className="aff-quais-departs-redesign__stop-bullet" />
|
||||
<span className={`aff-quais-departs-redesign__stop-text${stopTextClass}`}>
|
||||
{item.label}
|
||||
{isStopSuppressed && <span className="aff-quais-departs-redesign__stop-status-badge aff-quais-departs-redesign__stop-status-badge--suppressed"> (non desservi)</span>}
|
||||
{isStopDelayed && <span className="aff-quais-departs-redesign__stop-status-badge aff-quais-departs-redesign__stop-status-badge--delayed"> (+{stopDelayMin} min)</span>}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</ul>
|
||||
</div>
|
||||
{/* footer band same as classic departures with clock on the right */}
|
||||
<div className="aff-quais__footer" aria-hidden>
|
||||
<div className="aff-quais__footer-inner">
|
||||
<div className="aff-quais__footer-left"></div>
|
||||
<div className="aff-quais__footer-right"><Clock /></div>
|
||||
|
||||
{/* BOTTOM FOOTER: Full Width */}
|
||||
<div className="aff-quais-departs-redesign__footer">
|
||||
<div className="aff-quais-departs-redesign__footer-left">
|
||||
ain.
|
||||
</div>
|
||||
<div className="aff-quais-departs-redesign__footer-right">
|
||||
<Clock />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user