-> REFONTE TOTALE DE TOUS LES AFFICHEURS CLASSIQUES + QUAIS + OPTION TGV FONCTIONNALITÉS EN COURS DE DÉVELOPPEMENT : -> ADAPTABILITÉ EN FONCTION DES SITUATIONS
853 lines
36 KiB
JavaScript
853 lines
36 KiB
JavaScript
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 (
|
|
<div className="aff-classique-quai-dep__services">
|
|
{/* PMR / Fauteuil roulant */}
|
|
<svg width="30" height="30" viewBox="0 0 24 24" fill="#7e859b">
|
|
<path d="M12 2a2 2 0 100 4 2 2 0 000-4zm-2 6a1 1 0 00-1 1v6a1 1 0 001 1h4a1 1 0 001-1v-2a1 1 0 00-2 0v1h-2v-4h3a1 1 0 001-1V9a1 1 0 00-1-1H10zm-1 9a4 4 0 100 8 4 4 0 000-8zm0 2a2 2 0 110 4 2 2 0 010-4z"/>
|
|
</svg>
|
|
{/* Vélo */}
|
|
<svg width="34" height="30" viewBox="0 0 24 24" fill="none" stroke="#7e859b" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<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 100-2 1 1 0 000 2zm-3 11.5L9 11.5l3-3.5h3.5m-6.5 7L12 8"/>
|
|
</svg>
|
|
{/* Bar / Verre */}
|
|
<svg width="28" height="30" viewBox="0 0 24 24" fill="none" stroke="#7e859b" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M4 4L12 14L20 4H4Z" fill="#7e859b" />
|
|
<path d="M12 14V21M8 21H16" />
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<div className="aff-classique-quai-dep__clock">
|
|
<span className="aff-classique-quai-dep__clock-hm">{hh}:{mm}</span>
|
|
<span className="aff-classique-quai-dep__clock-s">{ss}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 <GareVideoprotection />;
|
|
}
|
|
|
|
// AFFICHAGE 2 : Départ en cours (T - 2 min)
|
|
if (isDepartEnCours) {
|
|
return <DepartEnCoursClassique />;
|
|
}
|
|
|
|
// AFFICHAGE 3 : Alternance si aucun train n'est attribué à ce quai
|
|
if (!selectedHoraire && gareParam) {
|
|
if (idlePhase === 0) {
|
|
return <AttentifsEnsemble />;
|
|
}
|
|
if (idlePhase === 1) {
|
|
return <ClassiquesDeparts defilement={true} />;
|
|
}
|
|
if (idlePhase === 2) {
|
|
return <ClassiquesInformations />;
|
|
}
|
|
}
|
|
|
|
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 (
|
|
<div className="aff-classique-quai-dep__container">
|
|
{/* PANNEAU GAUCHE : BLANC (Conforme Image 1 & Image 2) */}
|
|
<div className={`aff-classique-quai-dep__left ${isTgv ? 'aff-classique-quai-dep__left--tgv' : ''}`}>
|
|
{/* Logo Couleur Dynamique */}
|
|
<div className="aff-classique-quai-dep__brand">
|
|
<img src={brand.logo} alt={brand.label} className="aff-classique-quai-dep__brand-logo" />
|
|
{brand.showVoyageursText && (
|
|
<div className="aff-classique-quai-dep__sncf-voyageurs">VOYAGEURS</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Heure et Statut */}
|
|
<div className="aff-classique-quai-dep__time-row">
|
|
{(isSuppressed || isDelayed) ? (
|
|
showStatusAlt ? (
|
|
<span className={`aff-classique-quai-dep__status-alt ${isSuppressed ? 'aff-classique-quai-dep__status--suppressed' : 'aff-classique-quai-dep__status--delayed'}`}>
|
|
{displayStatusText}
|
|
</span>
|
|
) : (
|
|
<span className="aff-classique-quai-dep__time">
|
|
{formattedTime || '--:--'}
|
|
</span>
|
|
)
|
|
) : (
|
|
<>
|
|
<span className="aff-classique-quai-dep__time">
|
|
{formattedTime || '--:--'}
|
|
</span>
|
|
<span className="aff-classique-quai-dep__status">
|
|
{displayStatusText}
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Destination Principale */}
|
|
<div className={`aff-classique-quai-dep__destination ${isDifferentDest ? 'aff-classique-quai-dep__destination--multi' : ''}`}>
|
|
{destinationName1 || 'Aucun départ prévu'}
|
|
</div>
|
|
|
|
{/* Type de Train & Numéro 1 */}
|
|
<div className="aff-classique-quai-dep__train-info">
|
|
{trainType1 && <span className="aff-classique-quai-dep__train-type">{trainType1}</span>}
|
|
{trainNumber1 && <span className="aff-classique-quai-dep__train-number">{trainNumber1}</span>}
|
|
</div>
|
|
|
|
{/* Destination 2 si multi-tranche / destinations différentes (ex: Nice-Ville ou Bruxelles Midi) */}
|
|
{isDifferentDest && (
|
|
<>
|
|
<div className="aff-classique-quai-dep__destination aff-classique-quai-dep__destination--multi" style={{ marginTop: '16px' }}>
|
|
{destinationName2}
|
|
</div>
|
|
<div className="aff-classique-quai-dep__train-info">
|
|
{trainType2 && <span className="aff-classique-quai-dep__train-type">{trainType2}</span>}
|
|
{trainNumber2 && <span className="aff-classique-quai-dep__train-number">{trainNumber2}</span>}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Si même destination mais 2 numéros couplés (ex: 2 TGV départ 16h17 vers Paris) */}
|
|
{!isDifferentDest && trainNumber2 && trainNumber2 !== trainNumber1 && (
|
|
<div className="aff-classique-quai-dep__train-info" style={{ marginTop: '6px' }}>
|
|
{trainType2 && <span className="aff-classique-quai-dep__train-type">{trainType2}</span>}
|
|
<span className="aff-classique-quai-dep__train-number">{trainNumber2}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Pictogrammes de services pour TGV (PMR, Vélo, Bar) */}
|
|
{isTgv && <TgvServiceIcons />}
|
|
|
|
{/* Filigrane Vertical "départ" géant (orienté bas vers haut) */}
|
|
<div className="aff-classique-quai-dep__watermark" aria-hidden="true">
|
|
<span>départ</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* PANNEAU DROIT : BLEU NUIT AVEC DESSERTE (Conforme Image 1 & Fourche Image 2) */}
|
|
<div className={`aff-classique-quai-dep__right ${isTgv ? 'aff-classique-quai-dep__right--tgv' : ''}`}>
|
|
<div className="aff-classique-quai-dep__stops-wrapper">
|
|
{isDifferentDest && commonStops.length > 0 ? (
|
|
/* AFFICHAGE FOURCHE / BIFURCATION (Conforme Image 2) */
|
|
<div className="aff-classique-quai-dep__fork-container">
|
|
{/* Tronc commun */}
|
|
<ul className="aff-classique-quai-dep__stops-list">
|
|
{commonStops.map((stopName, idx) => (
|
|
<li key={idx} className="aff-classique-quai-dep__stop-item">
|
|
<span className="aff-classique-quai-dep__bullet" />
|
|
<span className="aff-classique-quai-dep__stop-name">{stopName}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
{/* Ligne d'embranchement avec courbe */}
|
|
<div className="aff-classique-quai-dep__fork-split-row">
|
|
<div className="aff-classique-quai-dep__fork-curve-rail" />
|
|
<div className="aff-classique-quai-dep__fork-branch-right">
|
|
<div className="aff-classique-quai-dep__fork-coaches-label">voitures 11 à 18</div>
|
|
<div className="aff-classique-quai-dep__fork-terminus-box">
|
|
<span className="aff-classique-quai-dep__bullet" style={{ position: 'static', margin: 0 }} />
|
|
<span className="aff-classique-quai-dep__fork-terminus-name">
|
|
{branch2Stops[branch2Stops.length - 1] || destinationName2}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Branche principale continuant vers terminus 1 */}
|
|
{branch1Stops.length > 0 && (
|
|
<ul className="aff-classique-quai-dep__stops-list" style={{ marginTop: '8px' }}>
|
|
{branch1Stops.map((stopName, idx) => {
|
|
const isLast = idx === branch1Stops.length - 1;
|
|
return (
|
|
<li
|
|
key={idx}
|
|
className={`aff-classique-quai-dep__stop-item ${isLast ? 'aff-classique-quai-dep__stop-item--dest' : ''}`}
|
|
>
|
|
<span className="aff-classique-quai-dep__bullet" />
|
|
<span className="aff-classique-quai-dep__stop-name">{stopName}</span>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
) : (
|
|
/* AFFICHAGE STANDARD DESSERTE VERTICALE (Conforme Image 1) */
|
|
stopsList.length > 0 ? (
|
|
<ul className="aff-classique-quai-dep__stops-list">
|
|
{stopsList.map((stopName, idx) => {
|
|
const isLast = idx === stopsList.length - 1;
|
|
return (
|
|
<li
|
|
key={idx}
|
|
className={`aff-classique-quai-dep__stop-item ${isLast ? 'aff-classique-quai-dep__stop-item--dest' : ''}`}
|
|
>
|
|
<span className="aff-classique-quai-dep__bullet" />
|
|
<span className="aff-classique-quai-dep__stop-name">
|
|
{stopName}
|
|
</span>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
) : (
|
|
<div style={{ color: '#ffffff', fontSize: '38px', fontWeight: 600, padding: '40px' }}>
|
|
Veuillez consulter les écrans du hall de la gare.
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* BANDEAU BAS : TGV COMPOSITION OU BANDEAU STANDARD */}
|
|
{isTgv ? (
|
|
<TgvCompositionQuai
|
|
platform={quaiParam || horaire?.quai || horaire?.quai_reel || horaire?.voie || horaire?._displayPlatform}
|
|
destination={destinationName1}
|
|
destination2={isDifferentDest ? destinationName2 : destinationName1}
|
|
composition={horaire?.composition}
|
|
marqueeMessage={horaire?._status?.causeDetails || horaire?.cause_retard || (isSuppressed ? `TRAIN SUPPRIMÉ - MERCI DE VOTRE COMPRÉHENSION` : (isDelayed ? `TRAIN RETARDÉ DE ${delayMinutes} MIN - MERCI DE VOTRE COMPRÉHENSION` : ''))}
|
|
/>
|
|
) : (
|
|
<div className="aff-classique-quai-dep__footer">
|
|
<div className="aff-classique-quai-dep__footer-track">
|
|
{quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai || horaire?.voie || horaire?._displayPlatform ? `Voie ${cleanPlatform(horaire.quai || horaire.voie || horaire._displayPlatform).toUpperCase()}` : '')}
|
|
</div>
|
|
<div className="aff-classique-quai-dep__footer-clock">
|
|
<DigitalClock />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |