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

-> IMPLÉMENTATION DONNÉES GTFS / GTFS-RT / SIRI LTE SNCF
-> RESTRUCTURATION DE L'APP.

FONCTIONNALITÉS EN COURS DE DÉVELOPPEMENT :

-> REFONTE DU DESIGN DES AFFICHEURS CLASSIQUES
-> AMÉLIORATION DES
This commit is contained in:
mathis committed 2026-08-30 01:58:16 +02:00
1 parent 0e0d8c435f
commit 72de543dc6
18 files changed
+1550 -544

No files matched your search

@@ -6,21 +6,34 @@ import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { fetchAllRows } from '../../../lib/fetchAllRows';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0');
const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`;
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`;
if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h');
}
return String(v).slice(0,5).replace(':','h');
@@ -93,7 +106,7 @@ export default function ClassiquesArrivees(){
const [allHoraires, setAllHoraires] = useState([]);
const [perturbations, setPerturbations] = useState([]);
// type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)
const [stationType, setStationType] = useState(null);
const [, setStationType] = useState(null);
const [, setTravaux] = useState([]);
const [, setLoading] = useState(false);
const [, setError] = useState(null);
@@ -107,7 +120,15 @@ export default function ClassiquesArrivees(){
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
useEffect(() => {
let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('interurbaine');
setResolvedGareId(null);
}
return;
}
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
@@ -137,9 +158,7 @@ export default function ClassiquesArrivees(){
}
} catch (e) { /* ignore */ }
};
if (supabase) {
fetchStationType();
}
fetchStationType();
return () => { mounted = false; };
}, [gareIdParam, gareDisplay]);
@@ -263,29 +282,18 @@ export default function ClassiquesArrivees(){
// parse HH:mm or ISO into minutes since midnight (utilisable ailleurs dans le composant)
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes();
return null;
}, []);
// retourne le threshold (en minutes) pour afficher le quai selon le type de gare
const thresholdForStationType = useCallback((typeGare) => {
const tG = String(typeGare || '').trim().toLowerCase();
// Vérification du type de gare
if (tG === 'interurbaine' || tG.includes('interurb')) {
return 360; // 6 heures
}
if (tG === 'ville' || tG.includes('ville')) {
return 30; // 30 minutes
}
// par défaut, 30 minutes
return 30;
}, []);
const formatTimeFromHM = useCallback((totalMinutes) => {
if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return '';
@@ -604,18 +612,57 @@ export default function ClassiquesArrivees(){
return () => clearTimeout(timeoutId);
}, []);
// Récupération Supabase — maintenant avec polling pour synchroniser sans recharger la page
// Récupération des données (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu
useEffect(() => {
let mounted = true;
let inFlight = false; // empêche les fetchs concurrents
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => {
if (!mounted) return;
if (inFlight) return; // éviter les appels simultanés si le fetch précédent n'est pas terminé
if (inFlight) return;
inFlight = true;
if (source === 'sncf') {
setLoading(true);
setError(null);
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'arrivees'
});
if (!mounted) { inFlight = false; return; }
if (sncfErr) {
setError(sncfErr);
} else {
const formatted = (sncfData || []).map(t => {
const timeStr = t.heure_arrivee || t.heure_depart || t.time || '';
return {
...t,
_displayTime: formatTimeNoSeconds(timeStr),
_displayPlatform: t.quai || t.voie || '',
_arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr),
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
};
});
setHoraires(formatted.slice(0, defilementMode ? 20 : 10));
setAllHoraires(formatted);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
} finally {
if (mounted) setLoading(false);
inFlight = false;
}
return;
}
if (!supabase) {
// Supabase non initialisé — avertir et définir un message d'erreur pour l'UI
// eslint-disable-next-line no-console
console.warn('Supabase non initialisé — vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY');
setError('Supabase non initialisé — vérifiez les variables d\'environnement');
setLoading(false);
@@ -1013,7 +1060,7 @@ export default function ClassiquesArrivees(){
const intervalId = setInterval(fetchData, intervalMs);
return () => { mounted = false; clearInterval(intervalId); };
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
// Helpers pour perturbations (memoized plus haut)
@@ -1089,21 +1136,6 @@ export default function ClassiquesArrivees(){
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
let showPlatform = !isSuppressed && Boolean(platform);
if (showPlatform) {
const nowHM = currentTime.getHours()*60 + currentTime.getMinutes();
const sched = (typeof horaire._arrHM === 'number') ? horaire._arrHM : parseTimeHM(horaire.heure_arrivee || horaire.time || horaire.heure || horaire._rawTime || '');
const threshold = thresholdForStationType(stationType);
if (typeof sched === 'number') {
let diff = sched - nowHM;
// Gérer le passage à minuit
if (diff < -720) { // e.g., now 23:00, sched 01:00, diff is ~ -22 hours
diff += 1440; // treat as next day
}
showPlatform = (diff >= 0 && diff <= threshold);
} else {
showPlatform = false;
}
}
const isSub = isSubstitution(horaire);
const infoText = isSub
@@ -6,21 +6,34 @@ import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { fetchAllRows } from '../../../lib/fetchAllRows';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0');
const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`;
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`;
if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h');
}
return String(v).slice(0,5).replace(':','h');
@@ -93,7 +106,7 @@ export default function ClassiquesDeparts(){
const [allHoraires, setAllHoraires] = useState([]);
const [perturbations, setPerturbations] = useState([]);
// type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)
const [stationType, setStationType] = useState(null);
const [, setStationType] = useState(null);
const [, setTravaux] = useState([]);
const [, setLoading] = useState(false);
const [, setError] = useState(null);
@@ -107,7 +120,15 @@ export default function ClassiquesDeparts(){
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
useEffect(() => {
let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('interurbaine');
setResolvedGareId(null);
}
return;
}
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
@@ -137,9 +158,7 @@ export default function ClassiquesDeparts(){
}
} catch (e) { /* ignore */ }
};
if (supabase) {
fetchStationType();
}
fetchStationType();
return () => { mounted = false; };
}, [gareIdParam, gareDisplay]);
@@ -263,29 +282,18 @@ export default function ClassiquesDeparts(){
// parse HH:mm or ISO into minutes since midnight (utilisable ailleurs dans le composant)
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes();
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
}, []);
// retourne le threshold (en minutes) pour afficher le quai selon le type de gare
const thresholdForStationType = useCallback((typeGare) => {
const tG = String(typeGare || '').trim().toLowerCase();
// Vérification du type de gare
if (tG === 'interurbaine' || tG.includes('interurb')) {
return 360; // 6 heures
}
if (tG === 'ville' || tG.includes('ville')) {
return 30; // 30 minutes
}
// par défaut, 30 minutes
return 30;
}, []);
const formatTimeFromHM = useCallback((totalMinutes) => {
if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return '';
@@ -604,18 +612,54 @@ export default function ClassiquesDeparts(){
return () => clearTimeout(timeoutId);
}, []);
// Récupération Supabase — maintenant avec polling pour synchroniser sans recharger la page
// Récupération des données (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu
useEffect(() => {
let mounted = true;
let inFlight = false; // empêche les fetchs concurrents
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => {
if (!mounted) return;
if (inFlight) return; // éviter les appels simultanés si le fetch précédent n'est pas terminé
if (inFlight) return;
inFlight = true;
if (source === 'sncf') {
setLoading(true);
setError(null);
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'departs'
});
if (!mounted) { inFlight = false; return; }
if (sncfErr) {
setError(sncfErr);
} else {
const formatted = (sncfData || []).map(t => ({
...t,
_displayTime: formatTimeNoSeconds(t.heure_depart),
_displayPlatform: t.quai || t.voie || '',
_depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart),
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
}));
setHoraires(formatted.slice(0, defilementMode ? 20 : 10));
setAllHoraires(formatted);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
} finally {
if (mounted) setLoading(false);
inFlight = false;
}
return;
}
if (!supabase) {
// Supabase non initialisé — avertir et définir un message d'erreur pour l'UI
// eslint-disable-next-line no-console
console.warn('Supabase non initialisé — vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY');
setError('Supabase non initialisé — vérifiez les variables d\'environnement');
setLoading(false);
@@ -1013,7 +1057,7 @@ export default function ClassiquesDeparts(){
const intervalId = setInterval(fetchData, intervalMs);
return () => { mounted = false; clearInterval(intervalId); };
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
// Helpers pour perturbations (memoized plus haut)
@@ -1089,21 +1133,6 @@ export default function ClassiquesDeparts(){
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
let showPlatform = !isSuppressed && Boolean(platform);
if (showPlatform) {
const nowHM = currentTime.getHours()*60 + currentTime.getMinutes();
const sched = (typeof horaire._depHM === 'number') ? horaire._depHM : parseTimeHM(horaire.heure_depart || horaire.time || horaire.heure || horaire._rawTime || '');
const threshold = thresholdForStationType(stationType);
if (typeof sched === 'number') {
let diff = sched - nowHM;
// Gérer le passage à minuit
if (diff < -720) { // e.g., now 23:00, sched 01:00, diff is ~ -22 hours
diff += 1440; // treat as next day
}
showPlatform = (diff >= 0 && diff <= threshold);
} else {
showPlatform = false;
}
}
const isSub = isSubstitution(horaire);
const infoText = isSub
@@ -4,6 +4,8 @@ import supabase from '../../../../lib/supabase';
import { fetchAllRows } from '../../../../lib/fetchAllRows';
import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../../services/railwayService';
import { getSncfTimetable } from '../../../../services/sncfApi';
function Clock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
@@ -46,7 +48,7 @@ export default function ClassiquesArriveesQuais() {
const [loadingHoraire, setLoadingHoraire] = useState(false);
const [, setHoraireError] = useState(null);
const [, setCandidatesCount] = useState(0);
const [stationType, setStationType] = useState(null);
const [, setStationType] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date());
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
@@ -111,11 +113,14 @@ export default function ClassiquesArriveesQuais() {
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes();
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
}, []);
@@ -123,15 +128,26 @@ export default function ClassiquesArriveesQuais() {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2,'0');
const hh = String(tryDate.getHours() % 24).padStart(2,'0');
const mm = String(tryDate.getMinutes()).padStart(2,'0');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`;
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`;
if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h');
}
return String(v).slice(0,5).replace(':','h');
@@ -145,13 +161,7 @@ export default function ClassiquesArriveesQuais() {
return `${hh}h${mm}`;
}, []);
const thresholdForStationType = useCallback((type) => {
if (!type) return 0;
const t = String(type).trim().toLowerCase();
if (t === 'interurbaine' || t.includes('interurb')) return 360;
if (t === 'ville' || t.includes('ville')) return 30;
return 0;
}, []);
@@ -457,6 +467,58 @@ export default function ClassiquesArriveesQuais() {
setLoadingHoraire(true);
setHoraireError(null);
setSelectedHoraire(null);
const source = getSourceFromPath(window.location.pathname);
if (source === 'sncf') {
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplayParam || gareParam,
mode: 'arrivees',
quai: quaiParam
});
if (!mounted) return;
if (sncfErr) {
setHoraireError(sncfErr);
} else if (sncfData && sncfData.length > 0) {
const train = sncfData[0];
const now = new Date();
const nowHM = now.getHours() * 60 + now.getMinutes();
const arrHM = parseTimeHM(train.heure_arrivee || train.heure_depart);
const chosen = {
horaire: {
...train,
_status: train._status || {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: train.retard_minutes || null,
causeDetails: train.cause_retard || null
}
},
stop: {
gare_nom: train.gare_arrivee_nom || gareDisplayParam || gareParam,
heure_arrivee: train.heure_arrivee || train.heure_depart,
quai: train.quai || quaiParam,
quai_reel: train.quai || quaiParam
},
arrHM: arrHM != null ? arrHM : nowHM,
rawTime: train.heure_arrivee || train.heure_depart,
circulatesToday: true,
circulatesTomorrow: false,
isTomorrow: false
};
setSelectedHoraire(chosen);
setCandidatesCount(sncfData.length);
setStationType('interurbaine');
}
} catch (e) {
if (mounted) setHoraireError(e.message || String(e));
} finally {
if (mounted) setLoadingHoraire(false);
}
return;
}
if (!supabase) {
setHoraireError('Supabase non initialisé');
setLoadingHoraire(false);
@@ -650,16 +712,10 @@ export default function ClassiquesArriveesQuais() {
loadNextForQuai();
return () => { mounted = false; };
}, [gareDisplayParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]);
}, [gareDisplayParam, gareParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]);
// Rail is handled purely in CSS for better performance and alignment
const showPlatform = (() => {
const sched = selectedHoraire ? selectedHoraire.arrHM : null;
if (sched === null || !stationType) return true;
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
const threshold = thresholdForStationType(stationType);
return nowHM >= (sched - threshold);
})();
const showPlatform = true;
if (!loadingHoraire && !selectedHoraire) {
return (
@@ -789,7 +845,7 @@ export default function ClassiquesArriveesQuais() {
try { const p = JSON.parse(horaire.gares_desservies); if (Array.isArray(p)) stops = p; } catch (e) { /* ignore */ }
}
}
if (!Array.isArray(stops) || stops.length === 0) stops = mock.stops || [];
if (!Array.isArray(stops) || stops.length === 0) stops = [];
const labeled = stops.map((stop, idx) => {
let label = '';
@@ -870,7 +926,7 @@ export default function ClassiquesArriveesQuais() {
{/* BOTTOM FOOTER: Full Width */}
<div className="aff-quais-arrivees-redesign__footer">
<div className="aff-quais-arrivees-redesign__footer-left">
ain.
{quaiParam ? `Voie ${quaiParam}` : (selectedHoraire?.horaire?.quai ? `Voie ${selectedHoraire.horaire.quai}` : (selectedHoraire?.stop?.quai ? `Voie ${selectedHoraire.stop.quai}` : ''))}
</div>
<div className="aff-quais-arrivees-redesign__footer-right">
<Clock />
@@ -4,6 +4,8 @@ import supabase from '../../../../lib/supabase';
import { fetchAllRows } from '../../../../lib/fetchAllRows';
import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../../services/railwayService';
import { getSncfTimetable } from '../../../../services/sncfApi';
function Clock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
@@ -46,7 +48,7 @@ export default function ClassiquesDepartsQuais() {
const [loadingHoraire, setLoadingHoraire] = useState(false);
const [, setHoraireError] = useState(null);
const [, setCandidatesCount] = useState(0);
const [stationType, setStationType] = useState(null);
const [, setStationType] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date());
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
@@ -111,11 +113,14 @@ export default function ClassiquesDepartsQuais() {
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes();
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
}, []);
@@ -123,15 +128,26 @@ export default function ClassiquesDepartsQuais() {
if (v == null) return '';
const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2,'0');
const hh = String(tryDate.getHours() % 24).padStart(2,'0');
const mm = String(tryDate.getMinutes()).padStart(2,'0');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`;
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`;
if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h');
}
return String(v).slice(0,5).replace(':','h');
@@ -145,13 +161,7 @@ export default function ClassiquesDepartsQuais() {
return `${hh}h${mm}`;
}, []);
const thresholdForStationType = useCallback((type) => {
if (!type) return 0;
const t = String(type).trim().toLowerCase();
if (t === 'interurbaine' || t.includes('interurb')) return 360;
if (t === 'ville' || t.includes('ville')) return 30;
return 0;
}, []);
@@ -457,6 +467,58 @@ export default function ClassiquesDepartsQuais() {
setLoadingHoraire(true);
setHoraireError(null);
setSelectedHoraire(null);
const source = getSourceFromPath(window.location.pathname);
if (source === 'sncf') {
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplayParam || gareParam,
mode: 'departs',
quai: quaiParam
});
if (!mounted) return;
if (sncfErr) {
setHoraireError(sncfErr);
} else if (sncfData && sncfData.length > 0) {
const train = sncfData[0];
const now = new Date();
const nowHM = now.getHours() * 60 + now.getMinutes();
const depHM = parseTimeHM(train.heure_depart);
const chosen = {
horaire: {
...train,
_status: train._status || {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: train.retard_minutes || null,
causeDetails: train.cause_retard || null
}
},
stop: {
gare_nom: train.gare_depart_nom || gareDisplayParam || gareParam,
heure_depart: train.heure_depart,
quai: train.quai || quaiParam,
quai_reel: train.quai || quaiParam
},
depHM: depHM != null ? depHM : nowHM,
rawTime: train.heure_depart,
circulatesToday: true,
circulatesTomorrow: false,
isTomorrow: false
};
setSelectedHoraire(chosen);
setCandidatesCount(sncfData.length);
setStationType('interurbaine');
}
} catch (e) {
if (mounted) setHoraireError(e.message || String(e));
} finally {
if (mounted) setLoadingHoraire(false);
}
return;
}
if (!supabase) {
setHoraireError('Supabase non initialisé');
setLoadingHoraire(false);
@@ -648,16 +710,10 @@ export default function ClassiquesDepartsQuais() {
loadNextForQuai();
return () => { mounted = false; };
}, [gareDisplayParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]);
}, [gareDisplayParam, gareParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]);
// Rail is handled purely in CSS for better performance and alignment
const showPlatform = (() => {
const sched = selectedHoraire ? selectedHoraire.depHM : null;
if (sched === null || !stationType) return true;
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
const threshold = thresholdForStationType(stationType);
return nowHM >= (sched - threshold);
})();
const showPlatform = true;
if (!loadingHoraire && !selectedHoraire) {
return (
@@ -786,7 +842,7 @@ export default function ClassiquesDepartsQuais() {
try { const p = JSON.parse(horaire.gares_desservies); if (Array.isArray(p)) stops = p; } catch (e) { /* ignore */ }
}
}
if (!Array.isArray(stops) || stops.length === 0) stops = mock.stops || [];
if (!Array.isArray(stops) || stops.length === 0) stops = [];
const labeled = stops.map((stop, idx) => {
let label = '';
@@ -868,7 +924,7 @@ export default function ClassiquesDepartsQuais() {
{/* BOTTOM FOOTER: Full Width */}
<div className="aff-quais-departs-redesign__footer">
<div className="aff-quais-departs-redesign__footer-left">
ain.
{quaiParam ? `Voie ${quaiParam}` : (selectedHoraire?.horaire?.quai ? `Voie ${selectedHoraire.horaire.quai}` : (selectedHoraire?.stop?.quai ? `Voie ${selectedHoraire.stop.quai}` : ''))}
</div>
<div className="aff-quais-departs-redesign__footer-right">
<Clock />