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

-> Implémentation des afficheurs "Sécurité"; "Informations" et "Correspondances" pour les afficheurs AFL

FONCTIONNALITÉS EN COURS DE DÉVELOPPEMENT :

-> REFONTE DU DESIGN DES AFFICHEURS CLASSIQUES
This commit is contained in:
mathis committed 2026-08-30 21:16:38 +02:00
1 parent 1c83953552
commit 4f3a34b101
24 files changed
+3517 -412

No files matched your search

+173 -70
View File
@@ -7,6 +7,9 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
import AFLSecurityPage from './components/AFLSecurityPage';
import AFLCorrespondancesPage from './components/AFLCorrespondancesPage';
import AFLInformationsPage from './components/AFLInformationsPage';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
@@ -132,11 +135,35 @@ export default function AFLArrivees(){
// gérer les +, décoder et remplacer les tirets par des espaces
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
// Support pour ?page=1, ?page=2 pour verrouiller l'affichage sur une page spécifique
const pageParam = params.get('page');
const lockedPageIndex = pageParam !== null && !isNaN(parseInt(pageParam, 10))
? Math.max(0, parseInt(pageParam, 10) - 1)
: null;
// Support pour verrouiller l'affichage sur une page spécifique via l'URL (?page=...)
// ?page=1 -> Horaires Page 1
// ?page=2 -> Horaires Page 2
// ?page=3 | ?page=info | ?page=informations -> Informations
// ?page=4 | ?page=securite | ?page=securite1 -> Sécurité (Image 2)
// ?page=securite2 -> Sécurité (Image 3)
// ?page=5 | ?page=correspondances -> Correspondances
const rawPageParam = params.get('page') ? String(params.get('page')).toLowerCase().trim() : null;
const parseLockedStep = (param) => {
if (!param) return null;
if (param === '1' || param === 'p1' || param === 'horaires1' || param === 'horaires_1') return { type: 'HORAIRES_1' };
if (param === '2' || param === 'p2' || param === 'horaires2' || param === 'horaires_2') return { type: 'HORAIRES_2' };
if (param === '3' || param === 'info' || param === 'information' || param === 'informations' || param === 'infos') return { type: 'INFORMATIONS' };
if (param === '4' || param === 'securite' || param === 'security' || param === 'safety' || param === 'securite1' || param === 'securite_1') return { type: 'SECURITE', variant: 1 };
if (param === 'securite2' || param === 'securite_2' || param === 'vigipirate' || param === '3117') return { type: 'SECURITE', variant: 2 };
if (param === '5' || param === 'correspondance' || param === 'correspondances' || param === 'corresp') return { type: 'CORRESPONDANCES' };
const n = parseInt(param, 10);
if (!isNaN(n)) {
if (n === 1) return { type: 'HORAIRES_1' };
if (n === 2) return { type: 'HORAIRES_2' };
if (n === 3) return { type: 'INFORMATIONS' };
if (n === 4) return { type: 'SECURITE', variant: 1 };
if (n >= 5) return { type: 'CORRESPONDANCES' };
}
return null;
};
const lockedStep = parseLockedStep(rawPageParam);
// debug flag activable via ?debug=1 or ?debug=true
const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');
@@ -158,7 +185,7 @@ export default function AFLArrivees(){
const [perturbations, setPerturbations] = useState([]);
// eslint-disable-next-line no-unused-vars
const [stationType, setStationType] = useState(null);
const [, setTravaux] = useState([]);
const [travaux, setTravaux] = useState([]);
const [travauxSubstitutions, setTravauxSubstitutions] = useState([]);
const [, setLoading] = useState(false);
const [, setError] = useState(null);
@@ -685,7 +712,7 @@ export default function AFLArrivees(){
}
};
});
setHoraires(formatted.slice(0, 10));
setHoraires(formatted);
setAllHoraires(formatted);
}
} catch (e) {
@@ -861,30 +888,54 @@ export default function AFLArrivees(){
const tomorrowStr = localDateStr(tomorrow);
const circulatesOn = (h, d) => {
if (!h || !d) return false;
const pad = (n) => String(n).padStart(2, '0');
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
const targetDay = d.getDay();
if (Array.isArray(h.dates) && h.dates.length > 0) {
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
}
const customDaysRaw = h.jours_personnalises;
if (!customDaysRaw) return false;
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try {
customDays = JSON.parse(customDaysRaw);
} catch (e) {
return false;
}
if (customDaysRaw) {
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
}
if (Array.isArray(customDays) && customDays.length > 0) {
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
if (match) return Boolean(match.circule);
}
}
if (Array.isArray(customDays)) {
const match = customDays.find(item => item.date === targetDateStr);
if (match) {
return Boolean(match.circule);
}
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
const colName = `circule_${dayNames[targetDay]}`;
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
if (db && targetDateHyphen < db) return false;
if (df && targetDateHyphen > df) return false;
return Boolean(h[colName]);
}
return false;
if (Array.isArray(h.days) && h.days.length > 0) {
if (!h.days.includes(targetDay)) return false;
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
return true;
}
const singleDate = h.date_depart || h.date || h.service_date;
if (singleDate) {
const sStr = String(singleDate).slice(0, 10);
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
}
return true;
};
// Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_*
@@ -1011,6 +1062,10 @@ export default function AFLArrivees(){
let serviceDateStr;
if (todayCandidates && todayCandidates.length > 0) {
finalList = todayCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999));
finalList = [...finalList, ...sortedTomorrow];
}
serviceDateStr = todayStr;
} else {
finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
@@ -1086,7 +1141,10 @@ export default function AFLArrivees(){
// Helpers pour perturbations (memoized plus haut)
const [pageIndex, setPageIndex] = useState(lockedPageIndex !== null ? lockedPageIndex : 0);
// Gestion du cycle d'alternance des pages :
// Horaires P1 -> Horaires P2 (si > 5 trains) -> Informations -> Sécurité & Sûreté (alternance Img 2 / Img 3) -> Correspondances
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [securityVariant, setSecurityVariant] = useState(2);
const [alternateDelay, setAlternateDelay] = useState(false);
useEffect(() => {
@@ -1095,33 +1153,96 @@ export default function AFLArrivees(){
}, 2000);
return () => clearInterval(delayTimer);
}, []);
const getPageBounds = useCallback((pIndex) => {
if (pIndex === 0) return { start: 0, end: 5 };
const start = 5 + (pIndex - 1) * 8;
return { start, end: start + 8 };
}, []);
const totalPages = Math.max(1, horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8));
// Construction des étapes de rotation :
// Les pages Informations, Sécurité 1 et 2, Correspondances ne sont affichées STRICTEMENT que si demandées dans l'URL (?pages=... ou ?infos=1 etc.)
const pagesParam = params.get('pages');
const selectedPagesList = pagesParam ? pagesParam.split(',').map(s => s.trim().toLowerCase()) : null;
const hasFlag = (key) => params.get(key) === '1' || params.get(key) === 'true';
const enableP1 = selectedPagesList
? (selectedPagesList.includes('p1') || selectedPagesList.includes('horaires') || selectedPagesList.includes('horaires_1') || selectedPagesList.includes('page1'))
: (hasFlag('p1') || hasFlag('horaires') || !pagesParam);
const enableP2 = selectedPagesList
? (selectedPagesList.includes('p2') || selectedPagesList.includes('horaires') || selectedPagesList.includes('horaires_2') || selectedPagesList.includes('page2'))
: (hasFlag('p2') || hasFlag('horaires') || !pagesParam);
const enableInfos = selectedPagesList
? (selectedPagesList.includes('infos') || selectedPagesList.includes('info') || selectedPagesList.includes('informations') || selectedPagesList.includes('information'))
: (hasFlag('infos') || hasFlag('info') || hasFlag('informations') || hasFlag('information'));
const enableSecurite1 = selectedPagesList
? (selectedPagesList.includes('securite1') || selectedPagesList.includes('securite_1') || (selectedPagesList.includes('securite') && !selectedPagesList.includes('securite2')))
: (hasFlag('securite1') || hasFlag('securite_1') || (hasFlag('securite') && !hasFlag('securite2')));
const enableSecurite2 = selectedPagesList
? (selectedPagesList.includes('securite2') || selectedPagesList.includes('securite_2') || selectedPagesList.includes('vigipirate') || selectedPagesList.includes('3117'))
: (hasFlag('securite2') || hasFlag('securite_2') || hasFlag('vigipirate') || hasFlag('3117'));
const enableCorresp = selectedPagesList
? (selectedPagesList.includes('corresp') || selectedPagesList.includes('correspondance') || selectedPagesList.includes('correspondances'))
: (hasFlag('corresp') || hasFlag('correspondance') || hasFlag('correspondances'));
const cycleSteps = [];
if (enableP1) cycleSteps.push({ type: 'HORAIRES_1' });
if (enableP2 && horaires.length > 5) cycleSteps.push({ type: 'HORAIRES_2' });
if (enableInfos) cycleSteps.push({ type: 'INFORMATIONS' });
if (enableSecurite1) cycleSteps.push({ type: 'SECURITE', variant: 1 });
if (enableSecurite2) cycleSteps.push({ type: 'SECURITE', variant: 2 });
if (enableCorresp) cycleSteps.push({ type: 'CORRESPONDANCES' });
if (cycleSteps.length === 0) {
cycleSteps.push({ type: 'HORAIRES_1' });
if (horaires.length > 5) cycleSteps.push({ type: 'HORAIRES_2' });
}
useEffect(() => {
if (lockedPageIndex !== null) {
setPageIndex(Math.min(lockedPageIndex, totalPages - 1));
return;
}
const pageInterval = setInterval(() => {
setPageIndex((prev) => {
const tot = horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8);
if (tot <= 1) return 0;
return (prev + 1) % tot;
});
}, 10000); // Change page every 10 seconds
return () => clearInterval(pageInterval);
}, [horaires.length, lockedPageIndex, totalPages]);
if (lockedStep !== null) return;
if (cycleSteps.length <= 1) return;
const { start, end } = getPageBounds(pageIndex);
const displayedHoraires = horaires.slice(start, end);
const isPage1 = pageIndex === 0;
const pageInterval = setInterval(() => {
setCurrentStepIndex((prev) => (prev + 1) % cycleSteps.length);
}, 10000); // 10 secondes par page
return () => clearInterval(pageInterval);
}, [cycleSteps.length, lockedStep]);
const activeStep = lockedStep || cycleSteps[currentStepIndex % cycleSteps.length] || { type: 'HORAIRES_1' };
const effectiveGareName = gareDisplay || gareParam || (horaires[0]?.gare_arrivee_nom) || (horaires[0]?.gare_arrivee) || (horaires[0]?.nom_gare) || (horaires[0]?.station_name) || '';
if (activeStep.type === 'INFORMATIONS') {
return (
<AFLInformationsPage
mode="arrivees"
gareName={effectiveGareName}
perturbations={perturbations}
travaux={travaux}
/>
);
}
if (activeStep.type === 'SECURITE') {
return (
<AFLSecurityPage
variant={activeStep.variant || securityVariant}
/>
);
}
if (activeStep.type === 'CORRESPONDANCES') {
return (
<AFLCorrespondancesPage
gareName={effectiveGareName}
/>
);
}
const isPage1 = activeStep.type === 'HORAIRES_1';
const displayedHoraires = isPage1 ? horaires.slice(0, 5) : horaires.slice(5, 13);
const totalPages = horaires.length <= 5 ? 1 : 2;
const pageIndex = isPage1 ? 0 : 1;
return (
<div className="afl-arr-container">
@@ -1178,26 +1299,8 @@ export default function AFLArrivees(){
''
).toString().trim();
const isCG = rawPlatform.toUpperCase() === 'CG' ||
rawPlatform.toUpperCase().includes('CG') ||
String(horaire.quai || '').toUpperCase().includes('CG') ||
String(horaire.quai_reel || '').toUpperCase().includes('CG') ||
String(horaire.quai_prevu || '').toUpperCase().includes('CG') ||
String(horaire.platform || '').toUpperCase().includes('CG');
const isCarTER = isCG ||
op.includes('CAR') ||
op.includes('AUTOCAR') ||
op.includes('BUS') ||
String(horaire.mode || '').toUpperCase().includes('BUS') ||
String(horaire.mode || '').toUpperCase().includes('CAR') ||
String(horaire.mode_transport || '').toUpperCase().includes('CAR') ||
String(horaire.mode_transport || '').toUpperCase().includes('BUS') ||
String(horaire.type_train || '').toUpperCase().includes('CAR') ||
String(horaire.type_train || '').toUpperCase().includes('BUS') ||
String(horaire.libelle_type_train || '').toUpperCase().includes('CAR') ||
String(horaire.libelle_type_train || '').toUpperCase().includes('BUS') ||
(travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure || '');
const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue';
@@ -1207,7 +1310,7 @@ export default function AFLArrivees(){
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
const isSuppressed = status && status.label && String(status.label).toLowerCase().includes('supprimé');
let showPlatform = !isSuppressed && Boolean(rawPlatform) && !isCG;
let showPlatform = !isSuppressed && Boolean(rawPlatform);
const isDelayed = status && status.label && String(status.label).toLowerCase().includes('retard');
const pillClass = isSuppressed ? 'afl-arr-pill--suppressed' : isDelayed ? 'afl-arr-pill--delayed' : 'afl-arr-pill--ontime';
+50 -22
View File
@@ -460,31 +460,55 @@ const AFLArriveesQuais = () => {
const tomorrowStr = localDateStr(tomorrow);
const circulatesOn = (h, d) => {
const pad = (n) => String(n).padStart(2, '0');
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
if (!h || !d) return false;
const pad = (n) => String(n).padStart(2, '0');
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
const targetDay = d.getDay();
const customDaysRaw = h.jours_personnalises;
if (!customDaysRaw) return false;
if (Array.isArray(h.dates) && h.dates.length > 0) {
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
}
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try {
customDays = JSON.parse(customDaysRaw);
} catch (e) {
return false;
}
}
const customDaysRaw = h.jours_personnalises;
if (customDaysRaw) {
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
}
if (Array.isArray(customDays) && customDays.length > 0) {
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
if (match) return Boolean(match.circule);
}
}
if (Array.isArray(customDays)) {
const match = customDays.find(item => item.date === targetDateStr);
if (match) {
return Boolean(match.circule);
}
}
return false;
};
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
const colName = `circule_${dayNames[targetDay]}`;
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
if (db && targetDateHyphen < db) return false;
if (df && targetDateHyphen > df) return false;
return Boolean(h[colName]);
}
if (Array.isArray(h.days) && h.days.length > 0) {
if (!h.days.includes(targetDay)) return false;
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
return true;
}
const singleDate = h.date_depart || h.date || h.service_date;
if (singleDate) {
const sStr = String(singleDate).slice(0, 10);
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
}
return true;
};
const parseDateOnly = (v) => {
if (!v) return null;
@@ -608,6 +632,10 @@ const AFLArriveesQuais = () => {
let serviceDateStr = '';
if (todayCandidates.length > 0) {
finalList = todayCandidates.slice().sort((a, b) => a._depHM - b._depHM);
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999));
finalList = [...finalList, ...sortedTomorrow];
}
serviceDateStr = todayStr;
} else {
finalList = tomorrowCandidates.slice().sort((a, b) => a._depHM - b._depHM);
+173 -70
View File
@@ -7,6 +7,9 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
import AFLSecurityPage from './components/AFLSecurityPage';
import AFLCorrespondancesPage from './components/AFLCorrespondancesPage';
import AFLInformationsPage from './components/AFLInformationsPage';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
@@ -135,11 +138,35 @@ export default function AFLDeparts(){
// gérer les +, décoder et remplacer les tirets par des espaces
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
// Support pour ?page=1, ?page=2 pour verrouiller l'affichage sur une page spécifique
const pageParam = params.get('page');
const lockedPageIndex = pageParam !== null && !isNaN(parseInt(pageParam, 10))
? Math.max(0, parseInt(pageParam, 10) - 1)
: null;
// Support pour verrouiller l'affichage sur une page spécifique via l'URL (?page=...)
// ?page=1 -> Horaires Page 1
// ?page=2 -> Horaires Page 2
// ?page=3 | ?page=info | ?page=informations -> Informations
// ?page=4 | ?page=securite | ?page=securite1 -> Sécurité (Image 2)
// ?page=securite2 -> Sécurité (Image 3)
// ?page=5 | ?page=correspondances -> Correspondances
const rawPageParam = params.get('page') ? String(params.get('page')).toLowerCase().trim() : null;
const parseLockedStep = (param) => {
if (!param) return null;
if (param === '1' || param === 'p1' || param === 'horaires1' || param === 'horaires_1') return { type: 'HORAIRES_1' };
if (param === '2' || param === 'p2' || param === 'horaires2' || param === 'horaires_2') return { type: 'HORAIRES_2' };
if (param === '3' || param === 'info' || param === 'information' || param === 'informations' || param === 'infos') return { type: 'INFORMATIONS' };
if (param === '4' || param === 'securite' || param === 'security' || param === 'safety' || param === 'securite1' || param === 'securite_1') return { type: 'SECURITE', variant: 1 };
if (param === 'securite2' || param === 'securite_2' || param === 'vigipirate' || param === '3117') return { type: 'SECURITE', variant: 2 };
if (param === '5' || param === 'correspondance' || param === 'correspondances' || param === 'corresp') return { type: 'CORRESPONDANCES' };
const n = parseInt(param, 10);
if (!isNaN(n)) {
if (n === 1) return { type: 'HORAIRES_1' };
if (n === 2) return { type: 'HORAIRES_2' };
if (n === 3) return { type: 'INFORMATIONS' };
if (n === 4) return { type: 'SECURITE', variant: 1 };
if (n >= 5) return { type: 'CORRESPONDANCES' };
}
return null;
};
const lockedStep = parseLockedStep(rawPageParam);
// debug flag activable via ?debug=1 or ?debug=true
const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');
@@ -164,7 +191,7 @@ export default function AFLDeparts(){
// type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)
// eslint-disable-next-line no-unused-vars
const [stationType, setStationType] = useState(null);
const [, setTravaux] = useState([]);
const [travaux, setTravaux] = useState([]);
const [travauxSubstitutions, setTravauxSubstitutions] = useState([]);
const [, setLoading] = useState(false);
const [, setError] = useState(null);
@@ -695,7 +722,7 @@ export default function AFLDeparts(){
causeDetails: t.cause_retard || null
}
}));
setHoraires(formatted.slice(0, 10));
setHoraires(formatted);
setAllHoraires(formatted);
}
} catch (e) {
@@ -886,30 +913,54 @@ export default function AFLDeparts(){
const tomorrowStr = localDateStr(tomorrow);
const circulatesOn = (h, d) => {
if (!h || !d) return false;
const pad = (n) => String(n).padStart(2, '0');
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
const targetDay = d.getDay();
if (Array.isArray(h.dates) && h.dates.length > 0) {
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
}
const customDaysRaw = h.jours_personnalises;
if (!customDaysRaw) return false;
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try {
customDays = JSON.parse(customDaysRaw);
} catch (e) {
return false;
}
if (customDaysRaw) {
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
}
if (Array.isArray(customDays) && customDays.length > 0) {
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
if (match) return Boolean(match.circule);
}
}
if (Array.isArray(customDays)) {
const match = customDays.find(item => item.date === targetDateStr);
if (match) {
return Boolean(match.circule);
}
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
const colName = `circule_${dayNames[targetDay]}`;
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
if (db && targetDateHyphen < db) return false;
if (df && targetDateHyphen > df) return false;
return Boolean(h[colName]);
}
return false;
if (Array.isArray(h.days) && h.days.length > 0) {
if (!h.days.includes(targetDay)) return false;
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
return true;
}
const singleDate = h.date_depart || h.date || h.service_date;
if (singleDate) {
const sStr = String(singleDate).slice(0, 10);
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
}
return true;
};
// Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_*
@@ -1036,6 +1087,10 @@ export default function AFLDeparts(){
let serviceDateStr;
if (todayCandidates && todayCandidates.length > 0) {
finalList = todayCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
finalList = [...finalList, ...sortedTomorrow];
}
serviceDateStr = todayStr;
} else {
finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
@@ -1111,7 +1166,10 @@ export default function AFLDeparts(){
// Helpers pour perturbations (memoized plus haut)
const [pageIndex, setPageIndex] = useState(lockedPageIndex !== null ? lockedPageIndex : 0);
// Gestion du cycle d'alternance des pages :
// Horaires P1 -> Horaires P2 (si > 5 trains) -> Informations -> Sécurité & Sûreté (alternance Img 2 / Img 3) -> Correspondances
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [securityVariant, setSecurityVariant] = useState(2);
const [alternateDelay, setAlternateDelay] = useState(false);
useEffect(() => {
@@ -1120,33 +1178,96 @@ export default function AFLDeparts(){
}, 2000);
return () => clearInterval(delayTimer);
}, []);
const getPageBounds = useCallback((pIndex) => {
if (pIndex === 0) return { start: 0, end: 5 };
const start = 5 + (pIndex - 1) * 8;
return { start, end: start + 8 };
}, []);
const totalPages = Math.max(1, horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8));
// Construction des étapes de rotation :
// Les pages Informations, Sécurité 1 et 2, Correspondances ne sont affichées STRICTEMENT que si demandées dans l'URL (?pages=... ou ?infos=1 etc.)
const pagesParam = params.get('pages');
const selectedPagesList = pagesParam ? pagesParam.split(',').map(s => s.trim().toLowerCase()) : null;
const hasFlag = (key) => params.get(key) === '1' || params.get(key) === 'true';
const enableP1 = selectedPagesList
? (selectedPagesList.includes('p1') || selectedPagesList.includes('horaires') || selectedPagesList.includes('horaires_1') || selectedPagesList.includes('page1'))
: (hasFlag('p1') || hasFlag('horaires') || !pagesParam);
const enableP2 = selectedPagesList
? (selectedPagesList.includes('p2') || selectedPagesList.includes('horaires') || selectedPagesList.includes('horaires_2') || selectedPagesList.includes('page2'))
: (hasFlag('p2') || hasFlag('horaires') || !pagesParam);
const enableInfos = selectedPagesList
? (selectedPagesList.includes('infos') || selectedPagesList.includes('info') || selectedPagesList.includes('informations') || selectedPagesList.includes('information'))
: (hasFlag('infos') || hasFlag('info') || hasFlag('informations') || hasFlag('information'));
const enableSecurite1 = selectedPagesList
? (selectedPagesList.includes('securite1') || selectedPagesList.includes('securite_1') || (selectedPagesList.includes('securite') && !selectedPagesList.includes('securite2')))
: (hasFlag('securite1') || hasFlag('securite_1') || (hasFlag('securite') && !hasFlag('securite2')));
const enableSecurite2 = selectedPagesList
? (selectedPagesList.includes('securite2') || selectedPagesList.includes('securite_2') || selectedPagesList.includes('vigipirate') || selectedPagesList.includes('3117'))
: (hasFlag('securite2') || hasFlag('securite_2') || hasFlag('vigipirate') || hasFlag('3117'));
const enableCorresp = selectedPagesList
? (selectedPagesList.includes('corresp') || selectedPagesList.includes('correspondance') || selectedPagesList.includes('correspondances'))
: (hasFlag('corresp') || hasFlag('correspondance') || hasFlag('correspondances'));
const cycleSteps = [];
if (enableP1) cycleSteps.push({ type: 'HORAIRES_1' });
if (enableP2 && horaires.length > 5) cycleSteps.push({ type: 'HORAIRES_2' });
if (enableInfos) cycleSteps.push({ type: 'INFORMATIONS' });
if (enableSecurite1) cycleSteps.push({ type: 'SECURITE', variant: 1 });
if (enableSecurite2) cycleSteps.push({ type: 'SECURITE', variant: 2 });
if (enableCorresp) cycleSteps.push({ type: 'CORRESPONDANCES' });
if (cycleSteps.length === 0) {
cycleSteps.push({ type: 'HORAIRES_1' });
if (horaires.length > 5) cycleSteps.push({ type: 'HORAIRES_2' });
}
useEffect(() => {
if (lockedPageIndex !== null) {
setPageIndex(Math.min(lockedPageIndex, totalPages - 1));
return;
}
const pageInterval = setInterval(() => {
setPageIndex((prev) => {
const tot = horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8);
if (tot <= 1) return 0;
return (prev + 1) % tot;
});
}, 10000); // Change page every 10 seconds
return () => clearInterval(pageInterval);
}, [horaires.length, lockedPageIndex, totalPages]);
if (lockedStep !== null) return;
if (cycleSteps.length <= 1) return;
const { start, end } = getPageBounds(pageIndex);
const displayedHoraires = horaires.slice(start, end);
const isPage1 = pageIndex === 0;
const pageInterval = setInterval(() => {
setCurrentStepIndex((prev) => (prev + 1) % cycleSteps.length);
}, 10000); // 10 secondes par page
return () => clearInterval(pageInterval);
}, [cycleSteps.length, lockedStep]);
const activeStep = lockedStep || cycleSteps[currentStepIndex % cycleSteps.length] || { type: 'HORAIRES_1' };
const effectiveGareName = gareDisplay || gareParam || (horaires[0]?.gare_depart_nom) || (horaires[0]?.gare_depart) || (horaires[0]?.nom_gare) || (horaires[0]?.station_name) || '';
if (activeStep.type === 'INFORMATIONS') {
return (
<AFLInformationsPage
mode="departs"
gareName={effectiveGareName}
perturbations={perturbations}
travaux={travaux}
/>
);
}
if (activeStep.type === 'SECURITE') {
return (
<AFLSecurityPage
variant={activeStep.variant || securityVariant}
/>
);
}
if (activeStep.type === 'CORRESPONDANCES') {
return (
<AFLCorrespondancesPage
gareName={effectiveGareName}
/>
);
}
const isPage1 = activeStep.type === 'HORAIRES_1';
const displayedHoraires = isPage1 ? horaires.slice(0, 5) : horaires.slice(5, 13);
const totalPages = horaires.length <= 5 ? 1 : 2;
const pageIndex = isPage1 ? 0 : 1;
return (
<div className="afl-container">
@@ -1203,26 +1324,8 @@ export default function AFLDeparts(){
''
).toString().trim();
const isCG = rawPlatform.toUpperCase() === 'CG' ||
rawPlatform.toUpperCase().includes('CG') ||
String(horaire.quai || '').toUpperCase().includes('CG') ||
String(horaire.quai_reel || '').toUpperCase().includes('CG') ||
String(horaire.quai_prevu || '').toUpperCase().includes('CG') ||
String(horaire.platform || '').toUpperCase().includes('CG');
const isCarTER = isCG ||
op.includes('CAR') ||
op.includes('AUTOCAR') ||
op.includes('BUS') ||
String(horaire.mode || '').toUpperCase().includes('BUS') ||
String(horaire.mode || '').toUpperCase().includes('CAR') ||
String(horaire.mode_transport || '').toUpperCase().includes('CAR') ||
String(horaire.mode_transport || '').toUpperCase().includes('BUS') ||
String(horaire.type_train || '').toUpperCase().includes('CAR') ||
String(horaire.type_train || '').toUpperCase().includes('BUS') ||
String(horaire.libelle_type_train || '').toUpperCase().includes('CAR') ||
String(horaire.libelle_type_train || '').toUpperCase().includes('BUS') ||
(travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
const destination = horaire.gare_arrivee_nom || '';
@@ -1232,7 +1335,7 @@ export default function AFLDeparts(){
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
const isSuppressed = status && status.label && String(status.label).toLowerCase().includes('supprimé');
let showPlatform = !isSuppressed && Boolean(rawPlatform) && !isCG;
let showPlatform = !isSuppressed && Boolean(rawPlatform);
const isDelayed = status && status.label && String(status.label).toLowerCase().includes('retard');
const pillClass = isSuppressed ? 'afl-pill--suppressed' : isDelayed ? 'afl-pill--delayed' : 'afl-pill--ontime';
+50 -22
View File
@@ -530,31 +530,55 @@ const AFLDepartsQuais = () => {
const tomorrowStr = localDateStr(tomorrow);
const circulatesOn = (h, d) => {
const pad = (n) => String(n).padStart(2, '0');
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
if (!h || !d) return false;
const pad = (n) => String(n).padStart(2, '0');
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
const targetDay = d.getDay();
const customDaysRaw = h.jours_personnalises;
if (!customDaysRaw) return false;
if (Array.isArray(h.dates) && h.dates.length > 0) {
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
}
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try {
customDays = JSON.parse(customDaysRaw);
} catch (e) {
return false;
}
}
const customDaysRaw = h.jours_personnalises;
if (customDaysRaw) {
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
}
if (Array.isArray(customDays) && customDays.length > 0) {
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
if (match) return Boolean(match.circule);
}
}
if (Array.isArray(customDays)) {
const match = customDays.find(item => item.date === targetDateStr);
if (match) {
return Boolean(match.circule);
}
}
return false;
};
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
const colName = `circule_${dayNames[targetDay]}`;
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
if (db && targetDateHyphen < db) return false;
if (df && targetDateHyphen > df) return false;
return Boolean(h[colName]);
}
if (Array.isArray(h.days) && h.days.length > 0) {
if (!h.days.includes(targetDay)) return false;
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
return true;
}
const singleDate = h.date_depart || h.date || h.service_date;
if (singleDate) {
const sStr = String(singleDate).slice(0, 10);
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
}
return true;
};
const parseDateOnly = (v) => {
if (!v) return null;
@@ -682,6 +706,10 @@ const AFLDepartsQuais = () => {
let serviceDateStr = '';
if (todayCandidates.length > 0) {
finalList = todayCandidates.slice().sort((a, b) => a._depHM - b._depHM);
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
finalList = [...finalList, ...sortedTomorrow];
}
serviceDateStr = todayStr;
} else {
finalList = tomorrowCandidates.slice().sort((a, b) => a._depHM - b._depHM);
@@ -0,0 +1,201 @@
/* AFL CORRESPONDANCES PAGE - DESIGN STRICT CONFORME A L'IMAGE 1 */
.afl-corresp-page {
font-family: 'Achemine', 'Segoe UI', Roboto, sans-serif !important;
background-color: #1f2125 !important;
color: #ffffff !important;
width: 100% !important;
height: 100% !important;
overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
user-select: none !important;
box-sizing: border-box !important;
}
.afl-corresp-page * {
box-sizing: border-box !important;
}
/* ==========================================================================
EN-TÊTE GRIS ARDOISE (108px)
========================================================================== */
.afl-corresp-header {
height: 108px !important;
min-height: 108px !important;
max-height: 108px !important;
background-color: #34373d !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding: 0 48px !important;
border-bottom: 2px solid #4a4e56 !important;
}
.afl-corresp-header-left {
display: flex !important;
align-items: center !important;
gap: 22px !important;
}
.afl-corresp-header-title {
font-size: 54px !important;
font-weight: 800 !important;
color: #ffffff !important;
letter-spacing: -0.5px !important;
line-height: 1 !important;
}
.afl-corresp-header-right {
display: flex !important;
align-items: center !important;
}
.afl-corresp-clock {
font-size: 56px !important;
font-weight: 800 !important;
color: #ffffff !important;
line-height: 1 !important;
letter-spacing: -0.96px !important;
}
/* ==========================================================================
LISTE DES LIGNES EN CORRESPONDANCE
========================================================================== */
.afl-corresp-list {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
padding: 0 !important;
margin: 0 !important;
background-color: #1f2125 !important;
}
.afl-corresp-group {
display: flex !important;
flex-direction: column !important;
width: 100% !important;
}
.afl-corresp-row {
height: 108px !important;
min-height: 108px !important;
max-height: 108px !important;
display: flex !important;
align-items: center !important;
width: 100% !important;
padding: 0 48px !important;
border-bottom: 1.5px solid #3c4048 !important;
}
/* Colonne 1 : Bus/Car/Tram + Numéro de ligne */
.afl-corresp-col-line {
display: flex !important;
align-items: center !important;
gap: 14px !important;
width: 180px !important;
min-width: 180px !important;
}
.afl-corresp-icon-box {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 58px !important;
height: 58px !important;
border: 2px solid rgba(255, 255, 255, 0.4) !important;
border-radius: 8px !important;
background: rgba(255, 255, 255, 0.05) !important;
flex-shrink: 0 !important;
}
.afl-corresp-line-badge {
background-color: #4a4e57 !important;
color: #ffffff !important;
font-size: 32px !important;
font-weight: 900 !important;
padding: 6px 14px !important;
border-radius: 8px !important;
line-height: 1 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3) !important;
min-width: 65px !important;
text-align: center !important;
}
.afl-corresp-line-spacer {
width: 180px !important;
min-width: 180px !important;
}
/* Colonne 2 : Arrêt - Destination */
.afl-corresp-col-dest {
flex: 1 !important;
overflow: hidden !important;
padding-right: 30px !important;
white-space: nowrap !important;
text-overflow: ellipsis !important;
}
.afl-corresp-dest-text {
font-size: 48px !important;
font-weight: 700 !important;
color: #ffffff !important;
letter-spacing: -0.5px !important;
line-height: 1 !important;
}
/* Colonne 3 : 2 Boîtes des minutes d'attente (Compatibles XXhXX min) */
.afl-corresp-col-times {
display: flex !important;
align-items: center !important;
gap: 20px !important;
flex-shrink: 0 !important;
}
.afl-corresp-time-box {
background-color: #d6dbe2 !important;
color: #111111 !important;
border-radius: 10px !important;
min-width: 155px !important;
height: 68px !important;
padding: 0 16px !important;
display: flex !important;
align-items: baseline !important;
justify-content: center !important;
gap: 4px !important;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.35) !important;
}
.afl-corresp-time-val {
font-size: 44px !important;
font-weight: 900 !important;
line-height: 1 !important;
letter-spacing: -1px !important;
}
.afl-corresp-time-unit {
font-size: 24px !important;
font-weight: 600 !important;
line-height: 1 !important;
opacity: 0.9 !important;
margin-left: 2px !important;
}
/* Message si liste vide */
.afl-corresp-empty {
flex: 1 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
color: #cccccc !important;
font-size: 32px !important;
font-weight: 500 !important;
padding: 40px !important;
text-align: center !important;
}
@@ -0,0 +1,214 @@
import React, { useState, useEffect } from 'react';
import './AFLCorrespondancesPage.css';
import { getCorrespondancesForStation, computeLiveWaitTimes, formatWaitTimeDisplay } from '../../../../services/correspondancesService';
// Horloge pour l'en-tête de correspondances
function CorrespondancesClock({ now }) {
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
return (
<div className="afl-corresp-clock" aria-hidden>
<span>{hh}:{mm}</span>
</div>
);
}
// Icône de Pôle d'échange / Gare Correspondances (bâtiment + trajet en pointillés)
const StationTransferIcon = ({ size = '52px', color = '#f5c518' }) => (
<svg
viewBox="0 0 48 48"
width={size}
height={size}
fill="none"
style={{ flexShrink: 0 }}
>
{/* Bâtiment gare / Pôle d'échange */}
<path
d="M6 22 L24 8 L42 22 V40 H6 Z"
stroke={color}
strokeWidth="3.5"
strokeLinejoin="round"
/>
<rect x="19" y="24" width="10" height="16" stroke={color} strokeWidth="3" />
<path d="M12 24 H15 M33 24 H36" stroke={color} strokeWidth="3.5" strokeLinecap="round" />
<path d="M12 32 H15 M33 32 H36" stroke={color} strokeWidth="3.5" strokeLinecap="round" />
{/* Trajet pointillé de correspondance */}
<path
d="M6 44 H42"
stroke={color}
strokeWidth="3"
strokeDasharray="4 4"
strokeLinecap="round"
/>
<circle cx="42" cy="44" r="3.5" fill={color} />
</svg>
);
// Icône de Transport Multimodal (Bus, Car, Tram)
const TransitCorrespIcon = ({ type = 'bus', size = '56px', color = '#ffffff' }) => (
<div className="afl-corresp-icon-box">
{type === 'tram' ? (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2l2 4h-4l2-4z" />
<rect x="4" y="6" width="16" height="13" rx="3" />
<line x1="4" y1="12" x2="20" y2="12" />
<circle cx="8" cy="16" r="1.5" fill={color} />
<circle cx="16" cy="16" r="1.5" fill={color} />
<line x1="8" y1="19" x2="8" y2="21" strokeWidth="2.4" />
<line x1="16" y1="19" x2="16" y2="21" strokeWidth="2.4" />
</svg>
) : type === 'car' ? (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="5" width="18" height="13" rx="3" />
<line x1="3" y1="11" x2="21" y2="11" />
<circle cx="7" cy="15" r="1.5" fill={color} />
<circle cx="17" cy="15" r="1.5" fill={color} />
<line x1="6" y1="18" x2="6" y2="20" strokeWidth="2.4" />
<line x1="18" y1="18" x2="18" y2="20" strokeWidth="2.4" />
<line x1="7" y1="8" x2="17" y2="8" strokeWidth="1.5" />
</svg>
) : (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="4" y="3" width="16" height="16" rx="3" />
<path d="M4 11h16" />
<circle cx="8" cy="15" r="1.5" fill={color} />
<circle cx="16" cy="15" r="1.5" fill={color} />
<path d="M6 19v2M18 19v2" strokeWidth="2.4" />
<path d="M9 6h6" strokeWidth="2" />
</svg>
)}
</div>
);
export default function AFLCorrespondancesPage({ gareName = '' }) {
const [correspondances, setCorrespondances] = useState([]);
const [, setLoading] = useState(true);
const [currentTime, setCurrentTime] = useState(new Date());
// Horloge en direct pour synchronisation exacte des décomptes
useEffect(() => {
const clockId = setInterval(() => setCurrentTime(new Date()), 1000);
return () => clearInterval(clockId);
}, []);
// Charger les correspondances aux alentours de la gare affichée
useEffect(() => {
let mounted = true;
async function loadData() {
try {
const list = await getCorrespondancesForStation(gareName);
if (mounted) {
setCorrespondances(list);
setLoading(false);
}
} catch (err) {
console.warn('[AFLCorrespondances] Erreur chargement correspondances:', err);
if (mounted) setLoading(false);
}
}
loadData();
// Actualiser les correspondances toutes les 30 secondes
const refreshTimer = setInterval(loadData, 30000);
return () => {
mounted = false;
clearInterval(refreshTimer);
};
}, [gareName]);
// Si aucune correspondance n'est disponible
if (!correspondances || correspondances.length === 0) {
return (
<div className="afl-corresp-page">
<header className="afl-corresp-header">
<div className="afl-corresp-header-left">
<StationTransferIcon />
<div className="afl-corresp-header-title">Correspondances</div>
</div>
<div className="afl-corresp-header-right">
<CorrespondancesClock now={currentTime} />
</div>
</header>
<div className="afl-corresp-empty">
Aucune correspondance directe actuellement signalée dans cette gare.
</div>
</div>
);
}
return (
<div className="afl-corresp-page">
{/* EN-TÊTE GRIS ARDOISE */}
<header className="afl-corresp-header">
<div className="afl-corresp-header-left">
<StationTransferIcon />
<div className="afl-corresp-header-title">Correspondances</div>
</div>
<div className="afl-corresp-header-right">
<CorrespondancesClock now={currentTime} />
</div>
</header>
{/* LISTE DES CORRESPONDANCES (Strictement conforme à l'Image 1) */}
<div className="afl-corresp-list">
{correspondances.map((cGroup) => (
<div key={cGroup.id} className="afl-corresp-group">
{cGroup.destinations.map((dest, dIdx) => {
const isFirstInGroup = dIdx === 0;
// Calcul en direct à la seconde près des 2 prochains départs (comme Image 1)
const times = computeLiveWaitTimes(dest, currentTime).slice(0, 2);
return (
<div key={dIdx} className="afl-corresp-row">
{/* COLONNE GAUCHE : Picto Transport et Badge Ligne UNIQUEMENT sur la 1ère destination du groupe */}
<div className="afl-corresp-col-line">
{isFirstInGroup ? (
<>
<TransitCorrespIcon type={cGroup.type} size="56px" />
<div
className="afl-corresp-line-badge"
style={{
backgroundColor: cGroup.color || '#4a4e57',
color: cGroup.textColor || '#ffffff'
}}
>
{cGroup.line}
</div>
</>
) : (
<div className="afl-corresp-line-spacer" />
)}
</div>
{/* COLONNE CENTRE : Arrêt - Destination */}
<div className="afl-corresp-col-dest">
<span className="afl-corresp-dest-text">{dest.name}</span>
</div>
{/* COLONNE DROITE : 2 Boîtes Minutes d'attente compatibles XXhXX min */}
<div className="afl-corresp-col-times">
{times.map((wait, wIdx) => {
const formatted = formatWaitTimeDisplay(wait);
return (
<div key={wIdx} className="afl-corresp-time-box">
<span className="afl-corresp-time-val">{formatted.val}</span>
{formatted.unit && <span className="afl-corresp-time-unit">{formatted.unit}</span>}
</div>
);
})}
</div>
</div>
);
})}
</div>
))}
</div>
</div>
);
}
@@ -0,0 +1,242 @@
/* AFL INFORMATIONS PAGE - DESIGN AUX COULEURS AFL */
.afl-info-page {
font-family: 'Achemine', 'Segoe UI', Roboto, sans-serif !important;
color: #ffffff !important;
width: 100% !important;
height: 100% !important;
overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
user-select: none !important;
box-sizing: border-box !important;
}
.afl-info-page * {
box-sizing: border-box !important;
}
/* Thème Départs (Bleu) */
.afl-info--departs {
background-color: #002d62 !important;
}
.afl-info--departs .afl-info-header {
background-color: #005fa9 !important;
border-bottom: 2px solid #ffffff !important;
}
/* Thème Arrivées (Vert) */
.afl-info--arrivees {
background-color: #173e31 !important;
}
.afl-info--arrivees .afl-info-header {
background-color: #337862 !important;
border-bottom: 2px solid #ffffff !important;
}
/* ==========================================================================
EN-TÊTE AFL (108px)
========================================================================== */
.afl-info-header {
height: 108px !important;
min-height: 108px !important;
max-height: 108px !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding: 0 48px !important;
}
.afl-info-header-left {
display: flex !important;
align-items: center !important;
gap: 22px !important;
}
.afl-info-header-title {
font-size: 54px !important;
font-weight: 800 !important;
color: #ffffff !important;
letter-spacing: -0.5px !important;
line-height: 1 !important;
}
.afl-info-header-right {
display: flex !important;
align-items: center !important;
}
.afl-info-clock {
font-size: 56px !important;
font-weight: 800 !important;
color: #ffffff !important;
line-height: 1 !important;
letter-spacing: -0.96px !important;
}
/* ==========================================================================
CONTENU PRINCIPAL
========================================================================== */
.afl-info-content {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
padding: 36px 60px !important;
overflow: hidden !important;
}
/* Grille des cartes en cas de perturbations ou travaux */
.afl-info-cards-container {
display: flex !important;
flex-direction: column !important;
gap: 24px !important;
height: 100% !important;
justify-content: center !important;
}
.afl-info-card {
background: rgba(255, 255, 255, 0.08) !important;
border-radius: 16px !important;
padding: 24px 32px !important;
border: 1.5px solid rgba(255, 255, 255, 0.2) !important;
backdrop-filter: blur(8px) !important;
}
.afl-info-card--perturbation {
border-left: 10px solid #f7a81b !important;
}
.afl-info-card--travaux {
border-left: 10px solid #3cd070 !important;
}
.afl-info-card-header {
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
margin-bottom: 16px !important;
}
.afl-info-badge {
display: flex !important;
align-items: center !important;
gap: 10px !important;
font-size: 26px !important;
font-weight: 800 !important;
text-transform: uppercase !important;
letter-spacing: 0.8px !important;
}
.afl-info-badge--alert {
color: #f7a81b !important;
}
.afl-info-badge--travaux {
color: #3cd070 !important;
}
.afl-info-station-name {
font-size: 32px !important;
font-weight: 700 !important;
color: #ffffff !important;
opacity: 0.9 !important;
}
.afl-info-card-body {
display: flex !important;
flex-direction: column !important;
gap: 14px !important;
}
.afl-info-perturb-item,
.afl-info-travaux-item {
display: flex !important;
flex-direction: column !important;
gap: 4px !important;
}
.afl-info-perturb-title,
.afl-info-travaux-title {
font-size: 38px !important;
font-weight: 700 !important;
color: #ffffff !important;
}
.afl-info-perturb-desc,
.afl-info-travaux-desc {
font-size: 26px !important;
font-weight: 400 !important;
color: #e0e6ed !important;
line-height: 1.3 !important;
}
/* ==========================================================================
AFFICHAGE NOMINAL (3 CARTES DE SERVICES & CONSEILS)
========================================================================== */
.afl-info-nominal-container {
display: flex !important;
flex-direction: column !important;
justify-content: space-evenly !important;
height: 100% !important;
gap: 18px !important;
}
.afl-info-nominal-card {
display: flex !important;
align-items: center !important;
gap: 36px !important;
background: rgba(255, 255, 255, 0.08) !important;
border: 1.5px solid rgba(255, 255, 255, 0.16) !important;
border-radius: 18px !important;
padding: 24px 36px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
}
.afl-info-nominal-icon-wrap {
width: 96px !important;
height: 96px !important;
min-width: 96px !important;
border-radius: 20px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25) !important;
}
.afl-info-nominal-icon--green {
background: linear-gradient(135deg, #1fa357, #13773d) !important;
}
.afl-info-nominal-icon--blue {
background: linear-gradient(135deg, #0088ff, #0055cc) !important;
}
.afl-info-nominal-icon--yellow {
background: linear-gradient(135deg, #e69d00, #b37400) !important;
}
.afl-info-nominal-text {
display: flex !important;
flex-direction: column !important;
gap: 6px !important;
}
.afl-info-nominal-title {
font-size: 40px !important;
font-weight: 800 !important;
color: #ffffff !important;
letter-spacing: -0.3px !important;
}
.afl-info-nominal-subtitle {
font-size: 27px !important;
font-weight: 400 !important;
color: #dbe4ef !important;
line-height: 1.25 !important;
}
@@ -0,0 +1,185 @@
import React, { useState, useEffect } from 'react';
import './AFLInformationsPage.css';
function InfoClock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id);
}, []);
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
return (
<div className="afl-info-clock" aria-hidden>
<span>{hh}:{mm}</span>
</div>
);
}
export default function AFLInformationsPage({
mode = 'departs',
gareName = '',
perturbations = [],
travaux = []
}) {
const isDeparts = mode === 'departs';
const themeClass = isDeparts ? 'afl-info--departs' : 'afl-info--arrivees';
// Filtrer les perturbations et travaux pertinents
const activePerturbs = (perturbations || []).filter(p => {
const t = (p.type_perturbation || p.type || '').toString().toLowerCase();
return Boolean(t);
});
const activeTravaux = (travaux || []).slice(0, 2);
const hasPerturbations = activePerturbs.length > 0;
const hasTravaux = activeTravaux.length > 0;
return (
<div className={`afl-info-page ${themeClass}`}>
{/* EN-TÊTE AUX COULEURS AFL */}
<header className="afl-info-header">
<div className="afl-info-header-left">
{/* Icône Information Blanche */}
<svg
viewBox="0 0 24 24"
width="52px"
height="52px"
fill="none"
stroke="#ffffff"
strokeWidth="2.4"
strokeLinecap="round"
strokeLinejoin="round"
style={{ flexShrink: 0 }}
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" strokeWidth="3.2" />
</svg>
<div className="afl-info-header-title">Informations</div>
</div>
<div className="afl-info-header-right">
<InfoClock />
</div>
</header>
{/* CONTENU PRINCIPAL DE LA PAGE INFORMATIONS */}
<div className="afl-info-content">
{hasPerturbations || hasTravaux ? (
<div className="afl-info-cards-container">
{/* CARTE PERTURBATION */}
{hasPerturbations && (
<div className="afl-info-card afl-info-card--perturbation">
<div className="afl-info-card-header">
<div className="afl-info-badge afl-info-badge--alert">
<svg viewBox="0 0 24 24" width="28px" height="28px" fill="none" stroke="currentColor" strokeWidth="2.5">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" strokeWidth="3" />
</svg>
<span>Information Trafic</span>
</div>
<div className="afl-info-station-name">{gareName || 'Votre Gare'}</div>
</div>
<div className="afl-info-card-body">
{activePerturbs.slice(0, 2).map((p, idx) => (
<div key={idx} className="afl-info-perturb-item">
<div className="afl-info-perturb-title">
{p.ligne_nom || p.ligne || p.type_perturbation || 'Perturbation en cours'}
</div>
<div className="afl-info-perturb-desc">
{p.message || p.description || p.detail || p.cause || 'Des retards ou adaptations de circulation peuvent intervenir sur vos trajets. Veuillez consulter les annonces en gare.'}
</div>
</div>
))}
</div>
</div>
)}
{/* CARTE TRAVAUX */}
{hasTravaux && (
<div className="afl-info-card afl-info-card--travaux">
<div className="afl-info-card-header">
<div className="afl-info-badge afl-info-badge--travaux">
<svg viewBox="0 0 24 24" width="28px" height="28px" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
</svg>
<span>Travaux de maintenance</span>
</div>
</div>
<div className="afl-info-card-body">
{activeTravaux.map((t, idx) => (
<div key={idx} className="afl-info-travaux-item">
<div className="afl-info-travaux-title">{t.nom || t.intitule || 'Modernisation du réseau'}</div>
<div className="afl-info-travaux-desc">
{t.description || t.motif || 'Des substitutions par autocar peuvent être mises en place pendant la période de travaux.'}
</div>
</div>
))}
</div>
</div>
)}
</div>
) : (
/* ÉCRAN NOMINAL : INFORMATIONS VOYAGEURS & TRAFIC FLUIDE */
<div className="afl-info-nominal-container">
{/* CARTE 1 : STATUT TRAFIC NORMAL */}
<div className="afl-info-nominal-card">
<div className="afl-info-nominal-icon-wrap afl-info-nominal-icon--green">
<svg viewBox="0 0 24 24" width="68px" height="68px" fill="none" stroke="#ffffff" strokeWidth="2.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
</div>
<div className="afl-info-nominal-text">
<div className="afl-info-nominal-title">Trafic normal</div>
<div className="afl-info-nominal-subtitle">
La circulation des trains est fluide sur l'ensemble de votre ligne.
</div>
</div>
</div>
{/* CARTE 2 : CONSEILS VOYAGEURS ET APPLICATION */}
<div className="afl-info-nominal-card">
<div className="afl-info-nominal-icon-wrap afl-info-nominal-icon--blue">
<svg viewBox="0 0 24 24" width="68px" height="68px" fill="none" stroke="#ffffff" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
<rect x="5" y="2" width="14" height="20" rx="3" />
<line x1="12" y1="18" x2="12.01" y2="18" strokeWidth="3" />
</svg>
</div>
<div className="afl-info-nominal-text">
<div className="afl-info-nominal-title">SNCF Connect &amp; Services</div>
<div className="afl-info-nominal-subtitle">
Retrouvez vos horaires en direct, achetez vos billets et suivez votre train en temps réel sur l'application SNCF Connect.
</div>
</div>
</div>
{/* CARTE 3 : CONSEILS DE SÉCURITÉ EN GARE */}
<div className="afl-info-nominal-card">
<div className="afl-info-nominal-icon-wrap afl-info-nominal-icon--yellow">
<svg viewBox="0 0 24 24" width="68px" height="68px" fill="none" stroke="#ffffff" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 12V8H6a2 2 0 0 1-2-2c0-1.1.9-2 2-2h12v4" />
<path d="M4 6v12c0 1.1.9 2 2 2h14v-4" />
<path d="M18 12a2 2 0 0 0-2 2c0 1.1.9 2 2 2h4v-4h-4z" />
</svg>
</div>
<div className="afl-info-nominal-text">
<div className="afl-info-nominal-title">Étiquetage de vos bagages</div>
<div className="afl-info-nominal-subtitle">
Pensez à apposer une étiquette visible avec vos nom, prénom et numéro de téléphone sur tous vos bagages.
</div>
</div>
</div>
</div>
)}
</div>
</div>
);
}
@@ -0,0 +1,362 @@
/* AFL SECURITY & SAFETY PAGE - CONFORME STRICTEMENT AUX MAQUETTES IMAGES 2 ET 3 */
.afl-sec-page {
font-family: 'Achemine', 'Segoe UI', Roboto, sans-serif !important;
width: 100% !important;
height: 100% !important;
overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
user-select: none !important;
box-sizing: border-box !important;
}
.afl-sec-page * {
box-sizing: border-box !important;
}
/* ==========================================================================
EN-TÊTE BLANCHE COMMUNE (108px)
========================================================================== */
.afl-sec-header {
height: 108px !important;
min-height: 108px !important;
max-height: 108px !important;
background-color: #ffffff !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding: 0 48px !important;
border-bottom: 2px solid #e0e0e0 !important;
}
.afl-sec-header-left {
display: flex !important;
align-items: center !important;
gap: 20px !important;
}
.afl-sec-header-title {
font-size: 54px !important;
font-weight: 800 !important;
color: #111111 !important;
letter-spacing: -0.5px !important;
line-height: 1 !important;
}
.afl-sec-header-right {
display: flex !important;
align-items: center !important;
}
.afl-sec-clock {
font-size: 56px !important;
font-weight: 800 !important;
color: #111111 !important;
line-height: 1 !important;
letter-spacing: -0.96px !important;
}
/* ==========================================================================
CONTENU VARIANT 1 : ORANGE (Traversée des voies - Image 2)
========================================================================== */
.afl-sec-content--orange {
flex: 1 !important;
background-color: #d63e18 !important;
display: flex !important;
flex-direction: column !important;
overflow: hidden !important;
}
/* Ruban rayé sécurité orange / blanc */
.afl-sec-hazard-ribbon {
height: 38px !important;
min-height: 38px !important;
width: 100% !important;
background: repeating-linear-gradient(
135deg,
#ffffff 0px,
#ffffff 24px,
#d63e18 24px,
#d63e18 48px
) !important;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2) !important;
}
.afl-sec-orange-body {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
justify-content: space-evenly !important;
padding: 30px 60px 20px 60px !important;
}
.afl-sec-orange-row {
display: flex !important;
align-items: center !important;
gap: 55px !important;
}
.afl-sec-orange-pictos {
display: flex !important;
align-items: center !important;
gap: 20px !important;
flex-shrink: 0 !important;
min-width: 320px !important;
justify-content: center !important;
}
.afl-sec-picto-wrap {
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.afl-sec-orange-text {
flex: 1 !important;
color: #ffffff !important;
font-size: 52px !important;
font-weight: 700 !important;
line-height: 1.25 !important;
letter-spacing: -0.3px !important;
}
/* Bas de page avec Logo SNCF GARES & CONNEXIONS */
.afl-sec-orange-footer {
display: flex !important;
justify-content: center !important;
align-items: center !important;
padding-top: 10px !important;
}
.afl-sec-sncf-badge {
background-color: #ffffff !important;
border-radius: 6px !important;
padding: 6px 18px !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.25) !important;
}
.afl-sec-sncf-bold {
color: #002d62 !important;
font-size: 26px !important;
font-weight: 900 !important;
letter-spacing: 1px !important;
line-height: 1 !important;
}
.afl-sec-sncf-sub {
color: #d63e18 !important;
font-size: 11px !important;
font-weight: 800 !important;
letter-spacing: 0.8px !important;
margin-top: 2px !important;
line-height: 1 !important;
}
/* ==========================================================================
CONTENU VARIANT 2 : ANTHRACITE (Attentifs Ensemble / 3117 - Image 3)
========================================================================== */
.afl-sec-content--anthracite {
flex: 1 !important;
background-color: #383a3f !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
padding: 40px 60px !important;
}
.afl-sec-vigipirate-grid {
display: flex !important;
height: 100% !important;
align-items: center !important;
gap: 50px !important;
}
/* Colonne Gauche */
.afl-sec-left-col {
width: 38% !important;
display: flex !important;
flex-direction: column !important;
justify-content: space-between !important;
align-items: flex-start !important;
height: 100% !important;
padding: 10px 0 25px 0 !important;
}
.afl-sec-vigipirate-title {
color: #f7a81b !important;
font-size: 68px !important;
font-weight: 900 !important;
line-height: 1.08 !important;
letter-spacing: 1.2px !important;
text-transform: uppercase !important;
text-shadow: 0 3px 6px rgba(0, 0, 0, 0.4) !important;
margin-top: 10px !important;
}
.afl-sec-vigipirate-emblem {
margin-top: auto !important;
padding-bottom: 20px !important;
display: flex !important;
align-items: center !important;
justify-content: flex-start !important;
width: 100% !important;
}
.afl-sec-vigipirate-img {
width: 600px !important;
max-width: 200% !important;
height: auto !important;
max-height: 600px !important;
object-fit: contain !important;
filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.4)) !important;
}
/* Colonne Droite */
.afl-sec-right-col {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
height: 100% !important;
padding-left: 20px !important;
}
/* Bulle Alerte 3117 */
.afl-sec-speech-bubble-wrapper {
display: flex !important;
align-items: flex-start !important;
gap: 15px !important;
margin-bottom: 30px !important;
}
.afl-sec-speech-bubble {
position: relative !important;
background-color: #ffffff !important;
border: 5px solid #d9241c !important;
border-radius: 28px !important;
padding: 16px 36px !important;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3) !important;
}
.afl-sec-bubble-title {
color: #d9241c !important;
font-size: 34px !important;
font-weight: 900 !important;
letter-spacing: 0.5px !important;
margin-bottom: 6px !important;
}
.afl-sec-bubble-numbers {
display: flex !important;
flex-direction: column !important;
gap: 4px !important;
}
.afl-sec-bubble-row {
display: flex !important;
align-items: baseline !important;
gap: 14px !important;
}
.afl-sec-num-lbl {
color: #d9241c !important;
font-size: 38px !important;
font-weight: 700 !important;
}
.afl-sec-num-val {
color: #d9241c !important;
font-size: 58px !important;
font-weight: 900 !important;
letter-spacing: 0.5px !important;
line-height: 1 !important;
}
/* Pointe de la bulle vers le bas à gauche */
.afl-sec-bubble-tail {
position: absolute !important;
bottom: -22px !important;
left: 45px !important;
width: 0 !important;
height: 0 !important;
border-left: 20px solid transparent !important;
border-right: 12px solid transparent !important;
border-top: 22px solid #d9241c !important;
}
.afl-sec-bubble-tail::after {
content: '' !important;
position: absolute !important;
bottom: 5px !important;
left: -15px !important;
width: 0 !important;
height: 0 !important;
border-left: 15px solid transparent !important;
border-right: 9px solid transparent !important;
border-top: 17px solid #ffffff !important;
}
.afl-sec-phone-badge {
margin-top: -10px !important;
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.35)) !important;
}
/* Textes de consignes Vigipirate / 3117 */
.afl-sec-vigipirate-text-group {
display: flex !important;
flex-direction: column !important;
gap: 12px !important;
}
.afl-sec-consigne-intro {
color: #f0f0f0 !important;
font-size: 27px !important;
font-weight: 500 !important;
line-height: 1.3 !important;
margin: 0 !important;
}
.afl-sec-consigne-main {
color: #ffffff !important;
font-size: 34px !important;
font-weight: 800 !important;
line-height: 1.25 !important;
margin: 4px 0 0 0 !important;
}
.afl-sec-consigne-hours {
color: #ffffff !important;
font-size: 28px !important;
font-weight: 700 !important;
margin: 0 !important;
}
.afl-sec-consigne-app-title {
color: #ffffff !important;
font-size: 31px !important;
font-weight: 800 !important;
margin: 6px 0 0 0 !important;
}
.afl-sec-consigne-app-sub {
color: #e0e0e0 !important;
font-size: 26px !important;
font-weight: 500 !important;
margin: 0 !important;
}
.afl-sec-footnotes {
margin-top: 18px !important;
color: #b0b0b0 !important;
font-size: 19px !important;
font-weight: 400 !important;
line-height: 1.4 !important;
}
@@ -0,0 +1,262 @@
import React, { useState, useEffect } from 'react';
import './AFLSecurityPage.css';
import vigipirateImg from './vigipirate.png';
// Horloge haute fidélité pour l'en-tête de sécurité
function SecurityClock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id);
}, []);
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
return (
<div className="afl-sec-clock" aria-hidden>
<span>{hh}:{mm}</span>
</div>
);
}
export default function AFLSecurityPage({ variant = 2 }) {
// variant 1 = Traversée des voies (Image 2 - Fond Orange)
// variant 2 = Attentifs Ensemble / Alerte 3117 (Image 3 - Fond Anthracite) (Par défaut)
return (
<div className={`afl-sec-page afl-sec-page--variant-${variant}`}>
{/* EN-TÊTE BLANCHE COMMUNE */}
<header className="afl-sec-header">
<div className="afl-sec-header-left">
{/* Icône Triangle Danger Noir */}
<svg
viewBox="0 0 24 24"
width="52px"
height="52px"
fill="none"
stroke="#111111"
strokeWidth="2.6"
strokeLinecap="round"
strokeLinejoin="round"
style={{ flexShrink: 0 }}
>
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" strokeWidth="3.2" />
</svg>
<div className="afl-sec-header-title">Sécurité & Sûreté</div>
</div>
<div className="afl-sec-header-right">
<SecurityClock />
</div>
</header>
{/* CONTENU VARIANT 1 : Traversée des voies / Trains dans les deux sens (Image 2) */}
{variant === 1 && (
<div className="afl-sec-content afl-sec-content--orange">
{/* Ruban rayé sécurité orange / blanc */}
<div className="afl-sec-hazard-ribbon" />
<div className="afl-sec-orange-body">
{/* LIGNE 1 : Trains circulent dans les deux sens */}
<div className="afl-sec-orange-row">
<div className="afl-sec-orange-pictos">
{/* Pictogramme Danger Triangle Blanc */}
<div className="afl-sec-picto-wrap">
<svg viewBox="0 0 100 100" width="125px" height="125px">
<path
d="M50 12 L92 84 L8 84 Z"
fill="none"
stroke="#ffffff"
strokeWidth="8"
strokeLinejoin="round"
strokeLinecap="round"
/>
<path
d="M50 36 L50 60"
stroke="#ffffff"
strokeWidth="8"
strokeLinecap="round"
/>
<circle cx="50" cy="73" r="5" fill="#ffffff" />
</svg>
</div>
{/* Pictogramme Train et personne traversant */}
<div className="afl-sec-picto-wrap">
<svg viewBox="0 0 160 120" width="180px" height="125px" fill="none">
{/* Quai / Ligne de sol */}
<line x1="10" y1="88" x2="150" y2="88" stroke="#ffffff" strokeWidth="5" strokeLinecap="round" />
<line x1="20" y1="88" x2="15" y2="105" stroke="#ffffff" strokeWidth="4" />
<line x1="70" y1="88" x2="65" y2="105" stroke="#ffffff" strokeWidth="4" />
<line x1="120" y1="88" x2="115" y2="105" stroke="#ffffff" strokeWidth="4" />
{/* Silhouette de personne sautant/courant */}
<circle cx="55" cy="30" r="7" fill="#ffffff" />
<path d="M48 42 L62 42 L72 58 L62 60 L56 50 L52 70 L42 78 L45 84" stroke="#ffffff" strokeWidth="4.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M48 42 L40 54 L32 50" stroke="#ffffff" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round" />
{/* Face avant du train */}
<rect x="95" y="28" width="45" height="52" rx="12" fill="#ffffff" />
<rect x="101" y="35" width="33" height="18" rx="5" fill="#d63e18" />
{/* Feux */}
<circle cx="106" cy="67" r="3.5" fill="#d63e18" />
<circle cx="129" cy="67" r="3.5" fill="#d63e18" />
{/* Ondes / Rayons de vitesse */}
<path d="M145 35 L153 30 M146 45 L155 45 M145 55 L153 60" stroke="#ffffff" strokeWidth="3.5" strokeLinecap="round" />
</svg>
</div>
</div>
<div className="afl-sec-orange-text">
Dans votre gare, des trains circulent dans les deux sens.
</div>
</div>
{/* LIGNE 2 : Signalisation & regarder de chaque côté */}
<div className="afl-sec-orange-row">
<div className="afl-sec-orange-pictos">
{/* Pictogramme Interdiction piéton avec feu clignotant */}
<div className="afl-sec-picto-wrap">
<svg viewBox="0 0 100 100" width="125px" height="125px" fill="none">
{/* Cercle barré */}
<circle cx="45" cy="50" r="36" stroke="#ffffff" strokeWidth="6" />
<line x1="22" y1="27" x2="68" y2="73" stroke="#ffffff" strokeWidth="6" />
{/* Silhouette piéton */}
<circle cx="45" cy="35" r="4.5" fill="#ffffff" />
<path d="M45 42 L45 58 M45 48 L37 56 M45 48 L53 54 M45 58 L39 70 M45 58 L51 70" stroke="#ffffff" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round" />
{/* Feu avec rayons */}
<circle cx="82" cy="28" r="5" fill="#ffffff" />
<path d="M82 14 L82 8 M72 20 L66 16 M92 20 L98 16 M72 36 L66 40 M92 36 L98 40" stroke="#ffffff" strokeWidth="3" strokeLinecap="round" />
<line x1="82" y1="35" x2="82" y2="85" stroke="#ffffff" strokeWidth="3.5" />
</svg>
</div>
{/* Pictogramme Piéton vert sur platelage avec signalisation */}
<div className="afl-sec-picto-wrap">
<svg viewBox="0 0 120 100" width="145px" height="125px" fill="none">
{/* Platelage / voie de traversée */}
<line x1="10" y1="84" x2="110" y2="84" stroke="#ffffff" strokeWidth="5" strokeLinecap="round" />
<line x1="20" y1="84" x2="15" y2="98" stroke="#ffffff" strokeWidth="3.5" />
<line x1="60" y1="84" x2="55" y2="98" stroke="#ffffff" strokeWidth="3.5" />
<line x1="100" y1="84" x2="95" y2="98" stroke="#ffffff" strokeWidth="3.5" />
{/* Silhouette Piéton Vert */}
<circle cx="55" cy="28" r="6" fill="#3cd070" />
<path d="M55 36 L55 54 L44 68 M55 42 L65 52 M55 42 L45 48 M55 54 L65 72" stroke="#3cd070" strokeWidth="4.5" strokeLinecap="round" strokeLinejoin="round" />
{/* Mât de signalisation */}
<line x1="90" y1="20" x2="90" y2="84" stroke="#ffffff" strokeWidth="4" strokeLinecap="round" />
<rect x="85" y="16" width="10" height="22" rx="3" fill="#ffffff" />
</svg>
</div>
</div>
<div className="afl-sec-orange-text">
Avant de traverser, respectez la signalisation et regardez de chaque côté.
</div>
</div>
{/* Logo SNCF GARES & CONNEXIONS en bas de page */}
<div className="afl-sec-orange-footer">
<div className="afl-sec-sncf-badge">
<span className="afl-sec-sncf-bold">SNCF</span>
<span className="afl-sec-sncf-sub">GARES &amp; CONNEXIONS</span>
</div>
</div>
</div>
</div>
)}
{/* CONTENU VARIANT 2 : ATTENTIFS ENSEMBLE / Alerte 3117 (Image 3) */}
{variant === 2 && (
<div className="afl-sec-content afl-sec-content--anthracite">
<div className="afl-sec-vigipirate-grid">
{/* COLONNE GAUCHE : Titre et Logo Vigipirate */}
<div className="afl-sec-left-col">
<div className="afl-sec-vigipirate-title">
ATTENTIFS<br />ENSEMBLE
</div>
{/* Logo Officiel Vigipirate fourni */}
<div className="afl-sec-vigipirate-emblem">
<img
src={vigipirateImg}
alt="Vigipirate Urgence Attentat"
className="afl-sec-vigipirate-img"
/>
</div>
</div>
{/* COLONNE DROITE : Bulle Alerte 3117 et textes de consigne */}
<div className="afl-sec-right-col">
{/* Bulle d'alerte avec picto téléphone */}
<div className="afl-sec-speech-bubble-wrapper">
<div className="afl-sec-speech-bubble">
<div className="afl-sec-bubble-title">ALERTE SNCF</div>
<div className="afl-sec-bubble-numbers">
<div className="afl-sec-bubble-row">
<span className="afl-sec-num-lbl">TÉL.</span>
<span className="afl-sec-num-val">3117*</span>
</div>
<div className="afl-sec-bubble-row">
<span className="afl-sec-num-lbl">SMS</span>
<span className="afl-sec-num-val">31177**</span>
</div>
</div>
{/* Pointe de la bulle */}
<div className="afl-sec-bubble-tail" />
</div>
{/* Picto Téléphone Portable Rouge */}
<div className="afl-sec-phone-badge">
<svg viewBox="0 0 60 90" width="70px" height="100px" fill="none">
<rect x="5" y="5" width="50" height="80" rx="10" fill="#d9241c" stroke="#ffffff" strokeWidth="3.5" />
{/* Écran blanc avec point d'exclamation rouge */}
<rect x="12" y="14" width="36" height="42" rx="4" fill="#ffffff" />
<line x1="30" y1="22" x2="30" y2="40" stroke="#d9241c" strokeWidth="5" strokeLinecap="round" />
<circle cx="30" cy="49" r="3" fill="#d9241c" />
{/* Clavier / Touches */}
<circle cx="18" cy="64" r="2.2" fill="#ffffff" />
<circle cx="30" cy="64" r="2.2" fill="#ffffff" />
<circle cx="42" cy="64" r="2.2" fill="#ffffff" />
<circle cx="18" cy="72" r="2.2" fill="#ffffff" />
<circle cx="30" cy="72" r="2.2" fill="#ffffff" />
<circle cx="42" cy="72" r="2.2" fill="#ffffff" />
</svg>
</div>
</div>
{/* Texte des consignes */}
<div className="afl-sec-vigipirate-text-group">
<p className="afl-sec-consigne-intro">
Si vous êtes témoin d'une situation qui présente un risque pour votre sécurité ou celle des autres voyageurs,
</p>
<p className="afl-sec-consigne-main">
APPELEZ LE 3117* ou ENVOYEZ UN SMS AU 31177**
</p>
<p className="afl-sec-consigne-hours">24h/24 • 7j/7</p>
<p className="afl-sec-consigne-app-title">
Téléchargez l'application ALERTE 3117
</p>
<p className="afl-sec-consigne-app-sub">
gratuite sur App Store et Google Play.
</p>
<div className="afl-sec-footnotes">
<div>* Appel enregistré / Numéro gratuit</div>
<div>** SMS enregistré / prix SMS de votre opérateur</div>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

@@ -650,7 +650,7 @@ export default function ClassiquesArrivees(){
}
};
});
setHoraires(formatted.slice(0, defilementMode ? 20 : 10));
setHoraires(formatted);
setAllHoraires(formatted);
}
} catch (e) {
@@ -839,30 +839,54 @@ export default function ClassiquesArrivees(){
const tomorrowStr = localDateStr(tomorrow);
const circulatesOn = (h, d) => {
if (!h || !d) return false;
const pad = (n) => String(n).padStart(2, '0');
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
const targetDay = d.getDay();
if (Array.isArray(h.dates) && h.dates.length > 0) {
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
}
const customDaysRaw = h.jours_personnalises;
if (!customDaysRaw) return false;
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try {
customDays = JSON.parse(customDaysRaw);
} catch (e) {
return false;
}
if (customDaysRaw) {
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
}
if (Array.isArray(customDays) && customDays.length > 0) {
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
if (match) return Boolean(match.circule);
}
}
if (Array.isArray(customDays)) {
const match = customDays.find(item => item.date === targetDateStr);
if (match) {
return Boolean(match.circule);
}
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
const colName = `circule_${dayNames[targetDay]}`;
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
if (db && targetDateHyphen < db) return false;
if (df && targetDateHyphen > df) return false;
return Boolean(h[colName]);
}
return false;
if (Array.isArray(h.days) && h.days.length > 0) {
if (!h.days.includes(targetDay)) return false;
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
return true;
}
const singleDate = h.date_depart || h.date || h.service_date;
if (singleDate) {
const sStr = String(singleDate).slice(0, 10);
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
}
return true;
};
// Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_*
@@ -990,6 +1014,10 @@ export default function ClassiquesArrivees(){
let serviceDateStr;
if (todayCandidates && todayCandidates.length > 0) {
finalList = todayCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999));
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999));
finalList = [...finalList, ...sortedTomorrow];
}
serviceDateStr = todayStr;
} else {
finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999));
@@ -647,7 +647,7 @@ export default function ClassiquesDeparts(){
causeDetails: t.cause_retard || null
}
}));
setHoraires(formatted.slice(0, defilementMode ? 20 : 10));
setHoraires(formatted);
setAllHoraires(formatted);
}
} catch (e) {
@@ -836,30 +836,54 @@ export default function ClassiquesDeparts(){
const tomorrowStr = localDateStr(tomorrow);
const circulatesOn = (h, d) => {
if (!h || !d) return false;
const pad = (n) => String(n).padStart(2, '0');
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
const targetDay = d.getDay();
if (Array.isArray(h.dates) && h.dates.length > 0) {
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
}
const customDaysRaw = h.jours_personnalises;
if (!customDaysRaw) return false;
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try {
customDays = JSON.parse(customDaysRaw);
} catch (e) {
return false;
}
if (customDaysRaw) {
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
}
if (Array.isArray(customDays) && customDays.length > 0) {
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
if (match) return Boolean(match.circule);
}
}
if (Array.isArray(customDays)) {
const match = customDays.find(item => item.date === targetDateStr);
if (match) {
return Boolean(match.circule);
}
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
const colName = `circule_${dayNames[targetDay]}`;
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
if (db && targetDateHyphen < db) return false;
if (df && targetDateHyphen > df) return false;
return Boolean(h[colName]);
}
return false;
if (Array.isArray(h.days) && h.days.length > 0) {
if (!h.days.includes(targetDay)) return false;
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
return true;
}
const singleDate = h.date_depart || h.date || h.service_date;
if (singleDate) {
const sStr = String(singleDate).slice(0, 10);
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
}
return true;
};
// Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_*
@@ -987,6 +1011,10 @@ export default function ClassiquesDeparts(){
let serviceDateStr;
if (todayCandidates && todayCandidates.length > 0) {
finalList = todayCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
finalList = [...finalList, ...sortedTomorrow];
}
serviceDateStr = todayStr;
} else {
finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
@@ -80,30 +80,54 @@ export default function ClassiquesArriveesQuais() {
const [, setTravaux] = useState([]);
const circulatesOn = useCallback((h, d) => {
const pad = (n) => String(n).padStart(2, '0');
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const customDaysRaw = h.jours_personnalises;
if (!customDaysRaw) return false;
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try {
customDays = JSON.parse(customDaysRaw);
} catch (e) {
return false;
}
}
if (Array.isArray(customDays)) {
const match = customDays.find(item => item.date === targetDateStr);
if (match) {
return Boolean(match.circule);
}
}
return false;
if (!h || !d) return false;
const pad = (n) => String(n).padStart(2, '0');
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
const targetDay = d.getDay();
if (Array.isArray(h.dates) && h.dates.length > 0) {
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
}
const customDaysRaw = h.jours_personnalises;
if (customDaysRaw) {
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
}
if (Array.isArray(customDays) && customDays.length > 0) {
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
if (match) return Boolean(match.circule);
}
}
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
const colName = `circule_${dayNames[targetDay]}`;
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
if (db && targetDateHyphen < db) return false;
if (df && targetDateHyphen > df) return false;
return Boolean(h[colName]);
}
if (Array.isArray(h.days) && h.days.length > 0) {
if (!h.days.includes(targetDay)) return false;
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
return true;
}
const singleDate = h.date_depart || h.date || h.service_date;
if (singleDate) {
const sStr = String(singleDate).slice(0, 10);
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
}
return true;
}, []);
const normalizeName = useCallback((s) => {
@@ -80,30 +80,54 @@ export default function ClassiquesDepartsQuais() {
const [, setTravaux] = useState([]);
const circulatesOn = useCallback((h, d) => {
const pad = (n) => String(n).padStart(2, '0');
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const customDaysRaw = h.jours_personnalises;
if (!customDaysRaw) return false;
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try {
customDays = JSON.parse(customDaysRaw);
} catch (e) {
return false;
}
}
if (Array.isArray(customDays)) {
const match = customDays.find(item => item.date === targetDateStr);
if (match) {
return Boolean(match.circule);
}
}
return false;
if (!h || !d) return false;
const pad = (n) => String(n).padStart(2, '0');
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
const targetDay = d.getDay();
if (Array.isArray(h.dates) && h.dates.length > 0) {
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
}
const customDaysRaw = h.jours_personnalises;
if (customDaysRaw) {
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
}
if (Array.isArray(customDays) && customDays.length > 0) {
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
if (match) return Boolean(match.circule);
}
}
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
const colName = `circule_${dayNames[targetDay]}`;
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
if (db && targetDateHyphen < db) return false;
if (df && targetDateHyphen > df) return false;
return Boolean(h[colName]);
}
if (Array.isArray(h.days) && h.days.length > 0) {
if (!h.days.includes(targetDay)) return false;
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
return true;
}
const singleDate = h.date_depart || h.date || h.service_date;
if (singleDate) {
const sStr = String(singleDate).slice(0, 10);
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
}
return true;
}, []);
const normalizeName = useCallback((s) => {
+59 -39
View File
@@ -887,7 +887,7 @@ export default function EVAArrivees() {
}
};
});
setHoraires(formatted.slice(0, 10));
setHoraires(formatted);
setAllHoraires(formatted);
}
} catch (e) {
@@ -1032,21 +1032,55 @@ export default function EVAArrivees() {
const tomorrowStr = localDateStr(tomorrow);
const circulatesOn = (h, d) => {
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const customDaysRaw = h.jours_personnalises;
if (!customDaysRaw) return false;
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try { customDays = JSON.parse(customDaysRaw); } catch (e) { return false; }
}
if (Array.isArray(customDays)) {
const match = customDays.find(item => item.date === targetDateStr);
if (match) return Boolean(match.circule);
}
return false;
};
if (!h || !d) return false;
const pad = (n) => String(n).padStart(2, '0');
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
const targetDay = d.getDay();
if (Array.isArray(h.dates) && h.dates.length > 0) {
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
}
const customDaysRaw = h.jours_personnalises;
if (customDaysRaw) {
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
}
if (Array.isArray(customDays) && customDays.length > 0) {
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
if (match) return Boolean(match.circule);
}
}
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
const colName = `circule_${dayNames[targetDay]}`;
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
if (db && targetDateHyphen < db) return false;
if (df && targetDateHyphen > df) return false;
return Boolean(h[colName]);
}
if (Array.isArray(h.days) && h.days.length > 0) {
if (!h.days.includes(targetDay)) return false;
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
return true;
}
const singleDate = h.date_depart || h.date || h.service_date;
if (singleDate) {
const sStr = String(singleDate).slice(0, 10);
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
}
return true;
};
const travauxList = travauxData || [];
const travauxSubstList = travauxSubstData || [];
@@ -1149,6 +1183,10 @@ export default function EVAArrivees() {
let serviceDateStr;
if (todayCandidates && todayCandidates.length > 0) {
finalList = todayCandidates.slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999));
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999));
finalList = [...finalList, ...sortedTomorrow];
}
serviceDateStr = todayStr;
} else {
finalList = (tomorrowCandidates || []).slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999));
@@ -1314,28 +1352,10 @@ export default function EVAArrivees() {
''
).toString().trim();
const isCG = rawPlatform.toUpperCase() === 'CG' ||
rawPlatform.toUpperCase().includes('CG') ||
String(horaire.quai || '').toUpperCase().includes('CG') ||
String(horaire.quai_reel || '').toUpperCase().includes('CG') ||
String(horaire.quai_prevu || '').toUpperCase().includes('CG') ||
String(horaire.platform || '').toUpperCase().includes('CG');
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
const isCarTER = isCG ||
op.includes('CAR') ||
op.includes('AUTOCAR') ||
op.includes('BUS') ||
String(horaire.mode || '').toUpperCase().includes('BUS') ||
String(horaire.mode || '').toUpperCase().includes('CAR') ||
String(horaire.mode_transport || '').toUpperCase().includes('CAR') ||
String(horaire.mode_transport || '').toUpperCase().includes('BUS') ||
String(horaire.type_train || '').toUpperCase().includes('CAR') ||
String(horaire.type_train || '').toUpperCase().includes('BUS') ||
String(horaire.libelle_type_train || '').toUpperCase().includes('CAR') ||
String(horaire.libelle_type_train || '').toUpperCase().includes('BUS') ||
(travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
const time = horaire._displayTime || formatTimeWithColon(horaire.heure_arrivee || horaire.time || horaire.heure || '');
let time = horaire._displayTime || formatTimeWithColon(horaire.heure_arrivee || horaire.time || horaire.heure || '');
const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue';
const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null);
@@ -1409,11 +1429,11 @@ export default function EVAArrivees() {
{/* SECTION DROITE : MODE / VOIE & BLOC OPERATEUR/NUMERO */}
<div className="eva-arr-col-right">
{isCarTER ? (
{isSub || isCarTER ? (
<div className="eva-arr-bus-box">
<EVABusIcon className="eva-arr-bus-icon" />
</div>
) : (rawPlatform && !isCG) ? (
) : (rawPlatform && !isSuppressed) ? (
<div className="eva-arr-voie-box">
<span className="eva-arr-voie-label">Voie</span>
<span className="eva-arr-voie-val">{rawPlatform}</span>
+59 -40
View File
@@ -880,7 +880,7 @@ export default function EVADeparts() {
causeDetails: t.cause_retard || null
}
}));
setHoraires(formatted.slice(0, 10));
setHoraires(formatted);
setAllHoraires(formatted);
}
} catch (e) {
@@ -1045,21 +1045,55 @@ export default function EVADeparts() {
const tomorrowStr = localDateStr(tomorrow);
const circulatesOn = (h, d) => {
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const customDaysRaw = h.jours_personnalises;
if (!customDaysRaw) return false;
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try { customDays = JSON.parse(customDaysRaw); } catch (e) { return false; }
}
if (Array.isArray(customDays)) {
const match = customDays.find(item => item.date === targetDateStr);
if (match) return Boolean(match.circule);
}
return false;
};
if (!h || !d) return false;
const pad = (n) => String(n).padStart(2, '0');
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
const targetDay = d.getDay();
if (Array.isArray(h.dates) && h.dates.length > 0) {
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
}
const customDaysRaw = h.jours_personnalises;
if (customDaysRaw) {
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
}
if (Array.isArray(customDays) && customDays.length > 0) {
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
if (match) return Boolean(match.circule);
}
}
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
const colName = `circule_${dayNames[targetDay]}`;
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
if (db && targetDateHyphen < db) return false;
if (df && targetDateHyphen > df) return false;
return Boolean(h[colName]);
}
if (Array.isArray(h.days) && h.days.length > 0) {
if (!h.days.includes(targetDay)) return false;
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
return true;
}
const singleDate = h.date_depart || h.date || h.service_date;
if (singleDate) {
const sStr = String(singleDate).slice(0, 10);
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
}
return true;
};
const travauxList = travauxData || [];
const travauxSubstList = travauxSubstData || [];
@@ -1162,6 +1196,10 @@ export default function EVADeparts() {
let serviceDateStr;
if (todayCandidates && todayCandidates.length > 0) {
finalList = todayCandidates.slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999));
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
finalList = [...finalList, ...sortedTomorrow];
}
serviceDateStr = todayStr;
} else {
finalList = (tomorrowCandidates || []).slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999));
@@ -1313,7 +1351,6 @@ export default function EVADeparts() {
const horaire = h || {};
const op = (horaire.type_train || horaire.op || horaire.type || 'TER').toUpperCase();
const number = horaire.numero_train || horaire.number || horaire.numero || '';
const rawPlatform = (
horaire._displayPlatform ||
horaire.quai_reel ||
@@ -1325,28 +1362,10 @@ export default function EVADeparts() {
''
).toString().trim();
const isCG = rawPlatform.toUpperCase() === 'CG' ||
rawPlatform.toUpperCase().includes('CG') ||
String(horaire.quai || '').toUpperCase().includes('CG') ||
String(horaire.quai_reel || '').toUpperCase().includes('CG') ||
String(horaire.quai_prevu || '').toUpperCase().includes('CG') ||
String(horaire.platform || '').toUpperCase().includes('CG');
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
const isCarTER = isCG ||
op.includes('CAR') ||
op.includes('AUTOCAR') ||
op.includes('BUS') ||
String(horaire.mode || '').toUpperCase().includes('BUS') ||
String(horaire.mode || '').toUpperCase().includes('CAR') ||
String(horaire.mode_transport || '').toUpperCase().includes('CAR') ||
String(horaire.mode_transport || '').toUpperCase().includes('BUS') ||
String(horaire.type_train || '').toUpperCase().includes('CAR') ||
String(horaire.type_train || '').toUpperCase().includes('BUS') ||
String(horaire.libelle_type_train || '').toUpperCase().includes('CAR') ||
String(horaire.libelle_type_train || '').toUpperCase().includes('BUS') ||
(travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
const time = horaire._displayTime || formatTimeWithColon(horaire.heure_depart || horaire.time || horaire.heure || '');
let time = horaire._displayTime || formatTimeWithColon(horaire.heure_depart || horaire.time || horaire.heure || '');
const destination = horaire.gare_arrivee_nom || horaire.gare_arrivee || 'Destination inconnue';
const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null);
@@ -1424,11 +1443,11 @@ export default function EVADeparts() {
{/* SECTION DROITE : MODE / VOIE & BLOC OPERATEUR/NUMERO */}
<div className="eva-dep-col-right">
{isCarTER ? (
{isSub || isCarTER ? (
<div className="eva-dep-bus-box">
<EVABusIcon className="eva-dep-bus-icon" />
</div>
) : (rawPlatform && !isCG) ? (
) : (rawPlatform && !isSuppressed) ? (
<div className="eva-dep-voie-box">
<span className="eva-dep-voie-label">Voie</span>
<span className="eva-dep-voie-val">{rawPlatform}</span>