RELEASE V5.6.0 : Voici les modifications apportées :

-> REFONTE TOTALE DE TOUS LES AFFICHEURS CLASSIQUES + QUAIS + OPTION TGV

FONCTIONNALITÉS EN COURS DE DÉVELOPPEMENT :

-> ADAPTABILITÉ EN FONCTION DES SITUATIONS
This commit is contained in:
mathis committed 2026-08-31 17:32:58 +02:00
1 parent 9c6ac798f1
commit d8f8ceaa9f
18 files changed
+2324 -533

No files matched your search

@@ -6,9 +6,23 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { fetchAllRows } from '../../../lib/fetchAllRows';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi';
import { matchStationNameStrict } from '../../../services/gtfsStations';
import { getTrainLogoAndMeta } from '../../../utils/trainLogoHelper';
// Pictogramme Autocar SNCF / MOBIGO
const AutocarIcon = ({ size = '50px', color = '#ffffff', style = {} }) => (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill={color}
style={{ flexShrink: 0, ...style }}
>
<path d="M4 16c0 .88.39 1.67 1 2.22V20c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h8v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1.78c.61-.55 1-1.34 1-2.22V6c0-3.5-3.58-4-8-4s-8 .5-8 4v10zm3.5 1c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm1.5-6H6V6h12v5z" />
</svg>
);
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
@@ -212,12 +226,8 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
if (!gerr && gdata && gdata.length) {
const exactMatch = gdata.find(g => {
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
return n1 === n2;
});
const chosen = exactMatch || gdata[0];
const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
fetchedType = chosen ? chosen.type : null;
fetchedId = chosen ? chosen.id : null;
}
@@ -650,21 +660,20 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
const stopForStation = (stops || []).find(s => {
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
const ns = normalizeName(nm || '');
const target = normalizeName(targetStationName || '');
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
}) || null;
horaire._status = displayStatus;
horaire._displayTime = displayTime;
// utiliser quai en priorité
horaire._displayPlatform = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay)) || horaire.quai || horaire.quai_reel || horaire.platform || '';
let platformVal = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || horaire.quai || horaire.quai_reel || horaire.platform || horaire.voie || '';
if (!platformVal) {
platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam);
}
horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
return horaire;
});
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
useEffect(() => {
// Alternance toutes les 4 secondes (type+numéro ↔ statut)
const id = setInterval(() => setShowStatus(s => !s), 4000);
// Alternance toutes les 2 secondes (type+numéro ↔ statut / suppression)
const id = setInterval(() => setShowStatus(s => !s), 2000);
return () => clearInterval(id);
}, []);
@@ -705,20 +714,26 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
if (sncfErr) {
setError(sncfErr);
} else {
const formatted = (sncfData || []).map(t => ({
...t,
_displayTime: formatTimeNoSeconds(t.heure_depart),
_displayPlatform: t.quai || t.voie || '',
_depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart),
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
}));
setHoraires(formatted);
setAllHoraires(formatted);
const formatted = (sncfData || []).map(t => {
const depHM = (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart);
const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
return {
...t,
_displayTime: formatTimeNoSeconds(t.heure_depart),
_displayPlatform: plat,
_depHM: depHM,
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
};
});
const maxCount = defilementMode ? 20 : 10;
const limited = (formatted || []).slice(0, maxCount);
setHoraires(limited);
setAllHoraires(limited);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
@@ -788,15 +803,14 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
// Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus)
let displayHoraires = parsedHoraires.slice();
if (gareDisplay) {
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
displayHoraires = displayHoraires.filter(h => {
// Exclure si la gare ciblée est la gare d'arrivée finale
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
if (destName === normalized) return false;
const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
if (matchStationNameStrict(destName, gareDisplay)) return false;
// Inclure si gare de départ correspond
const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString());
if (departName === normalized) return true;
const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString();
if (matchStationNameStrict(departName, gareDisplay)) return true;
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max)
const stopsList = getStops(h);
@@ -809,11 +823,11 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
}
}
for (const s of stopsList) {
const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString();
if (!stopName) continue;
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
if (isTerminus) continue;
if (stopName === normalized) return true;
if (matchStationNameStrict(stopName, gareDisplay)) return true;
}
}
return false;
@@ -886,7 +900,7 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
const stopsListForH = getStops(h);
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null;
const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null;
const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null;
const stop = byId || byName;
if (stop && (stop.heure_depart || stop.time || stop.heure)) raw = stop.heure_depart || stop.time || stop.heure;
}
@@ -1162,10 +1176,13 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
// Création des lignes vides pour créer l'espace
const emptyRows = Array.from({ length: 10 }).map((_, i) => ({ _isEmpty: true, id: `empty-${i}` }));
const maxRowsCount = defilementMode ? 20 : 10;
const limitedHoraires = (horaires || []).slice(0, maxRowsCount);
// Liste de lignes à rendre (horaires + espace + horaires + espace en mode défilement pour boucle infinie)
const rowsToRender = defilementMode
? [...horaires, ...emptyRows, ...horaires, ...emptyRows]
: horaires;
? [...limitedHoraires, ...emptyRows, ...limitedHoraires, ...emptyRows]
: limitedHoraires;
// Calcul dynamique de l'animation CSS
useEffect(() => {
@@ -1256,7 +1273,21 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
const isSuppressed = status && status.label === 'supprimé';
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
const showPlatform = !isSuppressed && Boolean(cleanPlatform);
const isCar = (
(horaire.type_train && (horaire.type_train.toUpperCase().includes('AUTOCAR') || horaire.type_train.toUpperCase().includes('CAR TER') || horaire.type_train.toUpperCase() === 'BUS')) ||
(horaire.op && (horaire.op.toUpperCase().includes('AUTOCAR') || horaire.op.toUpperCase().includes('CAR TER') || horaire.op.toUpperCase() === 'BUS')) ||
(horaire.type && (horaire.type.toUpperCase().includes('AUTOCAR') || horaire.type.toUpperCase().includes('CAR TER') || horaire.type.toUpperCase() === 'BUS')) ||
Boolean(horaire.is_substitution) ||
(numTrain && String(numTrain).startsWith('3') && String(numTrain).length >= 5) ||
typeLabel === 'AUTOCAR' ||
rawPlatform === 'AUTOCAR' ||
rawPlatform === 'CAR' ||
rawPlatform === 'BUS' ||
cleanPlatform === 'AUTOCAR' ||
cleanPlatform === 'CAR' ||
cleanPlatform === 'BUS'
);
const showPlatform = !isSuppressed && (isCar || Boolean(cleanPlatform));
const stopsList = isExtended ? getStopsListForDeparts(horaire, gareDisplay || gareParam, normalizeName) : [];
@@ -1277,7 +1308,10 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
isSuppressed ? (
<div className="aff-classiques-departs__status-suppressed">supprimé</div>
) : isDelayed ? (
<div className="aff-classiques-departs__status-delayed">retard {status.delayMinutes} min.</div>
<div className="aff-classiques-departs__status-delayed">
<div className="aff-classiques-departs__status-delayed-label">retardé</div>
<div className="aff-classiques-departs__status-delayed-time">+{status.delayMinutes || horaire.retard_minutes || 0} min.</div>
</div>
) : (
<div className="aff-classiques-departs__status-ontime">à l'heure</div>
)
@@ -1290,7 +1324,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
</div>
<div className="aff-classiques-departs__col-time">
<div className="aff-classiques-departs__time">{time}</div>
<div
className="aff-classiques-departs__time"
style={{ visibility: isSuppressed && showStatus ? 'hidden' : 'visible' }}
>
{time}
</div>
</div>
<div className="aff-classiques-departs__col-destination">
@@ -1300,7 +1339,11 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
<div className="aff-classiques-departs__col-platform">
{showPlatform ? (
<div className="aff-classiques-departs__platform-box">
<div className="aff-classiques-departs__platform-text">{cleanPlatform}</div>
{isCar ? (
<AutocarIcon size="48px" />
) : (
<div className="aff-classiques-departs__platform-text">{cleanPlatform}</div>
)}
</div>
) : null}
</div>
@@ -1343,7 +1386,10 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
isSuppressed ? (
<div className="aff-classiques-departs__status-suppressed">supprimé</div>
) : isDelayed ? (
<div className="aff-classiques-departs__status-delayed">retard {status.delayMinutes} min.</div>
<div className="aff-classiques-departs__status-delayed">
<div className="aff-classiques-departs__status-delayed-label">retardé</div>
<div className="aff-classiques-departs__status-delayed-time">+{status.delayMinutes || horaire.retard_minutes || 0} min.</div>
</div>
) : (
<div className="aff-classiques-departs__status-ontime">à l'heure</div>
)
@@ -1357,7 +1403,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
{/* COL 3 : Heure (grande, jaune) */}
<div className="aff-classiques-departs__col-time">
<div className="aff-classiques-departs__time">{time}</div>
<div
className="aff-classiques-departs__time"
style={{ visibility: isSuppressed && showStatus ? 'hidden' : 'visible' }}
>
{time}
</div>
</div>
{/* COL 4 : Destination */}
@@ -1369,7 +1420,11 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
<div className="aff-classiques-departs__col-platform">
{showPlatform ? (
<div className="aff-classiques-departs__platform-box">
<div className="aff-classiques-departs__platform-text">{cleanPlatform}</div>
{isCar ? (
<AutocarIcon size="48px" />
) : (
<div className="aff-classiques-departs__platform-text">{cleanPlatform}</div>
)}
</div>
) : null}
</div>