import React, { useEffect, useState, useCallback } from 'react'; import './ClassiquesDepartsQuais.css'; import supabase from '../../../../lib/supabase'; import { fetchAllRows } from '../../../../lib/fetchAllRows'; import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime'; import useAutoAnnonce from '../../../../hooks/useAutoAnnonce'; import { getSourceFromPath } from '../../../../services/railwayService'; import { getSncfTimetable } from '../../../../services/sncfApi'; import { matchStationNameStrict } from '../../../../services/gtfsStations'; import GareVideoprotection from '../../../../components/afficheurs/GareVideoprotection'; import AttentifsEnsemble from '../../../../components/afficheurs/AttentifsEnsemble'; import DepartEnCoursClassique from '../../../../components/afficheurs/DepartEnCoursClassique'; import ClassiquesInformations from '../ClassiquesInformations'; import ClassiquesDeparts from '../ClassiquesDeparts'; import TgvCompositionQuai from '../../../../components/afficheurs/TgvCompositionQuai'; function TgvServiceIcons() { return (
{/* PMR / Fauteuil roulant */} {/* Vélo */} {/* Bar / Verre */}
); } function DigitalClock() { const [now, setNow] = useState(new Date()); useEffect(() => { const id = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(id); }, []); const hh = String(now.getHours()).padStart(2, '0'); const mm = String(now.getMinutes()).padStart(2, '0'); const ss = String(now.getSeconds()).padStart(2, '0'); return (
{hh}:{mm} {ss}
); } function getTrainBrand(trainType = '', operator = '', isDeparture = true) { const combined = `${trainType || ''} ${operator || ''}`.toLowerCase().trim(); if (combined.includes('zou')) { return { logo: '/type/quais/logo-zou-color.svg', label: 'ZOU!', showVoyageursText: false }; } if (combined.includes('fluo')) { return { logo: '/type/quais/logo-fluo-color.svg', label: 'TER Fluo', showVoyageursText: false }; } if (combined.includes('aleop') || combined.includes('al\u00e9op')) { return { logo: '/type/quais/logo-aleop-color.svg', label: 'Aléop', showVoyageursText: false }; } if (combined.includes('breizh')) { return { logo: '/type/quais/logo-breizhgo-color.svg', label: 'BreizhGo', showVoyageursText: false }; } if (combined.includes('lio') || combined.includes('li\u00f4')) { return { logo: '/type/quais/logo-lio-color.svg', label: 'liO', showVoyageursText: false }; } if (combined.includes('mobigo')) { return { logo: '/type/quais/logo-mobigo-color.svg', label: 'Mobigo', showVoyageursText: false }; } if (combined.includes('nomad')) { return { logo: '/type/quais/logo-nomad-color.svg', label: 'Nomad', showVoyageursText: false }; } if (combined.includes('remi') || combined.includes('r\u00e9mi')) { return { logo: '/type/quais/logo-remi-color.svg', label: 'Rémi', showVoyageursText: false }; } if (combined.includes('inoui') || combined.includes('tgv inoui')) { return { logo: '/type/quais/logo-inoui-color.svg', label: 'TGV INOUI', showVoyageursText: false }; } if (combined.includes('ouigo classique')) { return { logo: '/type/quais/logo-ouigo-classique-color.svg', label: 'OUIGO Classique', showVoyageursText: false }; } if (combined.includes('ouigo')) { return { logo: '/type/quais/logo-ouigo-color.svg', label: 'OUIGO', showVoyageursText: false }; } if (combined.includes('intercite') || combined.includes('intercit\u00e9')) { return { logo: '/type/quais/logo-intercite-color.svg', label: 'INTERCITÉS', showVoyageursText: false }; } if (combined.includes('tgv')) { return { logo: '/type/quais/logo-tgv-color.svg', label: 'TGV', showVoyageursText: false }; } if (combined.includes('eurostar')) { return { logo: '/type/quais/logo-eurostar-color.svg', label: 'Eurostar', showVoyageursText: false }; } if (combined.includes('freccia') || combined.includes('trenitalia')) { return { logo: '/type/quais/logo-frecciarossa-color.svg', label: 'Frecciarossa', showVoyageursText: false }; } if (combined.includes('ice') || combined.includes('db')) { return { logo: '/type/quais/logo-ice-color.svg', label: 'ICE', showVoyageursText: false }; } if (combined.includes('ter')) { return { logo: '/type/quais/logo-ter-color.svg', label: 'TER', showVoyageursText: false }; } // Par défaut Départs : Logo SNCF Voyageurs (Image 1) if (isDeparture) { return { logo: '/type/quais/logo-sncf-color.svg', label: trainType || 'SNCF', showVoyageursText: true }; } return { logo: '/type/quais/logo-ter-color.svg', label: trainType || 'TER', showVoyageursText: false }; } export default function ClassiquesDepartsQuais() { const searchParams = new URLSearchParams(window.location.search || ''); let gareParam = searchParams.get('gare') || searchParams.get('station') || ''; const gareIdParam = searchParams.get('gare_id') || searchParams.get('gareId') || null; const quaiParam = searchParams.get('quai') || searchParams.get('voie') || searchParams.get('track') || searchParams.get('platform') || ''; // Fallback si la gare est dans le chemin if (!gareParam && typeof window !== 'undefined') { const cleanPath = window.location.pathname.replace(/^\/ferrovia/, ''); const parts = cleanPath.split('/').filter(Boolean); const qIdx = parts.findIndex(p => p === 'quais'); if (qIdx >= 0 && parts[qIdx + 1]) { gareParam = parts.slice(qIdx + 1).join('/'); } } const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null; const mock = { time: '09h11', status: "à l'heure", trainType: 'ZOU!', trainNumber: '881027', destination: 'Aubagne', stops: [ 'Marseille Blancarde', 'La Pomme', 'St Marcel', 'La Barasse', 'La Penne sur Huveaune', 'Aubagne' ] }; const [selectedHoraire, setSelectedHoraire] = useState(null); const [, setLoadingHoraire] = useState(false); const [, setHoraireError] = useState(null); const [currentTime, setCurrentTime] = useState(new Date()); const [idlePhase, setIdlePhase] = useState(0); // 0 = Attentifs (20s), 1 = Quai (20s), 2 = Information (10s) const [showStatusAlt, setShowStatusAlt] = useState(false); const pageParam = (searchParams.get('page') || searchParams.get('mode') || '').toLowerCase(); const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true'; const isForceDepartEnCours = pageParam === 'depart_en_cours' || searchParams.get('depart_en_cours') === 'true' || searchParams.get('test_depart') === 'true'; const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null); useAnnonceSonoreRealtime(resolvedGareId, 'ClassiquesDepartsQuais', quaiParam); useAutoAnnonce(selectedHoraire ? [selectedHoraire] : [], currentTime, false); // Horloge 1s useEffect(() => { const id = setInterval(() => setCurrentTime(new Date()), 1000); return () => clearInterval(id); }, []); // Alternance 3 secondes : Heure normale ↔ Statut (pour trains retardés ou supprimés) useEffect(() => { const id = setInterval(() => setShowStatusAlt(prev => !prev), 3000); return () => clearInterval(id); }, []); // Alternance par défaut (20s Attentifs -> 20s Quai -> 10s Information) si aucun train n'est prévu sur le quai useEffect(() => { if (selectedHoraire) return; let duration = 20000; if (idlePhase === 2) duration = 10000; else duration = 20000; const timer = setTimeout(() => { setIdlePhase(prev => (prev + 1) % 3); }, duration); return () => clearTimeout(timer); }, [idlePhase, selectedHoraire]); const normalizeName = useCallback((s) => { if (!s) return ''; try { return String(s) .normalize('NFD') .replace(/\p{M}/gu, '') .toLowerCase() .trim() .replace(/[-_]+/g, ' ') .replace(/\s+/g, ' '); } catch (e) { return String(s).toLowerCase().trim(); } }, []); const cleanPlatform = useCallback((p) => { if (!p) return ''; return String(p) .toLowerCase() .replace(/^(?:voie|quai|track|platform)\s*/i, '') .replace(/\s+/g, '') .trim(); }, []); const parseTimeHM = useCallback((s) => { if (!s) return null; if (typeof s === 'number') return s % 1440; const m = String(s).match(/(\d{1,2})[:h](\d{2})/i); if (m) { const h = parseInt(m[1], 10) % 24; return h * 60 + parseInt(m[2], 10); } const d = new Date(s); if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes(); return null; }, []); const formatTimeNoSeconds = useCallback((v) => { if (v == null) return ''; const tryDate = new Date(v); if (!isNaN(tryDate.getTime())) { const hh = String(tryDate.getHours() % 24).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0'); return `${hh}h${mm}`; } if (typeof v === 'string') { const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); if (m) { const h = parseInt(m[1], 10) % 24; return `${String(h).padStart(2, '0')}h${m[2]}`; } const m2 = v.match(/T(\d{2}):(\d{2})/); if (m2) { const h = parseInt(m2[1], 10) % 24; return `${String(h).padStart(2, '0')}h${m2[2]}`; } const m3 = v.match(/(\d{1,2})h(\d{2})/i); if (m3) { const h = parseInt(m3[1], 10) % 24; return `${String(h).padStart(2, '0')}h${m3[2]}`; } return v.slice(0, 5).replace(':', 'h'); } return String(v).slice(0, 5).replace(':', 'h'); }, []); const circulatesOn = useCallback((h, d) => { if (!h || !d) return false; const pad = (n) => String(n).padStart(2, '0'); const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`; const targetDay = d.getDay(); if (Array.isArray(h.dates) && h.dates.length > 0) { return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen); } const customDaysRaw = h.jours_personnalises; if (customDaysRaw) { let customDays = []; if (Array.isArray(customDaysRaw)) customDays = customDaysRaw; else if (typeof customDaysRaw === 'string') { try { customDays = JSON.parse(customDaysRaw); } catch (e) {} } if (Array.isArray(customDays) && customDays.length > 0) { const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact); if (match) return Boolean(match.circule); } } const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']; const colName = `circule_${dayNames[targetDay]}`; if (typeof h[colName] !== 'undefined' && h[colName] !== null) { const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null; const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null; if (db && targetDateHyphen < db) return false; if (df && targetDateHyphen > df) return false; return Boolean(h[colName]); } if (Array.isArray(h.days) && h.days.length > 0) { if (!h.days.includes(targetDay)) return false; if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false; if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false; return true; } const singleDate = h.date_depart || h.date || h.service_date; if (singleDate) { const sStr = String(singleDate).slice(0, 10); return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact; } return true; }, []); const getStops = useCallback((h) => { if (!h) return []; if (Array.isArray(h.stops) && h.stops.length) return h.stops; if (Array.isArray(h.gares_desservies) && h.gares_desservies.length) return h.gares_desservies; if (Array.isArray(h.garesDesservies) && h.garesDesservies.length) return h.garesDesservies; const candidates = ['stops', 'gares_desservies', 'garesDesservies', 'gares_desservies_json', 'arrets']; for (const key of candidates) { const raw = h[key]; if (!raw) continue; if (Array.isArray(raw)) return raw; if (typeof raw === 'string') { try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) return parsed; } catch (e) {} } } return []; }, []); // Chargement du train au départ const loadNextForQuai = useCallback(async () => { setLoadingHoraire(true); setHoraireError(null); const source = getSourceFromPath(window.location.pathname); const targetQuaiClean = cleanPlatform(quaiParam); let chosenTrain = null; // 1. Tenter la source SNCF si applicable if (source === 'sncf' || !supabase) { try { const { data: sncfData } = await getSncfTimetable({ gareName: gareDisplay || gareParam, mode: 'departs', quai: quaiParam }); if (sncfData && sncfData.length > 0) { let matchingTrains = sncfData; if (targetQuaiClean) { matchingTrains = sncfData.filter(t => cleanPlatform(t._displayPlatform || t.quai || t.voie || t.quai_reel) === targetQuaiClean); } const train = matchingTrains[0]; if (train) { const depHM = parseTimeHM(train.heure_depart || train.time || train.heure); const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || ''; const trainNumClean = (train.numero_train || train.num || '').toString(); // Recherche d'une deuxième tranche / train couplé au même horaire const partner = matchingTrains.find(t => { if (!t || t === train) return false; const tNum = (t.numero_train || t.num || '').toString(); const tHM = parseTimeHM(t.heure_depart || t.time || t.heure); return tNum !== trainNumClean && tHM === depHM; }); chosenTrain = { horaire: { ...train, quai: trainPlatform, quai_reel: trainPlatform, voie: trainPlatform, _displayPlatform: trainPlatform, _status: train._status || { label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"), detail: '', delayMinutes: train.retard_minutes || null, causeDetails: train.cause_retard || null } }, partner: partner || null, stop: { gare_nom: train.gare_depart_nom || gareDisplay || gareParam, heure_depart: train.heure_depart || train.time || train.heure, quai: trainPlatform }, depHM: depHM != null ? depHM : 0, rawTime: train.heure_depart || train.time || train.heure }; } } } catch (e) { console.warn('Notice getSncfTimetable:', e); } } // 2. Si pas trouvé via SNCF, ou si mode Supabase actif, interroger Supabase if (!chosenTrain && supabase) { try { if (gareIdParam) { setResolvedGareId(gareIdParam); } else if (gareDisplay) { const { data: gdata } = await supabase.from('gares').select('id,nom').ilike('nom', `%${gareDisplay}%`); if (gdata && gdata.length) { const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay)); const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null); if (chosen) setResolvedGareId(chosen.id); } } const { data: all, error } = await fetchAllRows(supabase.from('horaires').select('*')); if (!error && Array.isArray(all) && all.length > 0) { const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(now.getDate() + 1); const nowHM = now.getHours() * 60 + now.getMinutes(); const candidates = []; for (const h of all) { if (!h) continue; const circulatesToday = circulatesOn(h, now); const circulatesTomorrow = circulatesOn(h, tomorrow); const stops = getStops(h); let stopMatch = null; if (gareDisplay) { const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString(); if (matchStationNameStrict(destName, gareDisplay)) { // Destination finale -> terminus ici, pas un départ continue; } const departName = (h.gare_depart_nom || h.gare_depart || '').toString(); if (matchStationNameStrict(departName, gareDisplay)) { stopMatch = { heure_depart: h.heure_depart || h.time || h.heure, gare_nom: h.gare_depart_nom || h.gare_depart, quai_reel: h.quai_reel || h.voie || h.quai }; } if (!stopMatch && Array.isArray(stops)) { for (const s of stops) { const sName = (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || '')).toString(); if (matchStationNameStrict(sName, gareDisplay)) { stopMatch = s; break; } } } } else { stopMatch = { heure_depart: h.heure_depart || h.time || h.heure, gare_nom: h.gare_depart_nom || h.gare_depart, quai_reel: h.quai_reel || h.voie || h.quai }; } if (!stopMatch) continue; const rawTime = (stopMatch && typeof stopMatch === 'object' && (stopMatch.heure_depart || stopMatch.heure || stopMatch.time || stopMatch.heure_passage)) || h.heure_depart || h.time || h.heure; const depHM = parseTimeHM(rawTime); if (depHM == null) continue; let platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.voie)) || h.quai_reel || h.voie || h.platform || h.quai || ''; const platClean = cleanPlatform(platform); if (targetQuaiClean) { if (!platClean || platClean !== targetQuaiClean) { continue; } } candidates.push({ horaire: h, stop: stopMatch, depHM, rawTime, platClean, circulatesToday, circulatesTomorrow }); } // Filtrage strict par quai si demandé let filteredCandidates = candidates; if (targetQuaiClean) { filteredCandidates = candidates.filter(c => c.platClean === targetQuaiClean); } const todayCandidates = filteredCandidates.filter(c => c.circulatesToday && c.depHM >= nowHM - 1).sort((a, b) => a.depHM - b.depHM); const tomorrowCandidates = filteredCandidates.filter(c => c.circulatesTomorrow && (c.depHM < nowHM - 1 || !c.circulatesToday)).sort((a, b) => a.depHM - b.depHM); const chosen = todayCandidates.length > 0 ? todayCandidates[0] : (tomorrowCandidates.length > 0 ? tomorrowCandidates[0] : (filteredCandidates.length > 0 ? filteredCandidates.sort((a, b) => a.depHM - b.depHM)[0] : null)); if (chosen) { const partnerCand = filteredCandidates.find(c => { if (!c || c === chosen) return false; const cNum = (c.horaire.numero_train || c.horaire.num || '').toString(); const chosenNum = (chosen.horaire.numero_train || chosen.horaire.num || '').toString(); return cNum !== chosenNum && c.depHM === chosen.depHM; }); chosenTrain = { ...chosen, partner: partnerCand ? partnerCand.horaire : null }; } } } catch (err) { console.error('Erreur chargement horaire Supabase quai:', err); } } setSelectedHoraire(chosenTrain); setLoadingHoraire(false); }, [gareDisplay, gareParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, cleanPlatform, circulatesOn, getStops]); useEffect(() => { loadNextForQuai(); const interval = setInterval(loadNextForQuai, 15000); return () => clearInterval(interval); }, [loadNextForQuai]); // Données actives (réelles ou maquette Image 1 de secours) const horaire = selectedHoraire?.horaire; const rawDepartureTime = selectedHoraire ? (selectedHoraire.rawTime || horaire?.heure_depart || horaire?.time) : (gareParam ? null : mock.time); const formattedTime = rawDepartureTime ? formatTimeNoSeconds(rawDepartureTime) : ''; const statusObj = horaire?._status; const isSuppressed = statusObj?.label === 'supprimé' || horaire?.etat === 'supprime'; const isDelayed = (statusObj && typeof statusObj.delayMinutes === 'number' && statusObj.delayMinutes > 0) || (horaire?.retard_minutes > 0); const delayMinutes = statusObj?.delayMinutes || horaire?.retard_minutes || 0; let displayStatusText = "à l'heure"; if (isSuppressed) { displayStatusText = 'supprimé'; } else if (isDelayed) { displayStatusText = `retardé +${delayMinutes}min.`; } const destinationName = horaire ? (horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination || '') : (gareParam ? '' : mock.destination); const trainType = horaire ? (horaire.type_train || horaire.train_type || horaire.op || horaire.commercial_mode || 'TER') : (gareParam ? '' : mock.trainType); const trainNumber = horaire ? (horaire.numero_train || horaire.numero || horaire.number || '') : (gareParam ? '' : mock.trainNumber); const operator = horaire ? (horaire.operator || horaire.op || '') : ''; // Résolution dynamique du logo couleur const brand = getTrainBrand(trainType, operator, true); // Calcul de la liste des arrêts pour le panneau droit let stopsList = []; if (horaire) { const rawStops = getStops(horaire); if (Array.isArray(rawStops) && rawStops.length > 0) { stopsList = rawStops.map(s => (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''))).filter(Boolean); } // Filtrer les arrêts à partir de la gare courante if (gareDisplay && stopsList.length > 0) { const foundIdx = stopsList.findIndex(s => matchStationNameStrict(s, gareDisplay)); if (foundIdx >= 0 && foundIdx < stopsList.length - 1) { stopsList = stopsList.slice(foundIdx + 1); } } // Garantir que le terminus est présent en dernier if (destinationName) { const lastStop = stopsList.length > 0 ? stopsList[stopsList.length - 1] : ''; if (!matchStationNameStrict(lastStop, destinationName)) { stopsList.push(destinationName); } } } // Fallback propre if (!stopsList.length) { if (horaire && destinationName) { stopsList = [destinationName]; } else if (!gareParam) { stopsList = mock.stops; } } // 1. Détection "Départ en cours" (2 minutes avant l'heure de départ réelle) let isDepartEnCours = isForceDepartEnCours; if (!isDepartEnCours && selectedHoraire && rawDepartureTime) { const depHM = parseTimeHM(rawDepartureTime); if (depHM != null) { const nowSec = currentTime.getHours() * 3600 + currentTime.getMinutes() * 60 + currentTime.getSeconds(); const depSec = (depHM + delayMinutes) * 60; let diffSec = depSec - nowSec; if (diffSec < -43200) diffSec += 86400; if (diffSec <= 120 && diffSec >= 0) { isDepartEnCours = true; } } } // AFFICHAGE 1 : Page Vidéoprotection (via query) if (isVideoprotection) { return ; } // AFFICHAGE 2 : Départ en cours (T - 2 min) if (isDepartEnCours) { return ; } // AFFICHAGE 3 : Alternance si aucun train n'est attribué à ce quai if (!selectedHoraire && gareParam) { if (idlePhase === 0) { return ; } if (idlePhase === 1) { return ; } if (idlePhase === 2) { return ; } } const partner = selectedHoraire?.partner; const destinationName1 = destinationName; const destinationName2 = partner ? (partner.gare_arrivee_nom || partner.gare_arrivee || partner.destination || partner.dest || '') : (horaire?.second_destination || horaire?.dest2 || horaire?.destination2 || (horaire?.tranches && horaire?.tranches[1]?.destination) || ''); const trainType1 = trainType; const trainType2 = partner ? (partner.type_train || partner.train_type || partner.type || partner.op || 'TGV') : (horaire?.type_train2 || horaire?.train_type2 || (destinationName2 ? trainType : '')); const trainNumber1 = trainNumber; const trainNumber2 = partner ? (partner.numero_train || partner.numero || partner.num || '') : (horaire?.numero_train2 || horaire?.num2 || (horaire?.tranches && horaire?.tranches[1]?.num) || ''); const isDifferentDest = Boolean(destinationName1 && destinationName2 && !matchStationNameStrict(destinationName1, destinationName2)); // Récupération des arrêts de la deuxième tranche let stopsList2 = []; if (partner) { const rawStops2 = getStops(partner); if (Array.isArray(rawStops2) && rawStops2.length > 0) { stopsList2 = rawStops2.map(s => (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''))).filter(Boolean); } if (gareDisplay && stopsList2.length > 0) { const fIdx = stopsList2.findIndex(s => matchStationNameStrict(s, gareDisplay)); if (fIdx >= 0 && fIdx < stopsList2.length - 1) { stopsList2 = stopsList2.slice(fIdx + 1); } } if (destinationName2 && !matchStationNameStrict(stopsList2[stopsList2.length - 1] || '', destinationName2)) { stopsList2.push(destinationName2); } } // Calcul du tronc commun et des branches de séparation (Fourche Image 2) let commonStops = []; let branch1Stops = []; let branch2Stops = []; if (isDifferentDest && stopsList.length > 0 && stopsList2.length > 0) { let divIdx = 0; const minLen = Math.min(stopsList.length, stopsList2.length); while (divIdx < minLen && matchStationNameStrict(stopsList[divIdx], stopsList2[divIdx])) { commonStops.push(stopsList[divIdx]); divIdx++; } branch1Stops = stopsList.slice(divIdx); branch2Stops = stopsList2.slice(divIdx); } const isTgv = Boolean( (trainType1 || '').toUpperCase().includes('TGV') || (trainType2 || '').toUpperCase().includes('TGV') || (operator || '').toUpperCase().includes('TGV') || (brand && (brand.label || '').toUpperCase().includes('TGV')) || (horaire && ((horaire.type_train || horaire.type || '').toUpperCase().includes('TGV') || (horaire.op || '').toUpperCase().includes('TGV'))) ); return (
{/* PANNEAU GAUCHE : BLANC (Conforme Image 1 & Image 2) */}
{/* Logo Couleur Dynamique */}
{brand.label} {brand.showVoyageursText && (
VOYAGEURS
)}
{/* Heure et Statut */}
{(isSuppressed || isDelayed) ? ( showStatusAlt ? ( {displayStatusText} ) : ( {formattedTime || '--:--'} ) ) : ( <> {formattedTime || '--:--'} {displayStatusText} )}
{/* Destination Principale */}
{destinationName1 || 'Aucun départ prévu'}
{/* Type de Train & Numéro 1 */}
{trainType1 && {trainType1}} {trainNumber1 && {trainNumber1}}
{/* Destination 2 si multi-tranche / destinations différentes (ex: Nice-Ville ou Bruxelles Midi) */} {isDifferentDest && ( <>
{destinationName2}
{trainType2 && {trainType2}} {trainNumber2 && {trainNumber2}}
)} {/* Si même destination mais 2 numéros couplés (ex: 2 TGV départ 16h17 vers Paris) */} {!isDifferentDest && trainNumber2 && trainNumber2 !== trainNumber1 && (
{trainType2 && {trainType2}} {trainNumber2}
)} {/* Pictogrammes de services pour TGV (PMR, Vélo, Bar) */} {isTgv && } {/* Filigrane Vertical "départ" géant (orienté bas vers haut) */}
{/* PANNEAU DROIT : BLEU NUIT AVEC DESSERTE (Conforme Image 1 & Fourche Image 2) */}
{isDifferentDest && commonStops.length > 0 ? ( /* AFFICHAGE FOURCHE / BIFURCATION (Conforme Image 2) */
{/* Tronc commun */}
    {commonStops.map((stopName, idx) => (
  • {stopName}
  • ))}
{/* Ligne d'embranchement avec courbe */}
voitures 11 à 18
{branch2Stops[branch2Stops.length - 1] || destinationName2}
{/* Branche principale continuant vers terminus 1 */} {branch1Stops.length > 0 && (
    {branch1Stops.map((stopName, idx) => { const isLast = idx === branch1Stops.length - 1; return (
  • {stopName}
  • ); })}
)}
) : ( /* AFFICHAGE STANDARD DESSERTE VERTICALE (Conforme Image 1) */ stopsList.length > 0 ? (
    {stopsList.map((stopName, idx) => { const isLast = idx === stopsList.length - 1; return (
  • {stopName}
  • ); })}
) : (
Veuillez consulter les écrans du hall de la gare.
) )}
{/* BANDEAU BAS : TGV COMPOSITION OU BANDEAU STANDARD */} {isTgv ? ( ) : (
{quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai || horaire?.voie || horaire?._displayPlatform ? `Voie ${cleanPlatform(horaire.quai || horaire.voie || horaire._displayPlatform).toUpperCase()}` : '')}
)}
); }