V2.5 : VOICI LES MODIFICATIONS MAJEURS APPRTÉES DANS CETTE VERSION :

-> (BÉTA) : SYSTÈME D'ANNONCES EN GARE VIA UN MODEL IA DE SIMONE HERAULT.
This commit is contained in:
Ferrovia committed 2026-07-02 22:22:37 +02:00
1 parent ad3fc8938c
commit ad999aa44f
14 files changed
+517 -62

No files matched your search

@@ -3,6 +3,8 @@ import './ClassiquesArrivees.css';
import supabase from '../../../lib/supabase';
import Logo from '../../../components/Logo';
import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
@@ -97,15 +99,23 @@ export default function ClassiquesArrivees(){
// debug des données brutes renvoyées par Supabase
const [rawDebug, setRawDebug] = useState({ count: 0, sample: [] });
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
useAnnonceSonoreRealtime(resolvedGareId);
useAutoAnnonce(horaires, currentTime, true);
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
useEffect(() => {
let mounted = true;
const fetchStationType = async () => {
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
if (gareIdParam) {
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).limit(1);
if (!gerr && gdata && gdata.length) fetchedType = gdata[0] && gdata[0].type;
if (!gerr && gdata && gdata.length) {
fetchedType = gdata[0] && gdata[0].type;
fetchedId = gdata[0] && gdata[0].id;
}
} else if (gareDisplay) {
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
@@ -115,11 +125,14 @@ export default function ClassiquesArrivees(){
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
return n1 === n2;
});
fetchedType = exactMatch ? exactMatch.type : gdata[0].type;
const chosen = exactMatch || gdata[0];
fetchedType = chosen ? chosen.type : null;
fetchedId = chosen ? chosen.id : null;
}
}
if (mounted) {
setStationType(fetchedType ? String(fetchedType).toLowerCase() : null);
setResolvedGareId(fetchedId);
}
} catch (e) { /* ignore */ }
};
@@ -665,29 +678,29 @@ export default function ClassiquesArrivees(){
if (gareDisplay) {
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
displayHoraires = displayHoraires.filter(h => {
// Exclure si la gare ciblée est la gare d'arrivée finale
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
if (destName === normalized) return false;
// Inclure si gare de départ correspond
// Exclure si la gare ciblée est la gare de départ
const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString());
if (departName === normalized) return true;
if (departName === normalized) return false;
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max)
// Inclure si gare d'arrivée correspond
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
if (destName === normalized) return true;
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le point de départ (isFirst ou index==0)
const stopsList = getStops(h);
if (Array.isArray(stopsList) && stopsList.length) {
let maxIdx = null;
let minIdx = null;
for (const st of stopsList) {
if (typeof st.index !== 'undefined' && st.index !== null) {
const n = Number(st.index);
if (!Number.isNaN(n)) maxIdx = maxIdx == null ? n : Math.max(maxIdx, n);
if (!Number.isNaN(n)) minIdx = minIdx == null ? n : Math.min(minIdx, n);
}
}
for (const s of stopsList) {
const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
if (!stopName) continue;
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
if (isTerminus) continue;
const isOrigine = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && minIdx != null && Number(s.index) === minIdx));
if (isOrigine) continue;
if (stopName === normalized) return true;
}
}
@@ -696,29 +709,29 @@ export default function ClassiquesArrivees(){
} else if (gareIdParam) {
const gid = String(gareIdParam);
displayHoraires = displayHoraires.filter(h => {
// Exclure si gare d'arrivée finale correspond
const destId = (h.gare_arrivee_id || h.gare_arrivee || null);
if (destId != null && String(destId) === gid) return false;
// Inclure si gare de départ correspond
// Exclure si gare de départ correspond
const departId = h.gare_depart_id || h.gare_id || h.origin_id || null;
if (departId != null && String(departId) === gid) return true;
if (departId != null && String(departId) === gid) return false;
// Chercher dans les arrêts desservis (ignorer terminus)
// Inclure si gare d'arrivée finale correspond
const destId = (h.gare_arrivee_id || h.gare_arrivee || null);
if (destId != null && String(destId) === gid) return true;
// Chercher dans les arrêts desservis (ignorer origine)
const stopsList = getStops(h);
if (Array.isArray(stopsList) && stopsList.length) {
let maxIdx = null;
let minIdx = null;
for (const st of stopsList) {
if (typeof st.index !== 'undefined' && st.index !== null) {
const n = Number(st.index);
if (!Number.isNaN(n)) maxIdx = maxIdx == null ? n : Math.max(maxIdx, n);
if (!Number.isNaN(n)) minIdx = minIdx == null ? n : Math.min(minIdx, n);
}
}
for (const s of stopsList) {
const stopId = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
if (stopId == null) continue;
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
if (isTerminus) continue;
const isOrigine = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && minIdx != null && Number(s.index) === minIdx));
if (isOrigine) continue;
if (String(stopId) === gid) return true;
}
}
@@ -3,6 +3,8 @@ import './ClassiquesDeparts.css';
import supabase from '../../../lib/supabase';
import Logo from '../../../components/Logo';
import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
@@ -97,15 +99,23 @@ export default function ClassiquesDeparts(){
// debug des données brutes renvoyées par Supabase
const [rawDebug, setRawDebug] = useState({ count: 0, sample: [] });
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
useAnnonceSonoreRealtime(resolvedGareId);
useAutoAnnonce(horaires, currentTime, false);
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
useEffect(() => {
let mounted = true;
const fetchStationType = async () => {
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
if (gareIdParam) {
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).limit(1);
if (!gerr && gdata && gdata.length) fetchedType = gdata[0] && gdata[0].type;
if (!gerr && gdata && gdata.length) {
fetchedType = gdata[0] && gdata[0].type;
fetchedId = gdata[0] && gdata[0].id;
}
} else if (gareDisplay) {
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
@@ -115,11 +125,14 @@ export default function ClassiquesDeparts(){
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
return n1 === n2;
});
fetchedType = exactMatch ? exactMatch.type : gdata[0].type;
const chosen = exactMatch || gdata[0];
fetchedType = chosen ? chosen.type : null;
fetchedId = chosen ? chosen.id : null;
}
}
if (mounted) {
setStationType(fetchedType ? String(fetchedType).toLowerCase() : null);
setResolvedGareId(fetchedId);
}
} catch (e) { /* ignore */ }
};
@@ -1,6 +1,8 @@
import React, { useEffect, useState, useCallback } from 'react';
import './ClassiquesArriveesQuais.css';
import supabase from '../../../../lib/supabase';
import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
function Clock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
@@ -20,6 +22,13 @@ function Clock() {
}
export default function ClassiquesArriveesQuais() {
const paramsForThis = new URLSearchParams(window.location.search || '');
const gareParam = paramsForThis.get('gare');
const gareIdParam = paramsForThis.get('gare_id') || paramsForThis.get('gareId');
const quaiParam = paramsForThis.get('quai');
const gareDisplayParam = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g,' ')).replace(/-/g,' ') : null;
const mock = {
time: '16h44',
status: "à l'heure",
@@ -38,6 +47,10 @@ export default function ClassiquesArriveesQuais() {
const [, setCandidatesCount] = useState(0);
const [stationType, setStationType] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date());
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
useAnnonceSonoreRealtime(resolvedGareId);
useAutoAnnonce(selectedHoraire ? [selectedHoraire] : [], currentTime, true);
const [alternateDelayPhase, setAlternateDelayPhase] = useState(true);
const [alternateCancelPhase, setAlternateCancelPhase] = useState(true);
@@ -172,12 +185,7 @@ export default function ClassiquesArriveesQuais() {
return 0;
}, []);
const paramsForThis = new URLSearchParams(window.location.search || '');
const gareParam = paramsForThis.get('gare');
const gareIdParam = paramsForThis.get('gare_id') || paramsForThis.get('gareId');
const quaiParam = paramsForThis.get('quai');
const gareDisplayParam = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g,' ')).replace(/-/g,' ') : null;
const getStops = (h) => {
if (!h) return [];
@@ -489,22 +497,31 @@ export default function ClassiquesArriveesQuais() {
try {
let fetchedType = null;
try {
let fetchedId = gareIdParam || null;
if (gareIdParam) {
const { data: g } = await supabase.from('gares').select('type').eq('gare_id', gareIdParam).single();
if (g) fetchedType = g.type;
const { data: g } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).single();
if (g) {
fetchedType = g.type;
fetchedId = g.id;
}
} else if (gareDisplayParam) {
const { data: gdata } = await supabase.from('gares').select('type,nom').ilike('nom', `%${gareDisplayParam}%`);
const { data: gdata } = await supabase.from('gares').select('id,type,nom').ilike('nom', `%${gareDisplayParam}%`);
if (gdata && gdata.length) {
const exactMatch = gdata.find(g => {
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
const n2 = String(gareDisplayParam || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
return n1 === n2;
});
fetchedType = exactMatch ? exactMatch.type : gdata[0].type;
const chosen = exactMatch || gdata[0];
fetchedType = chosen ? chosen.type : null;
fetchedId = chosen ? chosen.id : null;
}
}
if (mounted) {
setStationType(fetchedType);
setResolvedGareId(fetchedId);
}
} catch (e) {}
if (mounted) setStationType(fetchedType);
const { data: all, error } = await supabase.from('horaires').select('id, *');
if (error) {
@@ -607,7 +624,7 @@ export default function ClassiquesArriveesQuais() {
}
if (!stopMatch) continue;
const platform = (stopMatch && (stopMatch.quai || stopMatch.platform || stopMatch.quay)) || h.platform || h.quai || '';
const platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.quay)) || h.quai_reel || h.platform || h.quai || '';
const normPlatform = String(platform || '').toLowerCase().replace(/\s+/g,'');
if (targetQuai) {
@@ -625,13 +642,13 @@ export default function ClassiquesArriveesQuais() {
if (stopIsObject && hasDepartureOnly) continue;
const rawTime = (hasArrival ? (stopMatch.heure_arrivee || stopMatch.time || stopMatch.heure || stopMatch.arrival_time || stopMatch.arrival) : (h.heure_arrivee || h.time || h.heure || h.arrival_time || h.arrival));
const depHM = parseTimeHM(rawTime);
if (depHM == null) continue;
const arrHM = parseTimeHM(rawTime);
if (arrHM == null) continue;
let minutesUntil = depHM - nowHM;
if (minutesUntil < 0) minutesUntil = depHM + 24*60 - nowHM;
let minutesUntil = arrHM - nowHM;
if (minutesUntil < 0) minutesUntil = arrHM + 24*60 - nowHM;
candidates.push({ horaire: h, stop: stopMatch, depHM, minutesUntil, rawTime, circulatesToday, circulatesTomorrow });
candidates.push({ horaire: h, stop: stopMatch, arrHM, minutesUntil, rawTime, circulatesToday, circulatesTomorrow });
}
if (!candidates.length) {
@@ -641,8 +658,8 @@ export default function ClassiquesArriveesQuais() {
return;
}
const todayCandidates = candidates.filter(c => c.circulatesToday && c.depHM > nowHM).sort((a,b) => a.depHM - b.depHM);
const tomorrowCandidates = candidates.filter(c => c.circulatesTomorrow && (c.depHM <= nowHM || !c.circulatesToday)).sort((a,b) => a.depHM - b.depHM);
const todayCandidates = candidates.filter(c => c.circulatesToday && c.arrHM >= nowHM - 1).sort((a,b) => a.arrHM - b.arrHM);
const tomorrowCandidates = candidates.filter(c => c.circulatesTomorrow && (c.arrHM < nowHM - 1 || !c.circulatesToday)).sort((a,b) => a.arrHM - b.arrHM);
let chosen = null;
if (todayCandidates.length > 0) {
@@ -669,7 +686,7 @@ export default function ClassiquesArriveesQuais() {
// Rail is handled purely in CSS for better performance and alignment
const showPlatform = (() => {
const sched = selectedHoraire ? selectedHoraire.depHM : null;
const sched = selectedHoraire ? selectedHoraire.arrHM : null;
if (sched === null || !stationType) return true;
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
const threshold = thresholdForStationType(stationType);
@@ -1,6 +1,8 @@
import React, { useEffect, useState, useCallback } from 'react';
import './ClassiquesDepartsQuais.css';
import supabase from '../../../../lib/supabase';
import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
function Clock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
@@ -20,6 +22,13 @@ function Clock() {
}
export default function ClassiquesDepartsQuais() {
const paramsForThis = new URLSearchParams(window.location.search || '');
const gareParam = paramsForThis.get('gare');
const gareIdParam = paramsForThis.get('gare_id') || paramsForThis.get('gareId');
const quaiParam = paramsForThis.get('quai');
const gareDisplayParam = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g,' ')).replace(/-/g,' ') : null;
const mock = {
time: '16h44',
status: "à l'heure",
@@ -38,6 +47,10 @@ export default function ClassiquesDepartsQuais() {
const [, setCandidatesCount] = useState(0);
const [stationType, setStationType] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date());
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
useAnnonceSonoreRealtime(resolvedGareId);
useAutoAnnonce(selectedHoraire ? [selectedHoraire] : [], currentTime, false);
const [alternateDelayPhase, setAlternateDelayPhase] = useState(true);
const [alternateCancelPhase, setAlternateCancelPhase] = useState(true);
@@ -172,12 +185,7 @@ export default function ClassiquesDepartsQuais() {
return 0;
}, []);
const paramsForThis = new URLSearchParams(window.location.search || '');
const gareParam = paramsForThis.get('gare');
const gareIdParam = paramsForThis.get('gare_id') || paramsForThis.get('gareId');
const quaiParam = paramsForThis.get('quai');
const gareDisplayParam = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g,' ')).replace(/-/g,' ') : null;
const getStops = (h) => {
if (!h) return [];
@@ -489,22 +497,31 @@ export default function ClassiquesDepartsQuais() {
try {
let fetchedType = null;
try {
let fetchedId = gareIdParam || null;
if (gareIdParam) {
const { data: g } = await supabase.from('gares').select('type').eq('gare_id', gareIdParam).single();
if (g) fetchedType = g.type;
const { data: g } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).single();
if (g) {
fetchedType = g.type;
fetchedId = g.id;
}
} else if (gareDisplayParam) {
const { data: gdata } = await supabase.from('gares').select('type,nom').ilike('nom', `%${gareDisplayParam}%`);
const { data: gdata } = await supabase.from('gares').select('id,type,nom').ilike('nom', `%${gareDisplayParam}%`);
if (gdata && gdata.length) {
const exactMatch = gdata.find(g => {
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
const n2 = String(gareDisplayParam || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
return n1 === n2;
});
fetchedType = exactMatch ? exactMatch.type : gdata[0].type;
const chosen = exactMatch || gdata[0];
fetchedType = chosen ? chosen.type : null;
fetchedId = chosen ? chosen.id : null;
}
}
if (mounted) {
setStationType(fetchedType);
setResolvedGareId(fetchedId);
}
} catch (e) {}
if (mounted) setStationType(fetchedType);
const { data: all, error } = await supabase.from('horaires').select('id, *');
if (error) {
@@ -605,7 +622,7 @@ export default function ClassiquesDepartsQuais() {
}
if (!stopMatch) continue;
const platform = (stopMatch && (stopMatch.quai || stopMatch.platform || stopMatch.quay)) || h.platform || h.quai || '';
const platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.quay)) || h.quai_reel || h.platform || h.quai || '';
const normPlatform = String(platform || '').toLowerCase().replace(/\s+/g,'');
if (targetQuai) {
@@ -639,8 +656,8 @@ export default function ClassiquesDepartsQuais() {
return;
}
const todayCandidates = candidates.filter(c => c.circulatesToday && c.depHM > nowHM).sort((a,b) => a.depHM - b.depHM);
const tomorrowCandidates = candidates.filter(c => c.circulatesTomorrow && (c.depHM <= nowHM || !c.circulatesToday)).sort((a,b) => a.depHM - b.depHM);
const todayCandidates = candidates.filter(c => c.circulatesToday && c.depHM >= nowHM - 1).sort((a,b) => a.depHM - b.depHM);
const tomorrowCandidates = candidates.filter(c => c.circulatesTomorrow && (c.depHM < nowHM - 1 || !c.circulatesToday)).sort((a,b) => a.depHM - b.depHM);
let chosen = null;
if (todayCandidates.length > 0) {