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:
1 parent
ad3fc8938c
commit
ad999aa44f
14 files changed
+517
-62
No files matched your search
@@ -0,0 +1,35 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{ p: 'src/pages/afficheurs/afl/AFLDeparts.js', arr: false },
|
||||||
|
{ p: 'src/pages/afficheurs/afl/AFLArrivees.js', arr: true },
|
||||||
|
{ p: 'src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js', arr: false },
|
||||||
|
{ p: 'src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js', arr: true },
|
||||||
|
{ p: 'src/pages/afficheurs/afl/quais/AFLDepartsQuais.js', arr: false },
|
||||||
|
{ p: 'src/pages/afficheurs/afl/quais/AFLArriveesQuais.js', arr: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { p, arr } of files) {
|
||||||
|
const fullPath = path.join(process.cwd(), p);
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
console.log(`File not found: ${fullPath}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let content = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
|
||||||
|
if (!content.includes('import useAutoAnnonce from')) {
|
||||||
|
content = content.replace(
|
||||||
|
"import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';",
|
||||||
|
"import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';\nimport useAutoAnnonce from '../../../hooks/useAutoAnnonce';"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content.includes('useAutoAnnonce(')) {
|
||||||
|
const hookCall = ` useAnnonceSonoreRealtime(resolvedGareId);\n useAutoAnnonce(horaires, currentTime, ${arr});`;
|
||||||
|
content = content.replace(" useAnnonceSonoreRealtime(resolvedGareId);", hookCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(fullPath, content, 'utf8');
|
||||||
|
console.log(`Patched ${p}`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const { createClient } = require('@supabase/supabase-js');
|
||||||
|
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||||
|
async function run() {
|
||||||
|
const { data, error } = await supabase.from('horaires').select('*').limit(1);
|
||||||
|
if (error) console.error(error);
|
||||||
|
else console.log(JSON.stringify(data[0], null, 2));
|
||||||
|
}
|
||||||
|
run();
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import supabase from '../lib/supabase';
|
||||||
|
|
||||||
|
export default function useAnnonceSonoreRealtime(gareId) {
|
||||||
|
const audioRef = useRef(null);
|
||||||
|
const lastAnnonceIdRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!supabase || !gareId) return;
|
||||||
|
|
||||||
|
console.debug(`[useAnnonceSonoreRealtime] Setup for gareId: ${gareId}`);
|
||||||
|
|
||||||
|
const playAnnonce = (newRecord) => {
|
||||||
|
// 1. Couper l'audio en cours de lecture
|
||||||
|
if (audioRef.current) {
|
||||||
|
try { audioRef.current.pause(); } catch (e) {}
|
||||||
|
audioRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Couper le TTS en cours de lecture
|
||||||
|
if (typeof window !== 'undefined' && window.speechSynthesis) {
|
||||||
|
try { window.speechSynthesis.cancel(); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Diffuser l'annonce
|
||||||
|
if (newRecord.audio_path) {
|
||||||
|
const { data } = supabase.storage.from('banque-annonces').getPublicUrl(newRecord.audio_path);
|
||||||
|
const publicUrl = data?.publicUrl;
|
||||||
|
|
||||||
|
if (publicUrl) {
|
||||||
|
console.debug(`[useAnnonceSonoreRealtime] Lecture audio de : ${publicUrl}`);
|
||||||
|
const audio = new Audio(publicUrl);
|
||||||
|
audioRef.current = audio;
|
||||||
|
audio.play().catch((err) => {
|
||||||
|
console.error('[useAnnonceSonoreRealtime] Erreur lecture audio:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (newRecord.tts_texte) {
|
||||||
|
if (typeof window !== 'undefined' && window.speechSynthesis) {
|
||||||
|
console.debug(`[useAnnonceSonoreRealtime] Lecture TTS de : "${newRecord.tts_texte}"`);
|
||||||
|
const utterance = new SpeechSynthesisUtterance(newRecord.tts_texte);
|
||||||
|
utterance.lang = 'fr-FR';
|
||||||
|
|
||||||
|
const voices = window.speechSynthesis.getVoices();
|
||||||
|
const frenchVoices = voices.filter(v => v.lang && (v.lang.includes('fr-FR') || v.lang.startsWith('fr')));
|
||||||
|
const claireVoice = frenchVoices.find(v => v.name && v.name.toLowerCase().includes('claire'));
|
||||||
|
|
||||||
|
utterance.voice = claireVoice || frenchVoices[0] || null;
|
||||||
|
window.speechSynthesis.speak(utterance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- FALLBACK POLLING (Seule méthode utilisée) ---
|
||||||
|
// Puisque le WebSocket vers ton instance auto-hébergée (wss://...) est bloqué par le reverse proxy,
|
||||||
|
// on désactive l'abonnement Realtime pour éviter les erreurs "CHANNEL_ERROR" dans ta console.
|
||||||
|
// À la place, on interroge la base de données toutes les 2.5 secondes.
|
||||||
|
const fetchLatest = async () => {
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('annonces_diffusions')
|
||||||
|
.select('*')
|
||||||
|
.eq('gare_id', gareId)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!error && data && data.length > 0) {
|
||||||
|
const latest = data[0];
|
||||||
|
|
||||||
|
// Si c'est une annonce qu'on n'a pas encore vue
|
||||||
|
if (latest.id !== lastAnnonceIdRef.current) {
|
||||||
|
const now = new Date();
|
||||||
|
const createdAt = new Date(latest.created_at);
|
||||||
|
const diffSeconds = (now.getTime() - createdAt.getTime()) / 1000;
|
||||||
|
|
||||||
|
// On ne joue l'annonce que si elle a été insérée il y a moins de 15 secondes
|
||||||
|
if (diffSeconds < 15) {
|
||||||
|
console.debug('[useAnnonceSonoreRealtime] Annonce spontanée détectée :', latest);
|
||||||
|
lastAnnonceIdRef.current = latest.id;
|
||||||
|
playAnnonce(latest);
|
||||||
|
} else {
|
||||||
|
// Si elle est trop vieille, on l'ignore silencieusement
|
||||||
|
lastAnnonceIdRef.current = latest.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Ignorer les erreurs réseau silencieusement
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// On lance le polling toutes les 2.5 secondes pour une réactivité maximale
|
||||||
|
const pollInterval = setInterval(fetchLatest, 2500);
|
||||||
|
// On fait aussi un fetch immédiat au démarrage pour initialiser l'ID
|
||||||
|
fetchLatest();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.debug('[useAnnonceSonoreRealtime] Nettoyage...');
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
|
||||||
|
if (audioRef.current) {
|
||||||
|
try { audioRef.current.pause(); } catch (e) {}
|
||||||
|
audioRef.current = null;
|
||||||
|
}
|
||||||
|
if (typeof window !== 'undefined' && window.speechSynthesis) {
|
||||||
|
try { window.speechSynthesis.cancel(); } catch (e) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [gareId]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
// Hook pour générer automatiquement les annonces sur l'afficheur (fallback TTS robuste)
|
||||||
|
export default function useAutoAnnonce(horaires, currentTime, isArrival = false) {
|
||||||
|
const announcedRefs = useRef(new Set());
|
||||||
|
const isPlayingRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!horaires || horaires.length === 0 || !currentTime) return;
|
||||||
|
|
||||||
|
// Déterminer le délai d'annonce (2 min avant l'arrivée ou le départ)
|
||||||
|
const TRIGGER_MINUTES = 2;
|
||||||
|
|
||||||
|
const checkAndPlay = () => {
|
||||||
|
if (isPlayingRef.current) return; // Ne pas interrompre si une annonce joue déjà
|
||||||
|
|
||||||
|
for (const train of horaires) {
|
||||||
|
const numTrain = train.num_train || train.numero_train || train.numero;
|
||||||
|
if (!numTrain) continue;
|
||||||
|
|
||||||
|
const timeStr = isArrival
|
||||||
|
? (train.heure_arrivee || train.arrival_time || train.time || train.heure)
|
||||||
|
: (train.heure_depart || train.departure_time || train.time || train.heure);
|
||||||
|
|
||||||
|
if (!timeStr) continue;
|
||||||
|
|
||||||
|
// Si timeStr est juste "HH:MM", on crée une date du jour avec cette heure
|
||||||
|
let trainTime;
|
||||||
|
if (typeof timeStr === 'string' && timeStr.length <= 5 && timeStr.includes(':')) {
|
||||||
|
const [hh, mm] = timeStr.split(':');
|
||||||
|
trainTime = new Date(currentTime);
|
||||||
|
trainTime.setHours(parseInt(hh, 10), parseInt(mm, 10), 0, 0);
|
||||||
|
} else {
|
||||||
|
trainTime = new Date(timeStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(trainTime.getTime())) continue;
|
||||||
|
|
||||||
|
const diffMs = trainTime.getTime() - currentTime.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
|
||||||
|
// Si on est dans la minute cible et que l'annonce n'a pas encore été jouée
|
||||||
|
if (diffMins === TRIGGER_MINUTES && !announcedRefs.current.has(numTrain)) {
|
||||||
|
announcedRefs.current.add(numTrain);
|
||||||
|
|
||||||
|
const gareObj = isArrival
|
||||||
|
? (train.gare_depart_nom || train.gare_depart || train.origine)
|
||||||
|
: (train.gare_arrivee_nom || train.gare_arrivee || train.destination);
|
||||||
|
|
||||||
|
const gareName = typeof gareObj === 'object' && gareObj !== null ? gareObj.nom : gareObj;
|
||||||
|
|
||||||
|
let voieText = train.voie ? `Voie ${train.voie}.` : "";
|
||||||
|
const typeTrain = train.type_train || "train";
|
||||||
|
const gareText = gareName || "destination inconnue";
|
||||||
|
|
||||||
|
const text = isArrival
|
||||||
|
? `${voieText} Le ${typeTrain} numéro ${numTrain} en provenance de ${gareText}, va entrer en gare. Éloignez-vous de la bordure du quai.`
|
||||||
|
: `${voieText} Le ${typeTrain} numéro ${numTrain} à destination de ${gareText}, va partir. Prenez garde à la fermeture des portes.`;
|
||||||
|
|
||||||
|
// Lancement de l'annonce TTS
|
||||||
|
isPlayingRef.current = true;
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && window.speechSynthesis) {
|
||||||
|
const utterance = new SpeechSynthesisUtterance(text);
|
||||||
|
utterance.lang = 'fr-FR';
|
||||||
|
const voices = window.speechSynthesis.getVoices();
|
||||||
|
const frenchVoices = voices.filter(v => v.lang && (v.lang.includes('fr-FR') || v.lang.startsWith('fr')));
|
||||||
|
const claireVoice = frenchVoices.find(v => v.name && v.name.toLowerCase().includes('claire'));
|
||||||
|
utterance.voice = claireVoice || frenchVoices[0] || null;
|
||||||
|
|
||||||
|
utterance.onend = () => {
|
||||||
|
isPlayingRef.current = false;
|
||||||
|
};
|
||||||
|
utterance.onerror = () => {
|
||||||
|
isPlayingRef.current = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
console.debug(`[useAutoAnnonce] Lecture TTS Automatique: "${text}"`);
|
||||||
|
window.speechSynthesis.speak(utterance);
|
||||||
|
} else {
|
||||||
|
isPlayingRef.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On s'arrête après avoir lancé une annonce pour éviter de tout superposer
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAndPlay();
|
||||||
|
}, [horaires, currentTime, isArrival]);
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import React, { useEffect, useState, useCallback } from 'react';
|
|||||||
import './AFLArrivees.css';
|
import './AFLArrivees.css';
|
||||||
import supabase from '../../../lib/supabase';
|
import supabase from '../../../lib/supabase';
|
||||||
import MarqueeText from '../../../components/MarqueeText';
|
import MarqueeText from '../../../components/MarqueeText';
|
||||||
|
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||||
|
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
|
||||||
|
|
||||||
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
||||||
const formatTimeNoSeconds = (v) => {
|
const formatTimeNoSeconds = (v) => {
|
||||||
@@ -84,15 +86,23 @@ export default function AFLArrivees(){
|
|||||||
// debug des données brutes renvoyées par Supabase
|
// debug des données brutes renvoyées par Supabase
|
||||||
const [, setRawDebug] = useState({ count: 0, sample: [] });
|
const [, 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
|
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
const fetchStationType = async () => {
|
const fetchStationType = async () => {
|
||||||
try {
|
try {
|
||||||
let fetchedType = null;
|
let fetchedType = null;
|
||||||
|
let fetchedId = gareIdParam || null;
|
||||||
if (gareIdParam) {
|
if (gareIdParam) {
|
||||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).limit(1);
|
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) {
|
} else if (gareDisplay) {
|
||||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
||||||
@@ -102,11 +112,14 @@ export default function AFLArrivees(){
|
|||||||
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||||
return n1 === n2;
|
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) {
|
if (mounted) {
|
||||||
setStationType(fetchedType ? String(fetchedType).toLowerCase() : null);
|
setStationType(fetchedType ? String(fetchedType).toLowerCase() : null);
|
||||||
|
setResolvedGareId(fetchedId);
|
||||||
}
|
}
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import supabase from '../../../lib/supabase';
|
import supabase from '../../../lib/supabase';
|
||||||
import './AFLArriveesQuais.css';
|
import './AFLArriveesQuais.css';
|
||||||
|
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||||
|
|
||||||
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
|
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
|
||||||
const formatTimeNoSeconds = (v) => {
|
const formatTimeNoSeconds = (v) => {
|
||||||
@@ -26,9 +27,46 @@ const AFLArriveesQuais = () => {
|
|||||||
// Parse URL manually since we are not in a React Router context
|
// Parse URL manually since we are not in a React Router context
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
const gareParam = searchParams.get('gare') || '';
|
const gareParam = searchParams.get('gare') || '';
|
||||||
|
const gareIdParam = searchParams.get('gare_id') || searchParams.get('gareId');
|
||||||
const quaiParam = searchParams.get('quai') || searchParams.get('voie') || '';
|
const quaiParam = searchParams.get('quai') || searchParams.get('voie') || '';
|
||||||
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
||||||
|
|
||||||
|
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
|
||||||
|
useAnnonceSonoreRealtime(resolvedGareId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
const resolveGareId = async () => {
|
||||||
|
if (!supabase) return;
|
||||||
|
try {
|
||||||
|
if (gareIdParam) {
|
||||||
|
const { data: gdata } = await supabase.from('gares').select('id').eq('id', gareIdParam).limit(1);
|
||||||
|
if (mounted && gdata && gdata.length) {
|
||||||
|
setResolvedGareId(gdata[0].id);
|
||||||
|
}
|
||||||
|
} else if (gareDisplay) {
|
||||||
|
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||||
|
const { data: gdata } = await supabase.from('gares').select('id, nom').ilike('nom', flexNeedle);
|
||||||
|
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(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||||
|
return n1 === n2;
|
||||||
|
});
|
||||||
|
const chosen = exactMatch || gdata[0];
|
||||||
|
if (mounted && chosen) {
|
||||||
|
setResolvedGareId(chosen.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[AFLArriveesQuais] Erreur resolution gare:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
resolveGareId();
|
||||||
|
return () => { mounted = false; };
|
||||||
|
}, [gareIdParam, gareDisplay]);
|
||||||
|
|
||||||
// Real-time clock update
|
// Real-time clock update
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
|
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import React, { useEffect, useState, useCallback } from 'react';
|
|||||||
import './AFLDeparts.css';
|
import './AFLDeparts.css';
|
||||||
import supabase from '../../../lib/supabase';
|
import supabase from '../../../lib/supabase';
|
||||||
import MarqueeText from '../../../components/MarqueeText';
|
import MarqueeText from '../../../components/MarqueeText';
|
||||||
|
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||||
|
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
|
||||||
|
|
||||||
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
||||||
const formatTimeNoSeconds = (v) => {
|
const formatTimeNoSeconds = (v) => {
|
||||||
@@ -84,15 +86,23 @@ export default function AFLDeparts(){
|
|||||||
// debug des données brutes renvoyées par Supabase
|
// debug des données brutes renvoyées par Supabase
|
||||||
const [, setRawDebug] = useState({ count: 0, sample: [] });
|
const [, 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
|
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
const fetchStationType = async () => {
|
const fetchStationType = async () => {
|
||||||
try {
|
try {
|
||||||
let fetchedType = null;
|
let fetchedType = null;
|
||||||
|
let fetchedId = gareIdParam || null;
|
||||||
if (gareIdParam) {
|
if (gareIdParam) {
|
||||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).limit(1);
|
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) {
|
} else if (gareDisplay) {
|
||||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
||||||
@@ -102,11 +112,14 @@ export default function AFLDeparts(){
|
|||||||
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||||
return n1 === n2;
|
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) {
|
if (mounted) {
|
||||||
setStationType(fetchedType ? String(fetchedType).toLowerCase() : null);
|
setStationType(fetchedType ? String(fetchedType).toLowerCase() : null);
|
||||||
|
setResolvedGareId(fetchedId);
|
||||||
}
|
}
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import supabase from '../../../lib/supabase';
|
import supabase from '../../../lib/supabase';
|
||||||
import './AFLDepartsQuais.css';
|
import './AFLDepartsQuais.css';
|
||||||
|
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||||
|
|
||||||
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
|
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
|
||||||
const formatTimeNoSeconds = (v) => {
|
const formatTimeNoSeconds = (v) => {
|
||||||
@@ -26,9 +27,46 @@ const AFLDepartsQuais = () => {
|
|||||||
// Parse URL manually since we are not in a React Router context
|
// Parse URL manually since we are not in a React Router context
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
const gareParam = searchParams.get('gare') || '';
|
const gareParam = searchParams.get('gare') || '';
|
||||||
|
const gareIdParam = searchParams.get('gare_id') || searchParams.get('gareId');
|
||||||
const quaiParam = searchParams.get('quai') || searchParams.get('voie') || '';
|
const quaiParam = searchParams.get('quai') || searchParams.get('voie') || '';
|
||||||
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
||||||
|
|
||||||
|
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
|
||||||
|
useAnnonceSonoreRealtime(resolvedGareId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
const resolveGareId = async () => {
|
||||||
|
if (!supabase) return;
|
||||||
|
try {
|
||||||
|
if (gareIdParam) {
|
||||||
|
const { data: gdata } = await supabase.from('gares').select('id').eq('id', gareIdParam).limit(1);
|
||||||
|
if (mounted && gdata && gdata.length) {
|
||||||
|
setResolvedGareId(gdata[0].id);
|
||||||
|
}
|
||||||
|
} else if (gareDisplay) {
|
||||||
|
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||||
|
const { data: gdata } = await supabase.from('gares').select('id, nom').ilike('nom', flexNeedle);
|
||||||
|
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(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||||
|
return n1 === n2;
|
||||||
|
});
|
||||||
|
const chosen = exactMatch || gdata[0];
|
||||||
|
if (mounted && chosen) {
|
||||||
|
setResolvedGareId(chosen.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[AFLDepartsQuais] Erreur resolution gare:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
resolveGareId();
|
||||||
|
return () => { mounted = false; };
|
||||||
|
}, [gareIdParam, gareDisplay]);
|
||||||
|
|
||||||
// Real-time clock update
|
// Real-time clock update
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
|
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import './ClassiquesArrivees.css';
|
|||||||
import supabase from '../../../lib/supabase';
|
import supabase from '../../../lib/supabase';
|
||||||
import Logo from '../../../components/Logo';
|
import Logo from '../../../components/Logo';
|
||||||
import MarqueeText from '../../../components/MarqueeText';
|
import MarqueeText from '../../../components/MarqueeText';
|
||||||
|
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||||
|
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
|
||||||
|
|
||||||
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
||||||
const formatTimeNoSeconds = (v) => {
|
const formatTimeNoSeconds = (v) => {
|
||||||
@@ -97,15 +99,23 @@ export default function ClassiquesArrivees(){
|
|||||||
// debug des données brutes renvoyées par Supabase
|
// debug des données brutes renvoyées par Supabase
|
||||||
const [rawDebug, setRawDebug] = useState({ count: 0, sample: [] });
|
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
|
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
const fetchStationType = async () => {
|
const fetchStationType = async () => {
|
||||||
try {
|
try {
|
||||||
let fetchedType = null;
|
let fetchedType = null;
|
||||||
|
let fetchedId = gareIdParam || null;
|
||||||
if (gareIdParam) {
|
if (gareIdParam) {
|
||||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).limit(1);
|
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) {
|
} else if (gareDisplay) {
|
||||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
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();
|
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||||
return n1 === n2;
|
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) {
|
if (mounted) {
|
||||||
setStationType(fetchedType ? String(fetchedType).toLowerCase() : null);
|
setStationType(fetchedType ? String(fetchedType).toLowerCase() : null);
|
||||||
|
setResolvedGareId(fetchedId);
|
||||||
}
|
}
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
};
|
};
|
||||||
@@ -665,29 +678,29 @@ export default function ClassiquesArrivees(){
|
|||||||
if (gareDisplay) {
|
if (gareDisplay) {
|
||||||
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
|
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
|
||||||
displayHoraires = displayHoraires.filter(h => {
|
displayHoraires = displayHoraires.filter(h => {
|
||||||
// Exclure si la gare ciblée est la gare d'arrivée finale
|
// Exclure si la gare ciblée est la gare de départ
|
||||||
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
|
||||||
if (destName === normalized) return false;
|
|
||||||
|
|
||||||
// Inclure si gare de départ correspond
|
|
||||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString());
|
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);
|
const stopsList = getStops(h);
|
||||||
if (Array.isArray(stopsList) && stopsList.length) {
|
if (Array.isArray(stopsList) && stopsList.length) {
|
||||||
let maxIdx = null;
|
let minIdx = null;
|
||||||
for (const st of stopsList) {
|
for (const st of stopsList) {
|
||||||
if (typeof st.index !== 'undefined' && st.index !== null) {
|
if (typeof st.index !== 'undefined' && st.index !== null) {
|
||||||
const n = Number(st.index);
|
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) {
|
for (const s of stopsList) {
|
||||||
const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
|
const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
|
||||||
if (!stopName) continue;
|
if (!stopName) continue;
|
||||||
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
|
const isOrigine = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && minIdx != null && Number(s.index) === minIdx));
|
||||||
if (isTerminus) continue;
|
if (isOrigine) continue;
|
||||||
if (stopName === normalized) return true;
|
if (stopName === normalized) return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -696,29 +709,29 @@ export default function ClassiquesArrivees(){
|
|||||||
} else if (gareIdParam) {
|
} else if (gareIdParam) {
|
||||||
const gid = String(gareIdParam);
|
const gid = String(gareIdParam);
|
||||||
displayHoraires = displayHoraires.filter(h => {
|
displayHoraires = displayHoraires.filter(h => {
|
||||||
// Exclure si gare d'arrivée finale correspond
|
// Exclure si gare de départ 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
|
|
||||||
const departId = h.gare_depart_id || h.gare_id || h.origin_id || null;
|
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);
|
const stopsList = getStops(h);
|
||||||
if (Array.isArray(stopsList) && stopsList.length) {
|
if (Array.isArray(stopsList) && stopsList.length) {
|
||||||
let maxIdx = null;
|
let minIdx = null;
|
||||||
for (const st of stopsList) {
|
for (const st of stopsList) {
|
||||||
if (typeof st.index !== 'undefined' && st.index !== null) {
|
if (typeof st.index !== 'undefined' && st.index !== null) {
|
||||||
const n = Number(st.index);
|
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) {
|
for (const s of stopsList) {
|
||||||
const stopId = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
|
const stopId = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
|
||||||
if (stopId == null) continue;
|
if (stopId == null) continue;
|
||||||
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
|
const isOrigine = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && minIdx != null && Number(s.index) === minIdx));
|
||||||
if (isTerminus) continue;
|
if (isOrigine) continue;
|
||||||
if (String(stopId) === gid) return true;
|
if (String(stopId) === gid) return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import './ClassiquesDeparts.css';
|
|||||||
import supabase from '../../../lib/supabase';
|
import supabase from '../../../lib/supabase';
|
||||||
import Logo from '../../../components/Logo';
|
import Logo from '../../../components/Logo';
|
||||||
import MarqueeText from '../../../components/MarqueeText';
|
import MarqueeText from '../../../components/MarqueeText';
|
||||||
|
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||||
|
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
|
||||||
|
|
||||||
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
||||||
const formatTimeNoSeconds = (v) => {
|
const formatTimeNoSeconds = (v) => {
|
||||||
@@ -97,15 +99,23 @@ export default function ClassiquesDeparts(){
|
|||||||
// debug des données brutes renvoyées par Supabase
|
// debug des données brutes renvoyées par Supabase
|
||||||
const [rawDebug, setRawDebug] = useState({ count: 0, sample: [] });
|
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
|
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
const fetchStationType = async () => {
|
const fetchStationType = async () => {
|
||||||
try {
|
try {
|
||||||
let fetchedType = null;
|
let fetchedType = null;
|
||||||
|
let fetchedId = gareIdParam || null;
|
||||||
if (gareIdParam) {
|
if (gareIdParam) {
|
||||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).limit(1);
|
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) {
|
} else if (gareDisplay) {
|
||||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
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();
|
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||||
return n1 === n2;
|
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) {
|
if (mounted) {
|
||||||
setStationType(fetchedType ? String(fetchedType).toLowerCase() : null);
|
setStationType(fetchedType ? String(fetchedType).toLowerCase() : null);
|
||||||
|
setResolvedGareId(fetchedId);
|
||||||
}
|
}
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import './ClassiquesArriveesQuais.css';
|
import './ClassiquesArriveesQuais.css';
|
||||||
import supabase from '../../../../lib/supabase';
|
import supabase from '../../../../lib/supabase';
|
||||||
|
import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime';
|
||||||
|
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
|
||||||
function Clock() {
|
function Clock() {
|
||||||
const [now, setNow] = useState(new Date());
|
const [now, setNow] = useState(new Date());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -20,6 +22,13 @@ function Clock() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ClassiquesArriveesQuais() {
|
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 = {
|
const mock = {
|
||||||
time: '16h44',
|
time: '16h44',
|
||||||
status: "à l'heure",
|
status: "à l'heure",
|
||||||
@@ -38,6 +47,10 @@ export default function ClassiquesArriveesQuais() {
|
|||||||
const [, setCandidatesCount] = useState(0);
|
const [, setCandidatesCount] = useState(0);
|
||||||
const [stationType, setStationType] = useState(null);
|
const [stationType, setStationType] = useState(null);
|
||||||
const [currentTime, setCurrentTime] = useState(new Date());
|
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 [alternateDelayPhase, setAlternateDelayPhase] = useState(true);
|
||||||
const [alternateCancelPhase, setAlternateCancelPhase] = useState(true);
|
const [alternateCancelPhase, setAlternateCancelPhase] = useState(true);
|
||||||
|
|
||||||
@@ -172,12 +185,7 @@ export default function ClassiquesArriveesQuais() {
|
|||||||
return 0;
|
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) => {
|
const getStops = (h) => {
|
||||||
if (!h) return [];
|
if (!h) return [];
|
||||||
@@ -489,22 +497,31 @@ export default function ClassiquesArriveesQuais() {
|
|||||||
try {
|
try {
|
||||||
let fetchedType = null;
|
let fetchedType = null;
|
||||||
try {
|
try {
|
||||||
|
let fetchedId = gareIdParam || null;
|
||||||
if (gareIdParam) {
|
if (gareIdParam) {
|
||||||
const { data: g } = await supabase.from('gares').select('type').eq('gare_id', gareIdParam).single();
|
const { data: g } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).single();
|
||||||
if (g) fetchedType = g.type;
|
if (g) {
|
||||||
|
fetchedType = g.type;
|
||||||
|
fetchedId = g.id;
|
||||||
|
}
|
||||||
} else if (gareDisplayParam) {
|
} 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) {
|
if (gdata && gdata.length) {
|
||||||
const exactMatch = gdata.find(g => {
|
const exactMatch = gdata.find(g => {
|
||||||
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
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();
|
const n2 = String(gareDisplayParam || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||||
return n1 === n2;
|
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) {}
|
} catch (e) {}
|
||||||
if (mounted) setStationType(fetchedType);
|
|
||||||
|
|
||||||
const { data: all, error } = await supabase.from('horaires').select('id, *');
|
const { data: all, error } = await supabase.from('horaires').select('id, *');
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -607,7 +624,7 @@ export default function ClassiquesArriveesQuais() {
|
|||||||
}
|
}
|
||||||
if (!stopMatch) continue;
|
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,'');
|
const normPlatform = String(platform || '').toLowerCase().replace(/\s+/g,'');
|
||||||
|
|
||||||
if (targetQuai) {
|
if (targetQuai) {
|
||||||
@@ -625,13 +642,13 @@ export default function ClassiquesArriveesQuais() {
|
|||||||
if (stopIsObject && hasDepartureOnly) continue;
|
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 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);
|
const arrHM = parseTimeHM(rawTime);
|
||||||
if (depHM == null) continue;
|
if (arrHM == null) continue;
|
||||||
|
|
||||||
let minutesUntil = depHM - nowHM;
|
let minutesUntil = arrHM - nowHM;
|
||||||
if (minutesUntil < 0) minutesUntil = depHM + 24*60 - 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) {
|
if (!candidates.length) {
|
||||||
@@ -641,8 +658,8 @@ export default function ClassiquesArriveesQuais() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const todayCandidates = candidates.filter(c => c.circulatesToday && c.depHM > nowHM).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.depHM <= nowHM || !c.circulatesToday)).sort((a,b) => a.depHM - b.depHM);
|
const tomorrowCandidates = candidates.filter(c => c.circulatesTomorrow && (c.arrHM < nowHM - 1 || !c.circulatesToday)).sort((a,b) => a.arrHM - b.arrHM);
|
||||||
|
|
||||||
let chosen = null;
|
let chosen = null;
|
||||||
if (todayCandidates.length > 0) {
|
if (todayCandidates.length > 0) {
|
||||||
@@ -669,7 +686,7 @@ export default function ClassiquesArriveesQuais() {
|
|||||||
|
|
||||||
// Rail is handled purely in CSS for better performance and alignment
|
// Rail is handled purely in CSS for better performance and alignment
|
||||||
const showPlatform = (() => {
|
const showPlatform = (() => {
|
||||||
const sched = selectedHoraire ? selectedHoraire.depHM : null;
|
const sched = selectedHoraire ? selectedHoraire.arrHM : null;
|
||||||
if (sched === null || !stationType) return true;
|
if (sched === null || !stationType) return true;
|
||||||
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
|
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
|
||||||
const threshold = thresholdForStationType(stationType);
|
const threshold = thresholdForStationType(stationType);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import './ClassiquesDepartsQuais.css';
|
import './ClassiquesDepartsQuais.css';
|
||||||
import supabase from '../../../../lib/supabase';
|
import supabase from '../../../../lib/supabase';
|
||||||
|
import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime';
|
||||||
|
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
|
||||||
function Clock() {
|
function Clock() {
|
||||||
const [now, setNow] = useState(new Date());
|
const [now, setNow] = useState(new Date());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -20,6 +22,13 @@ function Clock() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ClassiquesDepartsQuais() {
|
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 = {
|
const mock = {
|
||||||
time: '16h44',
|
time: '16h44',
|
||||||
status: "à l'heure",
|
status: "à l'heure",
|
||||||
@@ -38,6 +47,10 @@ export default function ClassiquesDepartsQuais() {
|
|||||||
const [, setCandidatesCount] = useState(0);
|
const [, setCandidatesCount] = useState(0);
|
||||||
const [stationType, setStationType] = useState(null);
|
const [stationType, setStationType] = useState(null);
|
||||||
const [currentTime, setCurrentTime] = useState(new Date());
|
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 [alternateDelayPhase, setAlternateDelayPhase] = useState(true);
|
||||||
const [alternateCancelPhase, setAlternateCancelPhase] = useState(true);
|
const [alternateCancelPhase, setAlternateCancelPhase] = useState(true);
|
||||||
|
|
||||||
@@ -172,12 +185,7 @@ export default function ClassiquesDepartsQuais() {
|
|||||||
return 0;
|
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) => {
|
const getStops = (h) => {
|
||||||
if (!h) return [];
|
if (!h) return [];
|
||||||
@@ -489,22 +497,31 @@ export default function ClassiquesDepartsQuais() {
|
|||||||
try {
|
try {
|
||||||
let fetchedType = null;
|
let fetchedType = null;
|
||||||
try {
|
try {
|
||||||
|
let fetchedId = gareIdParam || null;
|
||||||
if (gareIdParam) {
|
if (gareIdParam) {
|
||||||
const { data: g } = await supabase.from('gares').select('type').eq('gare_id', gareIdParam).single();
|
const { data: g } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).single();
|
||||||
if (g) fetchedType = g.type;
|
if (g) {
|
||||||
|
fetchedType = g.type;
|
||||||
|
fetchedId = g.id;
|
||||||
|
}
|
||||||
} else if (gareDisplayParam) {
|
} 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) {
|
if (gdata && gdata.length) {
|
||||||
const exactMatch = gdata.find(g => {
|
const exactMatch = gdata.find(g => {
|
||||||
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
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();
|
const n2 = String(gareDisplayParam || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||||
return n1 === n2;
|
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) {}
|
} catch (e) {}
|
||||||
if (mounted) setStationType(fetchedType);
|
|
||||||
|
|
||||||
const { data: all, error } = await supabase.from('horaires').select('id, *');
|
const { data: all, error } = await supabase.from('horaires').select('id, *');
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -605,7 +622,7 @@ export default function ClassiquesDepartsQuais() {
|
|||||||
}
|
}
|
||||||
if (!stopMatch) continue;
|
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,'');
|
const normPlatform = String(platform || '').toLowerCase().replace(/\s+/g,'');
|
||||||
|
|
||||||
if (targetQuai) {
|
if (targetQuai) {
|
||||||
@@ -639,8 +656,8 @@ export default function ClassiquesDepartsQuais() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const todayCandidates = candidates.filter(c => c.circulatesToday && c.depHM > nowHM).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 || !c.circulatesToday)).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;
|
let chosen = null;
|
||||||
if (todayCandidates.length > 0) {
|
if (todayCandidates.length > 0) {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const horaires = [{ heure_depart: '21:00', numero_train: '123' }];
|
||||||
|
const isArrival = false;
|
||||||
|
const currentTime = new Date();
|
||||||
|
currentTime.setHours(20, 55, 0, 0);
|
||||||
|
|
||||||
|
for (const train of horaires) {
|
||||||
|
const numTrain = train.num_train || train.numero_train || train.numero;
|
||||||
|
const timeStr = isArrival
|
||||||
|
? (train.heure_arrivee || train.arrival_time || train.time || train.heure)
|
||||||
|
: (train.heure_depart || train.departure_time || train.time || train.heure);
|
||||||
|
|
||||||
|
let trainTime;
|
||||||
|
if (typeof timeStr === 'string' && timeStr.length <= 5 && timeStr.includes(':')) {
|
||||||
|
const [hh, mm] = timeStr.split(':');
|
||||||
|
trainTime = new Date(currentTime);
|
||||||
|
trainTime.setHours(parseInt(hh, 10), parseInt(mm, 10), 0, 0);
|
||||||
|
} else {
|
||||||
|
trainTime = new Date(timeStr);
|
||||||
|
}
|
||||||
|
const diffMs = trainTime.getTime() - currentTime.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
console.log(`numTrain=${numTrain}, timeStr=${timeStr}, diffMins=${diffMins}`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const { createClient } = require('@supabase/supabase-js');
|
||||||
|
|
||||||
|
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||||
|
|
||||||
|
const channel = supabase
|
||||||
|
.channel('test_channel')
|
||||||
|
.on(
|
||||||
|
'postgres_changes',
|
||||||
|
{ event: 'INSERT', schema: 'public', table: 'annonces_diffusions' },
|
||||||
|
(payload) => {
|
||||||
|
console.log('Realtime Event Received!', payload);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.subscribe((status) => {
|
||||||
|
console.log('Subscription status:', status);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Waiting for events...');
|
||||||
|
// We will kill this process manually or let it timeout
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Timeout. Exiting.');
|
||||||
|
process.exit(0);
|
||||||
|
}, 10000);
|
||||||
Reference in new issue
Block a user