-
- {/* 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

;
- })()}
-
-
départ
- {/* affichage des états de chargement / erreur pour debug */}
- {horaireError ?
{horaireError}
: null}
- {loadingHoraire ?
Chargement...
: null}
- {debugEnabled ? (
-
-
Candidates: {candidatesCount}
- {selectedHoraire ?
{JSON.stringify(selectedHoraire, null, 2)} : null}
-
- ) : null}
- {/* logo du type en haut à droite */}
-
- {(() => {
- 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();
- return (
- <>
- {time}
- {status}
- >
- );
- })()}
-
-
- {(() => {
- const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
- const stop = selectedHoraire && selectedHoraire.stop ? selectedHoraire.stop : null;
- // For arrivals: show the origin of the train (gare_depart_*) as the main station label
- const destinationBig = horaire ? (horaire.gare_depart_nom || horaire.gare_depart || horaire.origin_name || '') : mock.destinationBig;
- const rawDestinationStation = stop ? ((typeof stop === 'string') ? stop : (stop.gare_nom || stop.nom || stop.name || stop.stop_name || '')) : (horaire ? (horaire.destination_station || '') : mock.destinationStation);
- return (
- <>
-
{destinationBig}
- >
- );
- })()}
-
-
{(() => {
- 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} {trainNumber}>);
- })()}
-
-
-
- {/* barre verticale de points (élément explicite pour fiabilité) */}
-
-
- {(() => {
- 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 || '';
- }
- }
- label = String(label || '').trim();
- return { label, original: stop, idx };
- }).filter(it => it.label);
-
- 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) {
- const target = selectedHoraire.stop;
- const targetName = (typeof target === 'string') ? normalizeName(target) : normalizeName((target.gare_nom || target.nom || target.name || target.stop_name || target.station || ''));
- 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; }
- }
- const name = (typeof s === 'string') ? normalizeName(s) : normalizeName((s && (s.gare_nom || s.nom || s.name || s.stop_name || s.station || '')) || '');
- if (name && targetName && name === targetName) { matchedIndex = i; break; }
- }
- }
- } catch (e) { /* ignore */ }
-
- // For arrivals: show the stops served BEFORE the display station (i.e. previous stops)
- // If we have a matchedIndex, keep only items with idx < matchedIndex.
- // Display stops in the reverse direction relative to the route: show the farthest previous stop first
- let subsequent = labeled.filter(it => (matchedIndex >= 0) ? (it.idx < matchedIndex) : true);
- // sort ascending so the oldest (farthest) previous stop appears first -> reverse of travel direction
- subsequent = subsequent.sort((a,b) => a.idx - b.idx);
- // Remove consecutive duplicate labels (case-insensitive) to avoid repeated identical station names
- if (subsequent.length > 1) {
- const collapsed = [];
- for (const it of subsequent) {
- const prev = collapsed.length ? String(collapsed[collapsed.length-1].label).trim().toLowerCase() : null;
- const cur = it.label ? String(it.label).trim().toLowerCase() : '';
- if (cur && prev && prev === cur) {
- // skip duplicate consecutive entry
- continue;
- }
- collapsed.push(it);
- }
- 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);
- return (
- -
-
- {item.label}
-
- );
- });
- })()}
-
-
- {/* footer band same as classic departures with clock on the right */}
-
+
+
Aucune Arrivée Prévue
+
+
);
-}
+ }
+ return (
+
+ {/* LEFT PANEL: Lilac/Light Grey Background */}
+
+ {/* SNCF Logo */}
+

+
+ {(() => {
+ const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
+ 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_arrivee || 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-arrivees-redesign__time--long-text';
+ if (isTimeSuppressed) timeClass += ' aff-quais-arrivees-redesign__time--suppressed';
+ if (isTimeDelayed) timeClass += ' aff-quais-arrivees-redesign__time--delayed';
+
+ let statusClass = '';
+ if (isSuppressed) statusClass += ' aff-quais-arrivees-redesign__status--suppressed';
+ if (isDelayed) statusClass += ' aff-quais-arrivees-redesign__status--delayed';
+
+ return (
+ <>
+ {timeText}
+ {statusText && {String(statusText).toLowerCase()}}
+ >
+ );
+ })()}
+
+
+
+ {(() => {
+ const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
+ return showPlatform ? (horaire ? (horaire.gare_depart_nom || horaire.gare_depart || horaire.origin_name || horaire.origin || '') : mock.destinationBig) : '--';
+ })()}
+
+
+
+ {(() => {
+ 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 ? (
+ <>
+ {trainType}
+ {trainNumber}
+ >
+ ) : '';
+ })()}
+
+
+ {/* Accessibility Icons */}
+
+
+ {/* Watermark */}
+
arrivée
+
+
+ {/* RIGHT PANEL: Green Background with Provenance Stops */}
+
+ {showPlatform &&
gares de provenance :
}
+
+ {(() => {
+ if (!showPlatform) return null;
+ const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
+ let stops = horaire ? getStops(horaire) : [];
+ if (!Array.isArray(stops) || stops.length === 0) {
+ 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 || [];
+
+ const labeled = stops.map((stop, idx) => {
+ let label = '';
+ if (typeof stop === 'string') label = stop;
+ else if (stop && typeof stop === 'object') {
+ 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)) || '';
+ if (!label && stop.stop && typeof stop.stop === 'object') {
+ label = stop.stop.name || stop.stop.stop_name || '';
+ }
+ }
+ label = String(label || '').trim();
+ return { label, original: stop, idx };
+ }).filter(it => it.label);
+
+ if (labeled.length === 0) return null;
+
+ let matchedIndex = -1;
+ try {
+ if (selectedHoraire && selectedHoraire.stop) {
+ const target = selectedHoraire.stop;
+ const targetName = (typeof target === 'string') ? normalizeName(target) : normalizeName((target.gare_nom || target.nom || target.name || target.stop_name || target.station || ''));
+ 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];
+ 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; }
+ }
+ const name = (typeof s === 'string') ? normalizeName(s) : normalizeName((s && (s.gare_nom || s.nom || s.name || s.stop_name || s.station || '')) || '');
+ if (name && targetName && name === targetName) { matchedIndex = i; break; }
+ }
+ }
+ } catch (e) { /* ignore */ }
+
+ let previousStops = labeled.filter(it => (matchedIndex >= 0) ? (it.idx <= matchedIndex) : true);
+ previousStops = previousStops.sort((a,b) => a.idx - b.idx);
+ if (previousStops.length > 1) {
+ const collapsed = [];
+ for (const it of previousStops) {
+ const prev = collapsed.length ? String(collapsed[collapsed.length-1].label).trim().toLowerCase() : null;
+ const cur = it.label ? String(it.label).trim().toLowerCase() : '';
+ if (cur && prev && prev === cur) {
+ continue;
+ }
+ collapsed.push(it);
+ }
+ previousStops = collapsed;
+ }
+ if (previousStops.length === 0) return null;
+
+ return previousStops.map((item, mapIndex) => {
+ 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-arrivees-redesign__stop-text--suppressed';
+ if (isStopDelayed) stopTextClass += ' aff-quais-arrivees-redesign__stop-text--delayed';
+
+ return (
+ -
+
+
+ {item.label}
+ {isStopSuppressed && (non desservi)}
+ {isStopDelayed && (+{stopDelayMin} min)}
+
+
+ );
+ });
+ })()}
+
+
+
+ {/* BOTTOM FOOTER: Full Width */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css
index 846a653..427f7b2 100644
--- a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css
+++ b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css
@@ -1,7 +1,3 @@
-/* Partie gauche strictement comme l'image */
-.achemine-font-face {
- /* helper placeholder (no output) */
-}
@font-face {
font-family: 'Achemine';
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
@@ -16,473 +12,333 @@
font-style: normal;
font-display: swap;
}
-.aff-quais__left--strict {
- width: 100%;
- max-width: 0;
- min-width: 700px;
- background: #fff;
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- justify-content: flex-start;
- /* Remonter l'ensemble du contenu vers le bas pour laisser la place au logo en haut */
- padding: 140px 32px 24px 32px;
- box-sizing: border-box;
- position: relative;
- /* leave space at bottom for the footer (height 86px) */
- height: calc(100vh - 86px);
- overflow: hidden;
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- box-shadow: none;
-}
-.aff-quais__left--strict,
-.aff-quais__left--strict * {
- font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
-}
-/* Apply blue color specifically for departures container only (avoid being overridden by arrivées stylesheet)
- Use :not(.aff-quais__container--arrivees) to scope to departures */
-.aff-quais__container--strict:not(.aff-quais__container--arrivees) .aff-quais__left--strict,
-.aff-quais__container--strict:not(.aff-quais__container--arrivees) .aff-quais__left--strict * {
- color: #0d4a87;
-}
-
-/* Strong override for departures container: ensure destination title isn't black/green due to other styles */
-.aff-quais__container--depart .aff-quais__left--strict,
-.aff-quais__container--depart .aff-quais__left--strict * {
- color: #0d4a87 !important;
-}
-.aff-quais__type-logo--strict{
- position: absolute;
- top: 18px;
- /* positionné à gauche dans la zone blanche (à droite du logo opérateur) */
- /* tout à gauche de la zone blanche */
- left: 32px;
- width: 120px;
- height: auto;
- object-fit: contain;
- z-index: 5;
- pointer-events: none;
-}
-.aff-quais__sncf-logo--strict {
- width: 86px;
- height: auto;
- margin-bottom: 18px;
- margin-left: 0;
- margin-top: 0;
- position: absolute;
- top: 18px;
- /* placé à droite du logo du type */
- left: 140px;
- z-index: 4;
-}
-.aff-quais__watermark {
- position: absolute;
- /* move watermark further to the right */
- right: -180px;
- top: 70%;
- transform: translateY(-50%) rotate(-90deg);
- /* police agrandie de 200% */
- font-size: 200px;
- color: rgba(23,55,113,0.07);
- font-weight: 800;
- letter-spacing: 6px;
- pointer-events: none;
- user-select: none;
- z-index: 0;
-}
-.aff-quais__row--time-status {
- display: flex;
- align-items: flex-end;
- gap: 18px;
- margin-bottom: 18px;
- z-index: 1;
-}
-.aff-quais__time--strict {
- color: #0d4a87;
- font-weight: 800;
- /* police agrandie de 200% */
- font-size: 112px;
- line-height: 1;
- letter-spacing: 0.5px;
- margin-right: 12px;
-}
-
-/* Wrapper that contains both the big time and the smaller status when alternating.
- Keeps the layout width/height stable so alternating does not reflow the surrounding text. */
-.aff-quais__time-status-wrapper {
- position: relative;
- display: inline-block; /* width is determined by the time element in normal flow */
- height: 112px; /* match .aff-quais__time font-size so status can be aligned at bottom */
- vertical-align: bottom;
- overflow: visible;
-}
-
-.aff-quais__time-status-wrapper .aff-quais__time--strict {
- position: relative; /* keep in flow to define wrapper width */
- display: block;
- top: 0;
- left: 0;
- transition: opacity 180ms linear;
-}
-
-.aff-quais__time-status-wrapper .aff-quais__time-status--strict {
- position: absolute; /* overlay on the same horizontal position */
- left: 0;
- right: 0;
- bottom: 0; /* align to bottom similar to previous align-self:flex-end */
- transition: opacity 180ms linear;
-}
-
-.aff-quais__time-status-wrapper .hidden { opacity: 0; pointer-events: none; }
-.aff-quais__time-status-wrapper .visible { opacity: 1; }
-.aff-quais__time-status--strict {
- color: #0d4a87;
- /* police agrandie de 200% */
- font-size: 44px;
- font-weight: 400;
- opacity: 1;
- align-self: flex-end;
- margin-bottom: 14px;
-}
-.aff-quais__destination--strict {
- margin-top: 10px;
- margin-bottom: 0;
- color: #0d4a87;
- z-index: 1;
-}
-.aff-quais__destination-line1--strict {
- /* police agrandie de 200% */
- font-size: 76px;
- font-weight: 800;
- line-height: 1.1;
-}
-.aff-quais__destination-line2--strict {
- /* police agrandie de 200% */
- font-size: 76px;
- font-weight: 800;
- line-height: 1.1;
-}
-.aff-quais__train-id--strict {
- margin-top: 12px;
- color: #0d4a87;
- font-weight: 700;
- /* police agrandie de 200% */
- font-size: 44px;
- letter-spacing: 0.5px;
- z-index: 1;
-}
-.aff-quais__train-number--strict {
- color: #0d4a87;
- font-weight: 400;
- /* police agrandie de 200% */
- font-size: 44px;
-}
-.aff-quais__quai{
- margin-top: 8px;
- color:#0d4a87;
- font-weight:800;
- background: rgba(23,55,113,0.08);
- padding:6px 10px;
- border-radius:6px;
- display:inline-block;
-}
-
-/* central divider */
-.aff-quais__divider{
- width:18px;
- background: linear-gradient(180deg, rgba(0,0,0,0.12), rgba(255, 255, 255, 0.18));
- display:flex;
- justify-content:center;
- align-items:flex-start;
- padding-top:36px;
-}
-.aff-quais__divider:before{
- content:'';
- width:6px;
- background: transparent;
- box-shadow: 0 18px 0 0 #f0e34f, 0 54px 0 0 #f0e34f, 0 90px 0 0 #f0e34f, 0 126px 0 0 #f0e34f, 0 162px 0 0 #f0e34f, 0 198px 0 0 #f0e34f;
- display:block;
- border-radius:4px;
-}
-
-/* Right panel with stops */
-.aff-quais__right{
- flex:1;
- background: linear-gradient(180deg,#205aa1 0%, #144b85 100%);
- display:flex;
- align-items:center;
- box-sizing:border-box;
- padding: 24px 36px;
-}
-.aff-quais__stops-box{
- width:100%;
- max-width:820px;
- margin-left:24px;
-}
-.aff-quais__stops-list{
- list-style:none;
- margin:0;
- padding:0;
- width:100%;
-}
-.aff-quais__stop{
- display:flex;
- align-items:center;
- gap:18px;
- padding: 10px 16px;
- color:#fff;
- font-size:30px;
- line-height:1.1;
-}
-.aff-quais__bullet{
- width:14px;
- height:14px;
- background: #bbd258; /* yellow bullet */
- border-radius:50%;
- display:inline-block;
- box-shadow: 0 0 0 4px rgba(240,227,79,0.06) inset;
-}
-.aff-quais__stop--active{
- background: rgba(155,123,224,0.15);
- border-radius:6px;
-}
-.aff-quais__stop--active .aff-quais__bullet{
- background:#9b7be0; /* purple for final */
-}
-.aff-quais__stop-text{
- flex:1;
- padding-left:6px;
-}
-
-/* --- STRICT STYLES FOR RIGHT PANEL --- */
-
-.aff-quais__right--strict {
- flex: 1;
- background: #205aa1; /* fond bleu uniforme */
- display: flex;
- align-items: flex-start;
- justify-content: flex-start;
- padding: 48px 0 48px 0;
- min-width: 0;
- /* leave space for footer */
- height: calc(100vh - 86px);
- box-sizing: border-box;
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
- box-shadow: none;
- position: relative; /* anchor for the stops rail */
-}
-
-.aff-quais__stops-list--strict {
- list-style: none;
+body, html {
margin: 0;
- /* variables pour contrôler la colonne de pastilles */
- --rail-gap: 72px; /* espacement vertical entre pastilles (ajuster si nécessaire) */
- --rail-offset: 24px; /* décalage vertical initial pour aligner la première pastille */
- --rail-left: 28px; /* position X du rail inside right panel */
- --rail-width: 12px; /* largeur du rail/pastille */
- --ul-padding: 56px; /* padding-left actuel de la liste */
- /* laisser un espace modéré à gauche pour la colonne de points */
- padding: 0 0 0 var(--ul-padding);
- width: 100%;
- max-width: 600px;
- position: relative;
+ padding: 0;
+ background-color: #0b376d;
+ overflow: hidden;
}
-/* élément rail explicite (utilisé pour afficher la colonne de pastilles) */
-.aff-quais__stops-rail{
- position: absolute;
- /* position relative to the right panel container */
- left: calc(var(--rail-left) - 8px);
- top: 0;
- bottom: 0;
- /* make rail clearly visible and wide enough */
- width: 30px;
- z-index: 2;
- pointer-events: none;
- /* base color: same as destination rectangle */
- background-color: #b6d2f7;
- overflow: visible;
-}
-
-.aff-quais__rail-dot{
- display: block;
- position: absolute;
- left: 50%;
- transform: translate(-50%, -50%);
- /* size is set dynamically from JS to match rail width */
- width: auto;
- height: auto;
- border-radius: 50%;
- background: #b9d25a;
- box-shadow: 0 0 0 4px rgba(255,230,102,0.06) inset;
-}
-
-.aff-quais__stop--strict {
- display: flex;
- align-items: center;
- /* agrandi de 150% */
- font-size: calc(2.1rem * 1.5);
- color: #fff;
- font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
- font-weight: 400;
- /* espacement augmenté entre les noms des gares */
- margin-bottom: 18px;
- line-height: 1.18;
- padding: 0 0 0 0;
- position: relative; /* each li is positioned so its ::before aligns with the rail */
-}
-
-.aff-quais__stop-bullet--strict {
- /* bullets individuels masqués : on utilise une barre verticale */
- display: none;
-}
-
-/* pastille positionnée pour chaque item, alignée sur la colonne (rail) */
-.aff-quais__stop--strict::before{
- /* disable per-item pseudo pastille — rail handles the dots */
- display: none;
-}
-
-/* barre verticale de points jaunes pour la liste des gares */
-/* removed ::before — using explicit .aff-quais__stops-rail element instead */
-
-.aff-quais__stop-text--strict{ /* ensure text doesn't overlap the rail */
- padding-left: 6px;
-}
-
-.aff-quais__status--alert{
- color: #ffe600;
- font-weight: 800;
-}
-
-.aff-quais__stop-text--strict {
- display: inline-block;
- font-size: inherit;
- font-weight: inherit;
- color: inherit;
-}
-
-.aff-quais__stop--strict-dest {
- font-weight: 800;
- background: #b6d2f7;
- color: #205aa1;
- border-radius: 6px;
- padding: 2px 32px 2px 0;
- margin-left: -45px;
- width: auto;
-
- padding-left: 45px;
-}
-
-/* Ensure the highlighted destination stop text is always white for readability */
-.aff-quais__stop--strict-dest .aff-quais__stop-text--strict {
- color: #ffffff !important;
-}
-
-.aff-quais__container--strict {
+.aff-quais-departs-redesign__container {
display: flex;
flex-direction: row;
width: 100vw;
height: 100vh;
- min-height: 0;
- min-width: 0;
- /* Palette locale pour l'afficheur départs : évite la palette arrivées importée ailleurs */
- --blue: #205aa1;
- --blue-dark: #144b85;
- --accent-yellow: #e1c14c;
- --white: #ffffff;
- --watermark-blue: rgba(23,55,113,0.07);
- background: var(--blue);
- box-sizing: border-box;
- overflow: hidden;
- /* make container positioned so the absolute footer can be placed relative to it */
position: relative;
+ font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
+ background-color: #0b376d;
+ color: #fff;
+ overflow: hidden;
}
-.aff-quais__separator--strict {
- width: 14px;
- min-width: 14px;
- max-width: 14px;
- background: #205aa1;
- /* align separator with the panels which leave room for footer */
- height: calc(100vh - 86px);
- display: block;
+.aff-quais-departs-redesign__empty {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+}
+
+.aff-quais-departs-redesign__empty-text {
+ font-size: 6rem;
+ font-weight: 700;
+ color: #ffffff;
+ text-align: center;
+}
+
+/* =========================================================
+ LEFT PANEL
+ ========================================================= */
+.aff-quais-departs-redesign__left {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 35%;
+ bottom: 80px;
+ background-color: #ffffff;
+ box-sizing: border-box;
+ padding: 30px 40px;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.aff-quais-departs-redesign__sncf-logo {
+ width: 90px;
+ height: auto;
+ margin-bottom: 25px;
z-index: 2;
}
-@media (max-width: 900px) {
- .aff-quais__right--strict {
- padding: 18px 0 18px 0;
- }
- /* moins d'espace en haut sur petits écrans */
- .aff-quais__left--strict { padding-top: 80px; }
- .aff-quais__stops-list--strict {
- padding-left: 12px;
- }
- .aff-quais__stop--strict {
- font-size: 1.1rem;
- }
- .aff-quais__stop-bullet--strict {
- width: 8px;
- height: 8px;
- margin-right: 8px;
- }
- .aff-quais__stop--strict-dest {
- padding: 1px 6px 1px 0;
- border-radius: 4px;
- }
+.aff-quais-departs-redesign__time-status {
+ display: flex;
+ align-items: baseline;
+ gap: 15px;
+ margin-bottom: 5px;
+ z-index: 2;
}
-/* --- END STRICT STYLES FOR RIGHT PANEL --- */
-
-.aff-quais__footer{
- position:absolute;
- left:0;
- right:0;
- bottom:0;
- height:86px;
- pointer-events:none;
-}
-.aff-quais__footer-inner{
- display:flex;
- justify-content:space-between;
- align-items:center;
- height:100%;
- padding: 0 28px;
- background: linear-gradient(to bottom, rgba(0,0,0,0.15), rgba(0,0,0,0.25));
-}
-.aff-quais__footer-left{
- color:#fff;
- font-weight:700;
- font-size:20px;
-}
-/* CLOCK BOX - unified */
-.aff-quais__clock-box{
- background: rgba(23,55,113,0.9);
- color:#fff;
- padding: 6px 12px;
- border-radius: 6px;
- border: 4px solid #ffffff;
-}
-.aff-quais__clock-main{
- font-size:18px;
- font-weight:800;
+.aff-quais-departs-redesign__time {
+ color: #0b376d;
+ font-weight: 700;
+ font-size: 7.5rem;
+ line-height: 1;
}
-/* position the clock bottom-right inside a small white rounded box like on the image */
-.aff-quais__footer-right{
- position:relative;
-}
-.aff-quais__clock{
- position:relative;
-}
-@media (max-width: 900px){
- .aff-quais__left{width:42%;}
- .aff-quais__destination-line1{font-size:36px}
- .aff-quais__destination-line2{font-size:28px}
- .aff-quais__time{font-size:48px}
+.aff-quais-departs-redesign__status {
+ color: #0b376d;
+ font-weight: 700;
+ font-size: 2.4rem;
+ line-height: 1;
+}
+
+.aff-quais-departs-redesign__destination {
+ color: #0b376d;
+ font-weight: 700;
+ font-size: 5.5rem; /* Slightly smaller to fit 2 lines if needed */
+ line-height: 1.1;
+ margin-bottom: 5px;
+ z-index: 2;
+ white-space: normal; /* Allow wrapping */
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-height: 13rem; /* Prevent overflowing panel */
+}
+
+.aff-quais-departs-redesign__train-info {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ z-index: 2;
+}
+
+.aff-quais-departs-redesign__train-type {
+ color: #0b376d;
+ font-weight: 700;
+ font-size: 2.2rem;
+ letter-spacing: 0.5px;
+}
+
+.aff-quais-departs-redesign__train-number {
+ color: #929bb0;
+ font-weight: 400;
+ font-size: 2.2rem;
+ letter-spacing: 0.5px;
+}
+
+.aff-quais-departs-redesign__icons {
+ position: absolute;
+ bottom: 20px;
+ left: 40px;
+ display: flex;
+ align-items: center;
+ z-index: 2;
+}
+
+.aff-quais-departs-redesign__watermark {
+ position: absolute;
+ right: 0;
+ top: 50%;
+ transform: translateY(-50%) rotate(-90deg);
+ transform-origin: right center;
+ font-size: 17rem; /* Massive size */
+ color: #e5e8ed;
+ font-weight: 700;
+ z-index: 1;
+ pointer-events: none;
+ user-select: none;
+ letter-spacing: 1.5rem;
+ line-height: 1;
+ white-space: nowrap;
+}
+
+/* =========================================================
+ RIGHT PANEL
+ ========================================================= */
+.aff-quais-departs-redesign__right {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 65%;
+ bottom: 80px;
+ background-color: #0b376d;
+ box-sizing: border-box;
+ padding: 30px 0 30px 0;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.aff-quais-departs-redesign__stops-list {
+ list-style: none;
+ margin: 0;
+ padding: 0 0 0 60px; /* Space from left edge */
+ width: 100%;
+ position: relative;
+ z-index: 2;
+ display: flex;
+ flex-direction: column;
+ gap: 8px; /* Closer spacing between stops */
+}
+
+.aff-quais-departs-redesign__stop {
+ display: flex;
+ align-items: center;
+ font-size: 3.4rem;
+ font-weight: 400;
+ color: #ffffff;
+ position: relative;
+ min-height: 42px;
+ padding-left: 50px; /* Space for rail and bullet */
+}
+
+/* Bullet styling */
+.aff-quais-departs-redesign__stop-bullet {
+ position: absolute;
+ left: 0; /* Centered on 24px rail: (24-16)/2 = 4 */
+ margin-left: 4px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 16px; /* Slightly larger bullet */
+ height: 16px;
+ background-color: #fbec22;
+ border-radius: 50%;
+ z-index: 4;
+}
+
+.aff-quais-departs-redesign__stop-text {
+ position: relative;
+ z-index: 4;
+ white-space: nowrap;
+ letter-spacing: 0.5px;
+ padding-left: 10px; /* Space from bullet */
+}
+
+/* The vertical rail segments drawn behind each bullet */
+.aff-quais-departs-redesign__stop::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ width: 24px; /* Much wider rail */
+ background-color: #5c538f;
+ top: -10px;
+ bottom: -10px;
+ z-index: 1;
+}
+
+/* Rail extends all the way to the top */
+.aff-quais-departs-redesign__stop:first-child::before {
+ top: -1000px;
+}
+
+/* Last stop has no rail continuing below */
+.aff-quais-departs-redesign__stop:last-child::before {
+ display: none;
+}
+
+/* Highlighted Destination Stop */
+.aff-quais-departs-redesign__stop--dest {
+ font-weight: 700;
+}
+
+.aff-quais-departs-redesign__stop--dest .aff-quais-departs-redesign__stop-text {
+ background-color: #5c538f;
+ padding: 4px 20px 4px 60px; /* Increased left padding to cover bullet */
+ margin-left: -60px; /* Pull back to exactly align with left:0 */
+ border-radius: 4px;
+ position: relative;
+ z-index: 3; /* Behind the bullet (z-index 4) */
+ display: inline-block;
+}
+
+/* =========================================================
+ FOOTER
+ ========================================================= */
+.aff-quais-departs-redesign__footer {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 80px;
+ background-color: #0b376d;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ box-sizing: border-box;
+ padding: 0 40px;
+ z-index: 10;
+}
+
+.aff-quais-departs-redesign__footer-left {
+ font-size: 3rem;
+ font-weight: 700;
+ color: #ffffff;
+ font-style: italic;
+ font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
+ letter-spacing: 0.5px;
+}
+
+.aff-quais-departs-redesign__footer-right {
+ display: flex;
+ align-items: center;
+}
+
+.aff-quais-departs-redesign__clock {
+ border: 2px solid #ffffff;
+ padding: 4px 14px;
+ font-size: 3rem;
+ font-weight: 700;
+ color: #ffffff;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
+}
+
+.aff-quais-departs-redesign__clock-s {
+ color: #f5a623;
+}
+
+.aff-quais-departs-redesign__time--long-text {
+ font-size: 3.5rem !important;
+ line-height: 1.2 !important;
+}
+
+.aff-quais-departs-redesign__time--suppressed,
+.aff-quais-departs-redesign__status--suppressed {
+ color: #ff4c5b !important;
+}
+
+.aff-quais-departs-redesign__time--delayed,
+.aff-quais-departs-redesign__status--delayed {
+ color: #ff9e43 !important;
+}
+
+/* Disrupted Stop styling */
+.aff-quais-departs-redesign__stop-text--suppressed {
+ text-decoration: line-through;
+ color: #ff4c5b;
+ opacity: 0.6;
+}
+
+.aff-quais-departs-redesign__stop-text--delayed {
+ color: #ff9e43;
+}
+
+.aff-quais-departs-redesign__stop-status-badge {
+ font-size: 2.2rem;
+ font-weight: bold;
+ margin-left: 10px;
+}
+
+.aff-quais-departs-redesign__stop-status-badge--suppressed {
+ color: #ff4c5b;
+}
+
+.aff-quais-departs-redesign__stop-status-badge--delayed {
+ color: #ff9e43;
}
diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js
index a2f8a83..214b762 100644
--- a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js
+++ b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js
@@ -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 (
-
-
{hh}:{mm}:{ss}
+
+ {hh}
+ {mm}
+ {ss}
);
}
-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 (
-
- --h--
-
- );
- }
-
- // Always render both time and status inside a fixed-size wrapper to avoid layout shift
+ if (!loadingHoraire && !selectedHoraire) {
return (
-
- {time}
- {label}
-
+
);
}
- return (
-
-
- {/* 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

;
- })()}
+ return (
+
+ {/* LEFT PANEL: White Background */}
+
+ {/* SNCF Logo */}
+

-
départ
- {/* affichage des états de chargement / erreur pour debug */}
- {horaireError ?
{horaireError}
: null}
- {loadingHoraire ?
Chargement...
: null}
- {debugEnabled ? (
-
-
Candidates: {candidatesCount}
- {selectedHoraire ?
{JSON.stringify(selectedHoraire, null, 2)} : null}
-
- ) : null}
- {/* logo du type en haut à droite */}
-
+
{(() => {
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 (
<>
- {time}
- {status}
+ {timeText}
+ {statusText && {String(statusText).toLowerCase()}}
>
);
})()}
-
+
+
{(() => {
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 (
- <>
-
{destinationBig}
-
- >
- );
+ return showPlatform ? (horaire ? (horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination || '') : mock.destinationBig) : '--';
})()}
-
{(() => {
- 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} {trainNumber}>);
- })()}
+
+
+ {(() => {
+ 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 ? (
+ <>
+ {trainType}
+ {trainNumber}
+ >
+ ) : '';
+ })()}
+
+
+ {/* Accessibility Icons */}
+
+
+ {/* Watermark */}
+
départ
-
-
- {/* barre verticale de points (élément explicite pour fiabilité) */}
-
-
+
+ {/* RIGHT PANEL: Blue Background with Stops */}
+
+
{(() => {
+ 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 (
- -
-
- {item.label}
+
-
+
+
+ {item.label}
+ {isStopSuppressed && (non desservi)}
+ {isStopDelayed && (+{stopDelayMin} min)}
+
);
});
})()}
- {/* footer band same as classic departures with clock on the right */}
-
-
-
-
+
+ {/* BOTTOM FOOTER: Full Width */}
+
);
-}
+}
\ No newline at end of file
diff --git a/test_date.js b/test_date.js
new file mode 100644
index 0000000..c9a9f3d
--- /dev/null
+++ b/test_date.js
@@ -0,0 +1,12 @@
+const horaire_date = "2026-06-14";
+const baseDate = new Date(horaire_date);
+console.log("baseDate before setHours:", baseDate.toString());
+const sched = 330; // 05:30
+baseDate.setHours(Math.floor(sched / 60), sched % 60, 0, 0);
+console.log("baseDate after setHours:", baseDate.toString());
+
+const currentTime = new Date("2026-06-14T04:49:15+02:00");
+console.log("currentTime:", currentTime.toString());
+
+const diffMinutes = (baseDate.getTime() - currentTime.getTime()) / 60000;
+console.log("diffMinutes:", diffMinutes);
diff --git a/test_db.js b/test_db.js
new file mode 100644
index 0000000..650d419
--- /dev/null
+++ b/test_db.js
@@ -0,0 +1,17 @@
+const { createClient } = require('@supabase/supabase-js');
+require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
+
+const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
+
+async function run() {
+ const gareDisplay = "Ouges";
+ const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
+ console.log("flexNeedle:", flexNeedle);
+ const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type').ilike('nom', flexNeedle).limit(1);
+ console.log("gdata:", gdata);
+ let fetchedType = null;
+ if (!gerr && gdata && gdata.length) fetchedType = gdata[0] && gdata[0].type;
+ console.log("fetchedType:", fetchedType);
+}
+
+run();
diff --git a/test_db2.js b/test_db2.js
new file mode 100644
index 0000000..e27b482
--- /dev/null
+++ b/test_db2.js
@@ -0,0 +1,10 @@
+const { createClient } = require('@supabase/supabase-js');
+require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
+const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
+
+async function run() {
+ const { data } = await supabase.from('gares').select('id,nom,type').ilike('nom', '%Ouges%');
+ console.log("Matched gares:", data);
+}
+
+run();
diff --git a/test_db3.js b/test_db3.js
new file mode 100644
index 0000000..7c89d5c
--- /dev/null
+++ b/test_db3.js
@@ -0,0 +1,12 @@
+const { createClient } = require('@supabase/supabase-js');
+require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
+const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
+
+async function run() {
+ const { data: horaires, error: hErr } = await supabase.from('horaires').select('*').limit(2);
+ console.log("Horaires sample:", horaires);
+ const { data: perturbs, error: pErr } = await supabase.from('perturbations').select('*').limit(2);
+ console.log("Perturbations sample:", perturbs);
+}
+
+run();
diff --git a/test_logic.js b/test_logic.js
new file mode 100644
index 0000000..340cde9
--- /dev/null
+++ b/test_logic.js
@@ -0,0 +1,85 @@
+const { createClient } = require('@supabase/supabase-js');
+require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
+const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
+
+const normalizeName = (s) => {
+ if (!s) return '';
+ try {
+ const t = String(s).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
+ return t.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ');
+ } catch (e) {
+ return String(s).toLowerCase().trim();
+ }
+};
+
+const parseTimeHM = (v) => {
+ if (!v) return null;
+ const m = String(v).match(/(\d{1,2}):(\d{2})/);
+ if (m) return parseInt(m[1], 10)*60 + parseInt(m[2], 10);
+ return null;
+};
+
+async function run() {
+ const gareDisplay = "Ouges";
+ const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
+ const { data: gdata } = await supabase.from('gares').select('id,type').ilike('nom', flexNeedle).limit(1);
+ const stationType = gdata && gdata.length ? gdata[0].type.toLowerCase() : null;
+ console.log("stationType:", stationType);
+
+ const thresholdForStationType = (typeGare) => {
+ const tG = String(typeGare || '').trim().toLowerCase();
+ if (tG === 'interurbaine' || tG.includes('interurb')) return 360;
+ if (tG === 'ville' || tG.includes('ville')) return 30;
+ return 30;
+ };
+ const threshold = thresholdForStationType(stationType);
+ console.log("threshold:", threshold);
+
+ const { data: allData } = await supabase.from('horaires').select('*').limit(200);
+ const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges"));
+ const h = horaires[0];
+
+ const stops = h.gares_desservies;
+ const targetStationName = gareDisplay;
+
+ const stopForStation = (stops || []).find(s => {
+ const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
+ const ns = normalizeName(nm || '');
+ const target = normalizeName(targetStationName || '');
+ return (targetStationName && (ns === target || ns.includes(target) || target.includes(ns)));
+ }) || null;
+
+ h._displayPlatform = (stopForStation && stopForStation.quai_reel) || h.quai_reel || '';
+ console.log("_displayPlatform:", h._displayPlatform);
+
+ const platform = (h._displayPlatform || '').toString().trim();
+ let showPlatform = Boolean(platform);
+ console.log("Boolean(platform):", showPlatform);
+
+ if (showPlatform) {
+ let diffMinutes = null;
+ h._depHM = parseTimeHM(h.heure_depart);
+ const sched = h._depHM;
+ console.log("sched:", sched);
+
+ const currentTime = new Date();
+ if (sched !== null) {
+ const baseDate = new Date();
+ baseDate.setHours(Math.floor(sched / 60), sched % 60, 0, 0);
+ const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
+ if (sched < 240 && nowHM > 1200) baseDate.setDate(baseDate.getDate() + 1);
+ else if (sched > 1200 && nowHM < 240) baseDate.setDate(baseDate.getDate() - 1);
+ diffMinutes = (baseDate.getTime() - currentTime.getTime()) / 60000;
+ }
+ console.log("diffMinutes:", diffMinutes);
+
+ if (diffMinutes !== null) {
+ showPlatform = (diffMinutes <= threshold);
+ } else {
+ showPlatform = false;
+ }
+ }
+ console.log("FINAL showPlatform:", showPlatform);
+}
+
+run();
diff --git a/test_ouges.js b/test_ouges.js
new file mode 100644
index 0000000..d3f531c
--- /dev/null
+++ b/test_ouges.js
@@ -0,0 +1,10 @@
+const { createClient } = require('@supabase/supabase-js');
+const supabaseUrl = 'https://supabase-ferrovia.mr-patator.fr';
+const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc1OTUxMjAxLCJleHAiOjE5MzM2MzEyMDF9.ZFuzZSHVOMX-poU1fhWTjZ2sn7LFz9NYATTwJknlSr0';
+const supabase = createClient(supabaseUrl, supabaseAnonKey);
+
+async function test() {
+ const { data: horaires, error } = await supabase.from('horaires').select('*').eq('numero_train', '891807');
+ console.log(horaires[0]);
+}
+test();
diff --git a/test_ouges2.js b/test_ouges2.js
new file mode 100644
index 0000000..5c500c4
--- /dev/null
+++ b/test_ouges2.js
@@ -0,0 +1,16 @@
+const { createClient } = require('@supabase/supabase-js');
+require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
+const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
+
+async function test() {
+ const { data: horaires } = await supabase.from('horaires').select('*').limit(100);
+ const matched = horaires.filter(h => JSON.stringify(h).includes("Ouges"));
+ const h = matched[0];
+ console.log("Train:", h.numero_train);
+ console.log("Stops:", h.gares_desservies.map(s => ({
+ gare_nom: s.gare_nom,
+ quai_reel: s.quai_reel,
+ gare_id: s.gare_id
+ })));
+}
+test();
diff --git a/test_pert.js b/test_pert.js
new file mode 100644
index 0000000..bd9cb4e
--- /dev/null
+++ b/test_pert.js
@@ -0,0 +1,10 @@
+const { createClient } = require('@supabase/supabase-js');
+require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
+const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
+
+async function run() {
+ const { data } = await supabase.from('perturbations').select('*');
+ console.log("Perturbations:", data);
+}
+
+run();
diff --git a/test_quai.js b/test_quai.js
new file mode 100644
index 0000000..18b8ac6
--- /dev/null
+++ b/test_quai.js
@@ -0,0 +1,10 @@
+const { createClient } = require('@supabase/supabase-js');
+const supabaseUrl = 'https://supabase-ferrovia.mr-patator.fr';
+const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc1OTUxMjAxLCJleHAiOjE5MzM2MzEyMDF9.ZFuzZSHVOMX-poU1fhWTjZ2sn7LFz9NYATTwJknlSr0';
+const supabase = createClient(supabaseUrl, supabaseAnonKey);
+
+async function test() {
+ const { data: horaires, error } = await supabase.from('horaires_departs').select('*');
+ console.log('Error:', error);
+}
+test();
diff --git a/test_sun.js b/test_sun.js
new file mode 100644
index 0000000..1802965
--- /dev/null
+++ b/test_sun.js
@@ -0,0 +1,10 @@
+const { createClient } = require('@supabase/supabase-js');
+require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
+const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
+
+async function run() {
+ const { data } = await supabase.from('horaires').select('numero_train, heure_depart, circule_dimanche').ilike('gares_desservies', '%Ouges%');
+ console.log("Trains Ouges on Sunday:", data);
+}
+
+run();
diff --git a/test_sun2.js b/test_sun2.js
new file mode 100644
index 0000000..dda6f4e
--- /dev/null
+++ b/test_sun2.js
@@ -0,0 +1,10 @@
+const { createClient } = require('@supabase/supabase-js');
+require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
+const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
+
+async function run() {
+ const { data } = await supabase.from('horaires').select('numero_train, circule_dimanche').in('numero_train', ['891801', '891813']);
+ console.log("Trains Ouges on Sunday:", data);
+}
+
+run();
diff --git a/test_sun3.js b/test_sun3.js
new file mode 100644
index 0000000..99fcb2b
--- /dev/null
+++ b/test_sun3.js
@@ -0,0 +1,11 @@
+const { createClient } = require('@supabase/supabase-js');
+require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
+const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
+
+async function run() {
+ const { data: allData } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies, circule_dimanche').limit(200);
+ const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges") && h.circule_dimanche === true);
+ console.log("Trains Ouges on Sunday:", horaires.length);
+}
+
+run();
diff --git a/test_sun4.js b/test_sun4.js
new file mode 100644
index 0000000..49c4f7e
--- /dev/null
+++ b/test_sun4.js
@@ -0,0 +1,14 @@
+const { createClient } = require('@supabase/supabase-js');
+require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
+const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
+
+async function run() {
+ const { data: allData } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, heure_depart, gares_desservies, circule_dimanche').limit(200);
+ const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges") && h.circule_dimanche === true);
+ horaires.forEach(h => {
+ const stop = h.gares_desservies.find(s => s.gare_nom === 'Ouges');
+ console.log(`Train ${h.numero_train} departs Ouges at ${stop ? stop.heure_depart : h.heure_depart}`);
+ });
+}
+
+run();
diff --git a/test_sun5.js b/test_sun5.js
new file mode 100644
index 0000000..78bcc83
--- /dev/null
+++ b/test_sun5.js
@@ -0,0 +1,14 @@
+const { createClient } = require('@supabase/supabase-js');
+require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
+const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
+
+async function run() {
+ const { data: allData } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, heure_depart, gares_desservies, circule_dimanche').limit(200);
+ const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges") && h.circule_dimanche === true);
+ horaires.forEach(h => {
+ const stop = h.gares_desservies.find(s => s.gare_nom === 'Ouges');
+ console.log(`Train ${h.numero_train} departs Ouges at ${stop ? stop.heure_depart : h.heure_depart} with quai_reel: ${stop ? stop.quai_reel : 'none'}`);
+ });
+}
+
+run();