RELEASE V5.5.0 : Voici les modifications apportées :
-> Implémentation des afficheurs "Sécurité"; "Informations" et "Correspondances" pour les afficheurs AFL FONCTIONNALITÉS EN COURS DE DÉVELOPPEMENT : -> REFONTE DU DESIGN DES AFFICHEURS CLASSIQUES
This commit is contained in:
1 parent
1c83953552
commit
4f3a34b101
24 files changed
+3509
-404
No files matched your search
@@ -0,0 +1,69 @@
|
|||||||
|
# 📋 Changelog / Patchnotes - Afficheurs Ferrovia
|
||||||
|
|
||||||
|
Toutes les modifications notables apportées au projet **Afficheurs Ferrovia** sont consignées dans ce document.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 [Version 4.0.0] - V4 (2026-08-30)
|
||||||
|
|
||||||
|
### 🌟 Nouveautés majeures
|
||||||
|
- **Intégration intégrale du GTFS SNCF officiel :**
|
||||||
|
- Référentiel exhaustif de **3 507 gares et haltes ferroviaires françaises** avec leurs identifiants UIC officiels.
|
||||||
|
- Traitement de **41 847 circulations ferroviaires** (TGV INOUI, OUIGO, TER, INTERCITÉS, INTERCITÉS DE NUIT, TGV Lyria, ICE, Autocars TER).
|
||||||
|
- Prise en charge complète des calendriers de circulation réguliers et des dates d'exceptions.
|
||||||
|
- **Flux Temps Réel SNCF SIRI-Lite & GTFS-RT :**
|
||||||
|
- Connexion directe aux flux temps réel du portail Open Data Transport (`transport.data.gouv.fr`).
|
||||||
|
- Récupération en continu des retards estimés, annulations et causes de perturbations.
|
||||||
|
- Extraction des voies et quais réels de départ et d'arrivée.
|
||||||
|
- **Résolution et recherche intelligente des gares :**
|
||||||
|
- Moteur de normalisation tolérant aux tirets, accents, abréviations et majuscules/minuscules (`cleanStationQuery`).
|
||||||
|
- Système d'alias prioritaires et scoring par tokens pour résoudre instantanément les requêtes comme `Valence-TGV`, `Paris-Montparnasse`, `Lyon-Part-Dieu`, etc.
|
||||||
|
|
||||||
|
### 🛠️ Corrections & Optimisations
|
||||||
|
- **Isolation stricte des quais par gare :**
|
||||||
|
- Suppression de la propagation globale du quai sur tout le trajet d'un train (ex: train multi-arrêts).
|
||||||
|
- La voie affichée correspond strictement à la voie de la gare consultée, et chaque arrêt intermédiaire affiche son propre quai réel.
|
||||||
|
- **Normalisation des horaires $\ge 24\text{h}$ :**
|
||||||
|
- Correction des horaires de trains de nuit (ex: `28h15` normalisé en `04h15` / `04:15`).
|
||||||
|
- **Fiabilité du build :**
|
||||||
|
- Résolution de 100 % des avertissements ESLint (`react-hooks/exhaustive-deps`, `no-unused-vars`).
|
||||||
|
- Compilation de production validée avec un statut `Compiled successfully`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚆 [Version 3.0.0] - V3
|
||||||
|
|
||||||
|
### 🌟 Nouveautés majeures
|
||||||
|
- **Afficheurs multi-styles :**
|
||||||
|
- **AFL (Afficheurs Fluides & Lumineux) :** Écrans haute lisibilité modernes bleus et verts pour halls et quais.
|
||||||
|
- **Classiques :** Écrans d'information voyageurs traditionnels (versions Départs, Arrivées et Quai).
|
||||||
|
- **EVA :** Écrans vidéo voyageurs enrichis.
|
||||||
|
- **Module d'annonces sonores :**
|
||||||
|
- Déclenchement automatique des jingles et annonces vocales lors de modifications d'horaires, retards ou départs imminents.
|
||||||
|
- **Support des tranches et trains combinés :**
|
||||||
|
- Affichage élégant des destinations multiples et des séparations de rames en cours de route.
|
||||||
|
|
||||||
|
### 🛠️ Corrections & Optimisations
|
||||||
|
- Défilement fluide des arrêts intermédiaires sur les écrans de quai.
|
||||||
|
- Gestion des travaux et des substitutions par autocar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚉 [Version 2.0.0] - V2
|
||||||
|
|
||||||
|
### 🌟 Nouveautés majeures
|
||||||
|
- **Écrans dédiés par Quai / Voie :**
|
||||||
|
- Ajout des vues `ClassiquesDepartsQuais` et `ClassiquesArriveesQuais` avec affichage grand format du prochain départ/arrivée.
|
||||||
|
- Liste détaillée des arrêts intermédiaires desservis en aval.
|
||||||
|
- **Synchronisation dynamique :**
|
||||||
|
- Intégration de la base Supabase avec synchronisation périodique automatique sans rechargement de page.
|
||||||
|
- Alternance visuelle entre heure prévue, état de retard et informations de perturbation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏁 [Version 1.0.0] - V1
|
||||||
|
|
||||||
|
### 🌟 Nouveautés initiales
|
||||||
|
- Création de la plateforme d'affichage ferroviaire inspirée des systèmes PIDS (Passenger Information Display Systems).
|
||||||
|
- Affichage des départs et arrivées en temps réel avec horloge synchronisée.
|
||||||
|
- Reproduction fidèle des polices, couleurs et contrastes de la signalétique ferroviaire.
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# Configuration Nginx pour Afficheurs Ferroviaires (React PWA)
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# OPTION 1 : Reverse Proxy vers PM2 / Node.js (port 4010)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name your-domain.com; # Remplacez par votre nom de domaine ou IP
|
||||||
|
|
||||||
|
# Redirection HTTP vers HTTPS (à décommenter en production avec SSL)
|
||||||
|
# return 301 https://$host$request_uri;
|
||||||
|
|
||||||
|
# Compression Gzip
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/xml
|
||||||
|
text/javascript
|
||||||
|
application/json
|
||||||
|
application/javascript
|
||||||
|
application/x-javascript
|
||||||
|
application/xml
|
||||||
|
application/xml+rss
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:4010;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# En-têtes pour WebSockets et Realtime (Supabase / SSE)
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
|
||||||
|
# En-têtes standards
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_read_timeout 86400s;
|
||||||
|
proxy_send_timeout 86400s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# OPTION 2 : Service direct des fichiers statiques (React Build SPA)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# server {
|
||||||
|
# listen 80;
|
||||||
|
# listen [::]:80;
|
||||||
|
# server_name your-domain.com;
|
||||||
|
#
|
||||||
|
# # Chemin absolu vers le dossier build de l'application
|
||||||
|
# root /var/www/afficheurs-ferrovia/build;
|
||||||
|
# index index.html;
|
||||||
|
#
|
||||||
|
# # Compression Gzip
|
||||||
|
# gzip on;
|
||||||
|
# gzip_vary on;
|
||||||
|
# gzip_min_length 1024;
|
||||||
|
# gzip_proxied any;
|
||||||
|
# gzip_comp_level 6;
|
||||||
|
# gzip_types
|
||||||
|
# text/plain
|
||||||
|
# text/css
|
||||||
|
# text/xml
|
||||||
|
# text/javascript
|
||||||
|
# application/json
|
||||||
|
# application/javascript
|
||||||
|
# application/x-javascript
|
||||||
|
# application/xml
|
||||||
|
# application/xml+rss
|
||||||
|
# image/svg+xml;
|
||||||
|
#
|
||||||
|
# # Routage SPA React (indispensable pour /afficheurs/*, /departs, /arrivees, etc.)
|
||||||
|
# location / {
|
||||||
|
# try_files $uri $uri/ /index.html;
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# # Cache pour les assets statiques avec hash (JS/CSS)
|
||||||
|
# location ~* \.(?:css|js)$ {
|
||||||
|
# expires 1y;
|
||||||
|
# access_log off;
|
||||||
|
# add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# # Cache pour les médias, polices et images
|
||||||
|
# location ~* \.(?:ico|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot|mp3|wav)$ {
|
||||||
|
# expires 30d;
|
||||||
|
# access_log off;
|
||||||
|
# add_header Cache-Control "public, max-age=2592000";
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# # Pas de cache pour index.html, manifest.json et service-worker (mise à jour PWA instantanée)
|
||||||
|
# location ~* (?:index\.html|manifest\.json|service-worker\.js)$ {
|
||||||
|
# expires -1;
|
||||||
|
# add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||||
|
# }
|
||||||
|
# }
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
@@ -16,6 +16,29 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
|||||||
const [manualPlatform, setManualPlatform] = useState('');
|
const [manualPlatform, setManualPlatform] = useState('');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// État des pages sélectionnées pour les afficheurs AFL
|
||||||
|
const [aflPages, setAflPages] = useState({
|
||||||
|
horaires: true,
|
||||||
|
infos: false,
|
||||||
|
securite1: false,
|
||||||
|
securite2: false,
|
||||||
|
corresp: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleAflPage = (key) => {
|
||||||
|
setAflPages(prev => ({ ...prev, [key]: !prev[key] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAllAflPages = (all) => {
|
||||||
|
setAflPages({
|
||||||
|
horaires: true,
|
||||||
|
infos: all,
|
||||||
|
securite1: all,
|
||||||
|
securite2: all,
|
||||||
|
corresp: all
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Decode and format station name for display
|
// Decode and format station name for display
|
||||||
const decodedGare = gareParam
|
const decodedGare = gareParam
|
||||||
? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ')
|
? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ')
|
||||||
@@ -49,7 +72,19 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
|||||||
const handleGeneral = () => {
|
const handleGeneral = () => {
|
||||||
if (typeof navigate === 'function') {
|
if (typeof navigate === 'function') {
|
||||||
const kebab = gareParam ? String(gareParam).trim() : '';
|
const kebab = gareParam ? String(gareParam).trim() : '';
|
||||||
navigate(`${prefix}/afficheurs/${modeParam}/${typeParam}?gare=${encodeURIComponent(kebab)}`);
|
let url = `${prefix}/afficheurs/${modeParam}/${typeParam}?gare=${encodeURIComponent(kebab)}`;
|
||||||
|
|
||||||
|
if (typeParam === 'afl') {
|
||||||
|
const activeKeys = Object.entries(aflPages)
|
||||||
|
.filter(([_, active]) => active)
|
||||||
|
.map(([key]) => key);
|
||||||
|
|
||||||
|
if (activeKeys.length > 0) {
|
||||||
|
url += `&pages=${activeKeys.join(',')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(url);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,9 +100,16 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSinglePage = (pageKey) => {
|
||||||
|
if (typeof navigate === 'function') {
|
||||||
|
const kebab = gareParam ? String(gareParam).trim() : '';
|
||||||
|
navigate(`${prefix}/afficheurs/${modeParam}/${typeParam}?gare=${encodeURIComponent(kebab)}&page=${pageKey}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
maxWidth: '650px',
|
maxWidth: '780px',
|
||||||
margin: '40px auto',
|
margin: '40px auto',
|
||||||
padding: '32px',
|
padding: '32px',
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
@@ -102,7 +144,7 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
|||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
lineHeight: 1.5
|
lineHeight: 1.5
|
||||||
}}>
|
}}>
|
||||||
Pour la gare de <strong>{decodedGare}</strong> en mode <strong>{modeParam === 'arrivees' ? 'Arrivées' : 'Départs'}</strong>.
|
Pour la gare de <strong>{decodedGare}</strong> en mode <strong>{modeParam === 'arrivees' ? 'Arrivées' : 'Départs'}</strong> (Style : <strong>{typeParam.toUpperCase()}</strong>).
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px', marginBottom: '32px' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px', marginBottom: '32px' }}>
|
||||||
@@ -117,11 +159,112 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
|||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
background: '#fafbfc'
|
background: '#fafbfc'
|
||||||
}}>
|
}}>
|
||||||
<div>
|
<div style={{ width: '100%' }}>
|
||||||
<h3 style={{ margin: '0 0 12px 0', color: '#00439e', fontSize: '20px' }}>Afficheur Général</h3>
|
<h3 style={{ margin: '0 0 12px 0', color: '#00439e', fontSize: '20px' }}>Afficheur Général</h3>
|
||||||
<p style={{ margin: '0 0 20px 0', color: '#666', fontSize: '14px', lineHeight: 1.4 }}>
|
<p style={{ margin: '0 0 16px 0', color: '#666', fontSize: '14px', lineHeight: 1.4 }}>
|
||||||
Affiche la liste complète de tous les trains programmés pour cette gare.
|
Affiche la liste des trains programmés avec possibilité d'inclure les pages d'informations et services.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{typeParam === 'afl' && (
|
||||||
|
<div style={{
|
||||||
|
background: '#ffffff',
|
||||||
|
border: '1px solid #d0e1fd',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '14px',
|
||||||
|
marginBottom: '20px'
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
|
||||||
|
<span style={{ fontSize: '13px', fontWeight: 700, color: '#002257' }}>
|
||||||
|
Pages à inclure dans la rotation :
|
||||||
|
</span>
|
||||||
|
<div style={{ display: 'flex', gap: '6px' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => selectAllAflPages(true)}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
color: '#00439e',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: 600,
|
||||||
|
cursor: 'pointer',
|
||||||
|
textDecoration: 'underline'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tout cocher
|
||||||
|
</button>
|
||||||
|
<span style={{ color: '#ccc', fontSize: '11px' }}>|</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => selectAllAflPages(false)}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
color: '#666',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: 600,
|
||||||
|
cursor: 'pointer',
|
||||||
|
textDecoration: 'underline'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Horaires seuls
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px', color: '#222', cursor: 'pointer' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={aflPages.horaires}
|
||||||
|
onChange={() => toggleAflPage('horaires')}
|
||||||
|
style={{ width: '16px', height: '16px', accentColor: '#00439e' }}
|
||||||
|
/>
|
||||||
|
<span><strong>Page 1 & 2</strong> (Horaires des trains)</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px', color: '#222', cursor: 'pointer' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={aflPages.infos}
|
||||||
|
onChange={() => toggleAflPage('infos')}
|
||||||
|
style={{ width: '16px', height: '16px', accentColor: '#00439e' }}
|
||||||
|
/>
|
||||||
|
<span><strong>Informations</strong> (Trafic & Travaux)</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px', color: '#222', cursor: 'pointer' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={aflPages.securite1}
|
||||||
|
onChange={() => toggleAflPage('securite1')}
|
||||||
|
style={{ width: '16px', height: '16px', accentColor: '#00439e' }}
|
||||||
|
/>
|
||||||
|
<span><strong>Sécurité 1</strong> (Bagages / Objets délaissés)</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px', color: '#222', cursor: 'pointer' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={aflPages.securite2}
|
||||||
|
onChange={() => toggleAflPage('securite2')}
|
||||||
|
style={{ width: '16px', height: '16px', accentColor: '#00439e' }}
|
||||||
|
/>
|
||||||
|
<span><strong>Sécurité 2</strong> (Vigipirate / Alerte 3117)</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px', color: '#222', cursor: 'pointer' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={aflPages.corresp}
|
||||||
|
onChange={() => toggleAflPage('corresp')}
|
||||||
|
style={{ width: '16px', height: '16px', accentColor: '#00439e' }}
|
||||||
|
/>
|
||||||
|
<span><strong>Correspondances</strong> (Transports urbains & bus)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleGeneral}
|
onClick={handleGeneral}
|
||||||
@@ -171,7 +314,7 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
|||||||
value={selectedPlatform}
|
value={selectedPlatform}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSelectedPlatform(e.target.value);
|
setSelectedPlatform(e.target.value);
|
||||||
setManualPlatform(''); // Reset manual entry on selection
|
setManualPlatform('');
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -234,6 +377,56 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{typeParam === 'afl' && (
|
||||||
|
<div style={{
|
||||||
|
borderTop: '1px solid #eef2f8',
|
||||||
|
paddingTop: '20px',
|
||||||
|
marginBottom: '20px'
|
||||||
|
}}>
|
||||||
|
<h4 style={{ margin: '0 0 10px 0', fontSize: '14px', color: '#555', fontWeight: 600 }}>
|
||||||
|
Accès direct à une page AFL spécifique (fixe) :
|
||||||
|
</h4>
|
||||||
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSinglePage('1')}
|
||||||
|
style={{ padding: '6px 12px', fontSize: '12px', borderRadius: '4px', border: '1px solid #ccc', background: '#fff', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Horaires (Page 1)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSinglePage('2')}
|
||||||
|
style={{ padding: '6px 12px', fontSize: '12px', borderRadius: '4px', border: '1px solid #ccc', background: '#fff', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Horaires (Page 2)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSinglePage('infos')}
|
||||||
|
style={{ padding: '6px 12px', fontSize: '12px', borderRadius: '4px', border: '1px solid #ccc', background: '#fff', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Informations
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSinglePage('securite1')}
|
||||||
|
style={{ padding: '6px 12px', fontSize: '12px', borderRadius: '4px', border: '1px solid #ccc', background: '#fff', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Sécurité 1
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSinglePage('securite2')}
|
||||||
|
style={{ padding: '6px 12px', fontSize: '12px', borderRadius: '4px', border: '1px solid #ccc', background: '#fff', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Sécurité 2
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSinglePage('corresp')}
|
||||||
|
style={{ padding: '6px 12px', fontSize: '12px', borderRadius: '4px', border: '1px solid #ccc', background: '#fff', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Correspondances
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
borderTop: '1px solid #eeeeee',
|
borderTop: '1px solid #eeeeee',
|
||||||
paddingTop: '20px',
|
paddingTop: '20px',
|
||||||
@@ -255,3 +448,4 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,9 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
|||||||
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
|
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
|
||||||
import { getSourceFromPath } from '../../../services/railwayService';
|
import { getSourceFromPath } from '../../../services/railwayService';
|
||||||
import { getSncfTimetable } from '../../../services/sncfApi';
|
import { getSncfTimetable } from '../../../services/sncfApi';
|
||||||
|
import AFLSecurityPage from './components/AFLSecurityPage';
|
||||||
|
import AFLCorrespondancesPage from './components/AFLCorrespondancesPage';
|
||||||
|
import AFLInformationsPage from './components/AFLInformationsPage';
|
||||||
|
|
||||||
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
||||||
const formatTimeNoSeconds = (v) => {
|
const formatTimeNoSeconds = (v) => {
|
||||||
@@ -132,11 +135,35 @@ export default function AFLArrivees(){
|
|||||||
// gérer les +, décoder et remplacer les tirets par des espaces
|
// gérer les +, décoder et remplacer les tirets par des espaces
|
||||||
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
||||||
|
|
||||||
// Support pour ?page=1, ?page=2 pour verrouiller l'affichage sur une page spécifique
|
// Support pour verrouiller l'affichage sur une page spécifique via l'URL (?page=...)
|
||||||
const pageParam = params.get('page');
|
// ?page=1 -> Horaires Page 1
|
||||||
const lockedPageIndex = pageParam !== null && !isNaN(parseInt(pageParam, 10))
|
// ?page=2 -> Horaires Page 2
|
||||||
? Math.max(0, parseInt(pageParam, 10) - 1)
|
// ?page=3 | ?page=info | ?page=informations -> Informations
|
||||||
: null;
|
// ?page=4 | ?page=securite | ?page=securite1 -> Sécurité (Image 2)
|
||||||
|
// ?page=securite2 -> Sécurité (Image 3)
|
||||||
|
// ?page=5 | ?page=correspondances -> Correspondances
|
||||||
|
const rawPageParam = params.get('page') ? String(params.get('page')).toLowerCase().trim() : null;
|
||||||
|
|
||||||
|
const parseLockedStep = (param) => {
|
||||||
|
if (!param) return null;
|
||||||
|
if (param === '1' || param === 'p1' || param === 'horaires1' || param === 'horaires_1') return { type: 'HORAIRES_1' };
|
||||||
|
if (param === '2' || param === 'p2' || param === 'horaires2' || param === 'horaires_2') return { type: 'HORAIRES_2' };
|
||||||
|
if (param === '3' || param === 'info' || param === 'information' || param === 'informations' || param === 'infos') return { type: 'INFORMATIONS' };
|
||||||
|
if (param === '4' || param === 'securite' || param === 'security' || param === 'safety' || param === 'securite1' || param === 'securite_1') return { type: 'SECURITE', variant: 1 };
|
||||||
|
if (param === 'securite2' || param === 'securite_2' || param === 'vigipirate' || param === '3117') return { type: 'SECURITE', variant: 2 };
|
||||||
|
if (param === '5' || param === 'correspondance' || param === 'correspondances' || param === 'corresp') return { type: 'CORRESPONDANCES' };
|
||||||
|
const n = parseInt(param, 10);
|
||||||
|
if (!isNaN(n)) {
|
||||||
|
if (n === 1) return { type: 'HORAIRES_1' };
|
||||||
|
if (n === 2) return { type: 'HORAIRES_2' };
|
||||||
|
if (n === 3) return { type: 'INFORMATIONS' };
|
||||||
|
if (n === 4) return { type: 'SECURITE', variant: 1 };
|
||||||
|
if (n >= 5) return { type: 'CORRESPONDANCES' };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const lockedStep = parseLockedStep(rawPageParam);
|
||||||
|
|
||||||
// debug flag activable via ?debug=1 or ?debug=true
|
// debug flag activable via ?debug=1 or ?debug=true
|
||||||
const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');
|
const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');
|
||||||
@@ -158,7 +185,7 @@ export default function AFLArrivees(){
|
|||||||
const [perturbations, setPerturbations] = useState([]);
|
const [perturbations, setPerturbations] = useState([]);
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const [stationType, setStationType] = useState(null);
|
const [stationType, setStationType] = useState(null);
|
||||||
const [, setTravaux] = useState([]);
|
const [travaux, setTravaux] = useState([]);
|
||||||
const [travauxSubstitutions, setTravauxSubstitutions] = useState([]);
|
const [travauxSubstitutions, setTravauxSubstitutions] = useState([]);
|
||||||
const [, setLoading] = useState(false);
|
const [, setLoading] = useState(false);
|
||||||
const [, setError] = useState(null);
|
const [, setError] = useState(null);
|
||||||
@@ -685,7 +712,7 @@ export default function AFLArrivees(){
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
setHoraires(formatted.slice(0, 10));
|
setHoraires(formatted);
|
||||||
setAllHoraires(formatted);
|
setAllHoraires(formatted);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -861,30 +888,54 @@ export default function AFLArrivees(){
|
|||||||
const tomorrowStr = localDateStr(tomorrow);
|
const tomorrowStr = localDateStr(tomorrow);
|
||||||
|
|
||||||
const circulatesOn = (h, d) => {
|
const circulatesOn = (h, d) => {
|
||||||
|
if (!h || !d) return false;
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||||
|
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
||||||
|
const targetDay = d.getDay();
|
||||||
|
|
||||||
|
if (Array.isArray(h.dates) && h.dates.length > 0) {
|
||||||
|
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
|
||||||
|
}
|
||||||
|
|
||||||
const customDaysRaw = h.jours_personnalises;
|
const customDaysRaw = h.jours_personnalises;
|
||||||
if (!customDaysRaw) return false;
|
if (customDaysRaw) {
|
||||||
|
let customDays = [];
|
||||||
let customDays = [];
|
if (Array.isArray(customDaysRaw)) {
|
||||||
if (Array.isArray(customDaysRaw)) {
|
customDays = customDaysRaw;
|
||||||
customDays = customDaysRaw;
|
} else if (typeof customDaysRaw === 'string') {
|
||||||
} else if (typeof customDaysRaw === 'string') {
|
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
|
||||||
try {
|
}
|
||||||
customDays = JSON.parse(customDaysRaw);
|
if (Array.isArray(customDays) && customDays.length > 0) {
|
||||||
} catch (e) {
|
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
|
||||||
return false;
|
if (match) return Boolean(match.circule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(customDays)) {
|
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
|
||||||
const match = customDays.find(item => item.date === targetDateStr);
|
const colName = `circule_${dayNames[targetDay]}`;
|
||||||
if (match) {
|
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
|
||||||
return Boolean(match.circule);
|
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
|
||||||
}
|
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
|
||||||
|
if (db && targetDateHyphen < db) return false;
|
||||||
|
if (df && targetDateHyphen > df) return false;
|
||||||
|
return Boolean(h[colName]);
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
if (Array.isArray(h.days) && h.days.length > 0) {
|
||||||
|
if (!h.days.includes(targetDay)) return false;
|
||||||
|
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
|
||||||
|
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleDate = h.date_depart || h.date || h.service_date;
|
||||||
|
if (singleDate) {
|
||||||
|
const sStr = String(singleDate).slice(0, 10);
|
||||||
|
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_*
|
// Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_*
|
||||||
@@ -1011,6 +1062,10 @@ export default function AFLArrivees(){
|
|||||||
let serviceDateStr;
|
let serviceDateStr;
|
||||||
if (todayCandidates && todayCandidates.length > 0) {
|
if (todayCandidates && todayCandidates.length > 0) {
|
||||||
finalList = todayCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
finalList = todayCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
||||||
|
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
|
||||||
|
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999));
|
||||||
|
finalList = [...finalList, ...sortedTomorrow];
|
||||||
|
}
|
||||||
serviceDateStr = todayStr;
|
serviceDateStr = todayStr;
|
||||||
} else {
|
} else {
|
||||||
finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
||||||
@@ -1086,7 +1141,10 @@ export default function AFLArrivees(){
|
|||||||
|
|
||||||
// Helpers pour perturbations (memoized plus haut)
|
// Helpers pour perturbations (memoized plus haut)
|
||||||
|
|
||||||
const [pageIndex, setPageIndex] = useState(lockedPageIndex !== null ? lockedPageIndex : 0);
|
// Gestion du cycle d'alternance des pages :
|
||||||
|
// Horaires P1 -> Horaires P2 (si > 5 trains) -> Informations -> Sécurité & Sûreté (alternance Img 2 / Img 3) -> Correspondances
|
||||||
|
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||||
|
const [securityVariant, setSecurityVariant] = useState(2);
|
||||||
const [alternateDelay, setAlternateDelay] = useState(false);
|
const [alternateDelay, setAlternateDelay] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1096,32 +1154,95 @@ export default function AFLArrivees(){
|
|||||||
return () => clearInterval(delayTimer);
|
return () => clearInterval(delayTimer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getPageBounds = useCallback((pIndex) => {
|
// Construction des étapes de rotation :
|
||||||
if (pIndex === 0) return { start: 0, end: 5 };
|
// Les pages Informations, Sécurité 1 et 2, Correspondances ne sont affichées STRICTEMENT que si demandées dans l'URL (?pages=... ou ?infos=1 etc.)
|
||||||
const start = 5 + (pIndex - 1) * 8;
|
const pagesParam = params.get('pages');
|
||||||
return { start, end: start + 8 };
|
const selectedPagesList = pagesParam ? pagesParam.split(',').map(s => s.trim().toLowerCase()) : null;
|
||||||
}, []);
|
const hasFlag = (key) => params.get(key) === '1' || params.get(key) === 'true';
|
||||||
|
|
||||||
const totalPages = Math.max(1, horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8));
|
const enableP1 = selectedPagesList
|
||||||
|
? (selectedPagesList.includes('p1') || selectedPagesList.includes('horaires') || selectedPagesList.includes('horaires_1') || selectedPagesList.includes('page1'))
|
||||||
|
: (hasFlag('p1') || hasFlag('horaires') || !pagesParam);
|
||||||
|
|
||||||
|
const enableP2 = selectedPagesList
|
||||||
|
? (selectedPagesList.includes('p2') || selectedPagesList.includes('horaires') || selectedPagesList.includes('horaires_2') || selectedPagesList.includes('page2'))
|
||||||
|
: (hasFlag('p2') || hasFlag('horaires') || !pagesParam);
|
||||||
|
|
||||||
|
const enableInfos = selectedPagesList
|
||||||
|
? (selectedPagesList.includes('infos') || selectedPagesList.includes('info') || selectedPagesList.includes('informations') || selectedPagesList.includes('information'))
|
||||||
|
: (hasFlag('infos') || hasFlag('info') || hasFlag('informations') || hasFlag('information'));
|
||||||
|
|
||||||
|
const enableSecurite1 = selectedPagesList
|
||||||
|
? (selectedPagesList.includes('securite1') || selectedPagesList.includes('securite_1') || (selectedPagesList.includes('securite') && !selectedPagesList.includes('securite2')))
|
||||||
|
: (hasFlag('securite1') || hasFlag('securite_1') || (hasFlag('securite') && !hasFlag('securite2')));
|
||||||
|
|
||||||
|
const enableSecurite2 = selectedPagesList
|
||||||
|
? (selectedPagesList.includes('securite2') || selectedPagesList.includes('securite_2') || selectedPagesList.includes('vigipirate') || selectedPagesList.includes('3117'))
|
||||||
|
: (hasFlag('securite2') || hasFlag('securite_2') || hasFlag('vigipirate') || hasFlag('3117'));
|
||||||
|
|
||||||
|
const enableCorresp = selectedPagesList
|
||||||
|
? (selectedPagesList.includes('corresp') || selectedPagesList.includes('correspondance') || selectedPagesList.includes('correspondances'))
|
||||||
|
: (hasFlag('corresp') || hasFlag('correspondance') || hasFlag('correspondances'));
|
||||||
|
|
||||||
|
const cycleSteps = [];
|
||||||
|
if (enableP1) cycleSteps.push({ type: 'HORAIRES_1' });
|
||||||
|
if (enableP2 && horaires.length > 5) cycleSteps.push({ type: 'HORAIRES_2' });
|
||||||
|
if (enableInfos) cycleSteps.push({ type: 'INFORMATIONS' });
|
||||||
|
if (enableSecurite1) cycleSteps.push({ type: 'SECURITE', variant: 1 });
|
||||||
|
if (enableSecurite2) cycleSteps.push({ type: 'SECURITE', variant: 2 });
|
||||||
|
if (enableCorresp) cycleSteps.push({ type: 'CORRESPONDANCES' });
|
||||||
|
|
||||||
|
if (cycleSteps.length === 0) {
|
||||||
|
cycleSteps.push({ type: 'HORAIRES_1' });
|
||||||
|
if (horaires.length > 5) cycleSteps.push({ type: 'HORAIRES_2' });
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lockedPageIndex !== null) {
|
if (lockedStep !== null) return;
|
||||||
setPageIndex(Math.min(lockedPageIndex, totalPages - 1));
|
if (cycleSteps.length <= 1) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pageInterval = setInterval(() => {
|
|
||||||
setPageIndex((prev) => {
|
|
||||||
const tot = horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8);
|
|
||||||
if (tot <= 1) return 0;
|
|
||||||
return (prev + 1) % tot;
|
|
||||||
});
|
|
||||||
}, 10000); // Change page every 10 seconds
|
|
||||||
return () => clearInterval(pageInterval);
|
|
||||||
}, [horaires.length, lockedPageIndex, totalPages]);
|
|
||||||
|
|
||||||
const { start, end } = getPageBounds(pageIndex);
|
const pageInterval = setInterval(() => {
|
||||||
const displayedHoraires = horaires.slice(start, end);
|
setCurrentStepIndex((prev) => (prev + 1) % cycleSteps.length);
|
||||||
const isPage1 = pageIndex === 0;
|
}, 10000); // 10 secondes par page
|
||||||
|
|
||||||
|
return () => clearInterval(pageInterval);
|
||||||
|
}, [cycleSteps.length, lockedStep]);
|
||||||
|
|
||||||
|
const activeStep = lockedStep || cycleSteps[currentStepIndex % cycleSteps.length] || { type: 'HORAIRES_1' };
|
||||||
|
|
||||||
|
const effectiveGareName = gareDisplay || gareParam || (horaires[0]?.gare_arrivee_nom) || (horaires[0]?.gare_arrivee) || (horaires[0]?.nom_gare) || (horaires[0]?.station_name) || '';
|
||||||
|
|
||||||
|
if (activeStep.type === 'INFORMATIONS') {
|
||||||
|
return (
|
||||||
|
<AFLInformationsPage
|
||||||
|
mode="arrivees"
|
||||||
|
gareName={effectiveGareName}
|
||||||
|
perturbations={perturbations}
|
||||||
|
travaux={travaux}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeStep.type === 'SECURITE') {
|
||||||
|
return (
|
||||||
|
<AFLSecurityPage
|
||||||
|
variant={activeStep.variant || securityVariant}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeStep.type === 'CORRESPONDANCES') {
|
||||||
|
return (
|
||||||
|
<AFLCorrespondancesPage
|
||||||
|
gareName={effectiveGareName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPage1 = activeStep.type === 'HORAIRES_1';
|
||||||
|
const displayedHoraires = isPage1 ? horaires.slice(0, 5) : horaires.slice(5, 13);
|
||||||
|
const totalPages = horaires.length <= 5 ? 1 : 2;
|
||||||
|
const pageIndex = isPage1 ? 0 : 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="afl-arr-container">
|
<div className="afl-arr-container">
|
||||||
@@ -1178,26 +1299,8 @@ export default function AFLArrivees(){
|
|||||||
''
|
''
|
||||||
).toString().trim();
|
).toString().trim();
|
||||||
|
|
||||||
const isCG = rawPlatform.toUpperCase() === 'CG' ||
|
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
|
||||||
rawPlatform.toUpperCase().includes('CG') ||
|
const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
|
||||||
String(horaire.quai || '').toUpperCase().includes('CG') ||
|
|
||||||
String(horaire.quai_reel || '').toUpperCase().includes('CG') ||
|
|
||||||
String(horaire.quai_prevu || '').toUpperCase().includes('CG') ||
|
|
||||||
String(horaire.platform || '').toUpperCase().includes('CG');
|
|
||||||
|
|
||||||
const isCarTER = isCG ||
|
|
||||||
op.includes('CAR') ||
|
|
||||||
op.includes('AUTOCAR') ||
|
|
||||||
op.includes('BUS') ||
|
|
||||||
String(horaire.mode || '').toUpperCase().includes('BUS') ||
|
|
||||||
String(horaire.mode || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.mode_transport || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.mode_transport || '').toUpperCase().includes('BUS') ||
|
|
||||||
String(horaire.type_train || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.type_train || '').toUpperCase().includes('BUS') ||
|
|
||||||
String(horaire.libelle_type_train || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.libelle_type_train || '').toUpperCase().includes('BUS') ||
|
|
||||||
(travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
|
|
||||||
|
|
||||||
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure || '');
|
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure || '');
|
||||||
const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue';
|
const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue';
|
||||||
@@ -1207,7 +1310,7 @@ export default function AFLArrivees(){
|
|||||||
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
||||||
|
|
||||||
const isSuppressed = status && status.label && String(status.label).toLowerCase().includes('supprimé');
|
const isSuppressed = status && status.label && String(status.label).toLowerCase().includes('supprimé');
|
||||||
let showPlatform = !isSuppressed && Boolean(rawPlatform) && !isCG;
|
let showPlatform = !isSuppressed && Boolean(rawPlatform);
|
||||||
|
|
||||||
const isDelayed = status && status.label && String(status.label).toLowerCase().includes('retard');
|
const isDelayed = status && status.label && String(status.label).toLowerCase().includes('retard');
|
||||||
const pillClass = isSuppressed ? 'afl-arr-pill--suppressed' : isDelayed ? 'afl-arr-pill--delayed' : 'afl-arr-pill--ontime';
|
const pillClass = isSuppressed ? 'afl-arr-pill--suppressed' : isDelayed ? 'afl-arr-pill--delayed' : 'afl-arr-pill--ontime';
|
||||||
|
|||||||
@@ -460,31 +460,55 @@ const AFLArriveesQuais = () => {
|
|||||||
const tomorrowStr = localDateStr(tomorrow);
|
const tomorrowStr = localDateStr(tomorrow);
|
||||||
|
|
||||||
const circulatesOn = (h, d) => {
|
const circulatesOn = (h, d) => {
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
if (!h || !d) return false;
|
||||||
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||||
|
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
||||||
|
const targetDay = d.getDay();
|
||||||
|
|
||||||
const customDaysRaw = h.jours_personnalises;
|
if (Array.isArray(h.dates) && h.dates.length > 0) {
|
||||||
if (!customDaysRaw) return false;
|
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
|
||||||
|
}
|
||||||
|
|
||||||
let customDays = [];
|
const customDaysRaw = h.jours_personnalises;
|
||||||
if (Array.isArray(customDaysRaw)) {
|
if (customDaysRaw) {
|
||||||
customDays = customDaysRaw;
|
let customDays = [];
|
||||||
} else if (typeof customDaysRaw === 'string') {
|
if (Array.isArray(customDaysRaw)) {
|
||||||
try {
|
customDays = customDaysRaw;
|
||||||
customDays = JSON.parse(customDaysRaw);
|
} else if (typeof customDaysRaw === 'string') {
|
||||||
} catch (e) {
|
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
|
||||||
return false;
|
}
|
||||||
}
|
if (Array.isArray(customDays) && customDays.length > 0) {
|
||||||
}
|
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
|
||||||
|
if (match) return Boolean(match.circule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(customDays)) {
|
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
|
||||||
const match = customDays.find(item => item.date === targetDateStr);
|
const colName = `circule_${dayNames[targetDay]}`;
|
||||||
if (match) {
|
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
|
||||||
return Boolean(match.circule);
|
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
|
||||||
}
|
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
|
||||||
}
|
if (db && targetDateHyphen < db) return false;
|
||||||
return false;
|
if (df && targetDateHyphen > df) return false;
|
||||||
};
|
return Boolean(h[colName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(h.days) && h.days.length > 0) {
|
||||||
|
if (!h.days.includes(targetDay)) return false;
|
||||||
|
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
|
||||||
|
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleDate = h.date_depart || h.date || h.service_date;
|
||||||
|
if (singleDate) {
|
||||||
|
const sStr = String(singleDate).slice(0, 10);
|
||||||
|
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const parseDateOnly = (v) => {
|
const parseDateOnly = (v) => {
|
||||||
if (!v) return null;
|
if (!v) return null;
|
||||||
@@ -608,6 +632,10 @@ const AFLArriveesQuais = () => {
|
|||||||
let serviceDateStr = '';
|
let serviceDateStr = '';
|
||||||
if (todayCandidates.length > 0) {
|
if (todayCandidates.length > 0) {
|
||||||
finalList = todayCandidates.slice().sort((a, b) => a._depHM - b._depHM);
|
finalList = todayCandidates.slice().sort((a, b) => a._depHM - b._depHM);
|
||||||
|
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
|
||||||
|
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999));
|
||||||
|
finalList = [...finalList, ...sortedTomorrow];
|
||||||
|
}
|
||||||
serviceDateStr = todayStr;
|
serviceDateStr = todayStr;
|
||||||
} else {
|
} else {
|
||||||
finalList = tomorrowCandidates.slice().sort((a, b) => a._depHM - b._depHM);
|
finalList = tomorrowCandidates.slice().sort((a, b) => a._depHM - b._depHM);
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
|||||||
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
|
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
|
||||||
import { getSourceFromPath } from '../../../services/railwayService';
|
import { getSourceFromPath } from '../../../services/railwayService';
|
||||||
import { getSncfTimetable } from '../../../services/sncfApi';
|
import { getSncfTimetable } from '../../../services/sncfApi';
|
||||||
|
import AFLSecurityPage from './components/AFLSecurityPage';
|
||||||
|
import AFLCorrespondancesPage from './components/AFLCorrespondancesPage';
|
||||||
|
import AFLInformationsPage from './components/AFLInformationsPage';
|
||||||
|
|
||||||
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
||||||
const formatTimeNoSeconds = (v) => {
|
const formatTimeNoSeconds = (v) => {
|
||||||
@@ -135,11 +138,35 @@ export default function AFLDeparts(){
|
|||||||
// gérer les +, décoder et remplacer les tirets par des espaces
|
// gérer les +, décoder et remplacer les tirets par des espaces
|
||||||
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
||||||
|
|
||||||
// Support pour ?page=1, ?page=2 pour verrouiller l'affichage sur une page spécifique
|
// Support pour verrouiller l'affichage sur une page spécifique via l'URL (?page=...)
|
||||||
const pageParam = params.get('page');
|
// ?page=1 -> Horaires Page 1
|
||||||
const lockedPageIndex = pageParam !== null && !isNaN(parseInt(pageParam, 10))
|
// ?page=2 -> Horaires Page 2
|
||||||
? Math.max(0, parseInt(pageParam, 10) - 1)
|
// ?page=3 | ?page=info | ?page=informations -> Informations
|
||||||
: null;
|
// ?page=4 | ?page=securite | ?page=securite1 -> Sécurité (Image 2)
|
||||||
|
// ?page=securite2 -> Sécurité (Image 3)
|
||||||
|
// ?page=5 | ?page=correspondances -> Correspondances
|
||||||
|
const rawPageParam = params.get('page') ? String(params.get('page')).toLowerCase().trim() : null;
|
||||||
|
|
||||||
|
const parseLockedStep = (param) => {
|
||||||
|
if (!param) return null;
|
||||||
|
if (param === '1' || param === 'p1' || param === 'horaires1' || param === 'horaires_1') return { type: 'HORAIRES_1' };
|
||||||
|
if (param === '2' || param === 'p2' || param === 'horaires2' || param === 'horaires_2') return { type: 'HORAIRES_2' };
|
||||||
|
if (param === '3' || param === 'info' || param === 'information' || param === 'informations' || param === 'infos') return { type: 'INFORMATIONS' };
|
||||||
|
if (param === '4' || param === 'securite' || param === 'security' || param === 'safety' || param === 'securite1' || param === 'securite_1') return { type: 'SECURITE', variant: 1 };
|
||||||
|
if (param === 'securite2' || param === 'securite_2' || param === 'vigipirate' || param === '3117') return { type: 'SECURITE', variant: 2 };
|
||||||
|
if (param === '5' || param === 'correspondance' || param === 'correspondances' || param === 'corresp') return { type: 'CORRESPONDANCES' };
|
||||||
|
const n = parseInt(param, 10);
|
||||||
|
if (!isNaN(n)) {
|
||||||
|
if (n === 1) return { type: 'HORAIRES_1' };
|
||||||
|
if (n === 2) return { type: 'HORAIRES_2' };
|
||||||
|
if (n === 3) return { type: 'INFORMATIONS' };
|
||||||
|
if (n === 4) return { type: 'SECURITE', variant: 1 };
|
||||||
|
if (n >= 5) return { type: 'CORRESPONDANCES' };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const lockedStep = parseLockedStep(rawPageParam);
|
||||||
|
|
||||||
// debug flag activable via ?debug=1 or ?debug=true
|
// debug flag activable via ?debug=1 or ?debug=true
|
||||||
const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');
|
const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');
|
||||||
@@ -164,7 +191,7 @@ export default function AFLDeparts(){
|
|||||||
// type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)
|
// type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const [stationType, setStationType] = useState(null);
|
const [stationType, setStationType] = useState(null);
|
||||||
const [, setTravaux] = useState([]);
|
const [travaux, setTravaux] = useState([]);
|
||||||
const [travauxSubstitutions, setTravauxSubstitutions] = useState([]);
|
const [travauxSubstitutions, setTravauxSubstitutions] = useState([]);
|
||||||
const [, setLoading] = useState(false);
|
const [, setLoading] = useState(false);
|
||||||
const [, setError] = useState(null);
|
const [, setError] = useState(null);
|
||||||
@@ -695,7 +722,7 @@ export default function AFLDeparts(){
|
|||||||
causeDetails: t.cause_retard || null
|
causeDetails: t.cause_retard || null
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
setHoraires(formatted.slice(0, 10));
|
setHoraires(formatted);
|
||||||
setAllHoraires(formatted);
|
setAllHoraires(formatted);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -886,30 +913,54 @@ export default function AFLDeparts(){
|
|||||||
const tomorrowStr = localDateStr(tomorrow);
|
const tomorrowStr = localDateStr(tomorrow);
|
||||||
|
|
||||||
const circulatesOn = (h, d) => {
|
const circulatesOn = (h, d) => {
|
||||||
|
if (!h || !d) return false;
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||||
|
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
||||||
|
const targetDay = d.getDay();
|
||||||
|
|
||||||
|
if (Array.isArray(h.dates) && h.dates.length > 0) {
|
||||||
|
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
|
||||||
|
}
|
||||||
|
|
||||||
const customDaysRaw = h.jours_personnalises;
|
const customDaysRaw = h.jours_personnalises;
|
||||||
if (!customDaysRaw) return false;
|
if (customDaysRaw) {
|
||||||
|
let customDays = [];
|
||||||
let customDays = [];
|
if (Array.isArray(customDaysRaw)) {
|
||||||
if (Array.isArray(customDaysRaw)) {
|
customDays = customDaysRaw;
|
||||||
customDays = customDaysRaw;
|
} else if (typeof customDaysRaw === 'string') {
|
||||||
} else if (typeof customDaysRaw === 'string') {
|
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
|
||||||
try {
|
}
|
||||||
customDays = JSON.parse(customDaysRaw);
|
if (Array.isArray(customDays) && customDays.length > 0) {
|
||||||
} catch (e) {
|
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
|
||||||
return false;
|
if (match) return Boolean(match.circule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(customDays)) {
|
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
|
||||||
const match = customDays.find(item => item.date === targetDateStr);
|
const colName = `circule_${dayNames[targetDay]}`;
|
||||||
if (match) {
|
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
|
||||||
return Boolean(match.circule);
|
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
|
||||||
}
|
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
|
||||||
|
if (db && targetDateHyphen < db) return false;
|
||||||
|
if (df && targetDateHyphen > df) return false;
|
||||||
|
return Boolean(h[colName]);
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
if (Array.isArray(h.days) && h.days.length > 0) {
|
||||||
|
if (!h.days.includes(targetDay)) return false;
|
||||||
|
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
|
||||||
|
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleDate = h.date_depart || h.date || h.service_date;
|
||||||
|
if (singleDate) {
|
||||||
|
const sStr = String(singleDate).slice(0, 10);
|
||||||
|
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_*
|
// Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_*
|
||||||
@@ -1036,6 +1087,10 @@ export default function AFLDeparts(){
|
|||||||
let serviceDateStr;
|
let serviceDateStr;
|
||||||
if (todayCandidates && todayCandidates.length > 0) {
|
if (todayCandidates && todayCandidates.length > 0) {
|
||||||
finalList = todayCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
finalList = todayCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
||||||
|
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
|
||||||
|
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
||||||
|
finalList = [...finalList, ...sortedTomorrow];
|
||||||
|
}
|
||||||
serviceDateStr = todayStr;
|
serviceDateStr = todayStr;
|
||||||
} else {
|
} else {
|
||||||
finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
||||||
@@ -1111,7 +1166,10 @@ export default function AFLDeparts(){
|
|||||||
|
|
||||||
// Helpers pour perturbations (memoized plus haut)
|
// Helpers pour perturbations (memoized plus haut)
|
||||||
|
|
||||||
const [pageIndex, setPageIndex] = useState(lockedPageIndex !== null ? lockedPageIndex : 0);
|
// Gestion du cycle d'alternance des pages :
|
||||||
|
// Horaires P1 -> Horaires P2 (si > 5 trains) -> Informations -> Sécurité & Sûreté (alternance Img 2 / Img 3) -> Correspondances
|
||||||
|
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||||
|
const [securityVariant, setSecurityVariant] = useState(2);
|
||||||
const [alternateDelay, setAlternateDelay] = useState(false);
|
const [alternateDelay, setAlternateDelay] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1121,32 +1179,95 @@ export default function AFLDeparts(){
|
|||||||
return () => clearInterval(delayTimer);
|
return () => clearInterval(delayTimer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getPageBounds = useCallback((pIndex) => {
|
// Construction des étapes de rotation :
|
||||||
if (pIndex === 0) return { start: 0, end: 5 };
|
// Les pages Informations, Sécurité 1 et 2, Correspondances ne sont affichées STRICTEMENT que si demandées dans l'URL (?pages=... ou ?infos=1 etc.)
|
||||||
const start = 5 + (pIndex - 1) * 8;
|
const pagesParam = params.get('pages');
|
||||||
return { start, end: start + 8 };
|
const selectedPagesList = pagesParam ? pagesParam.split(',').map(s => s.trim().toLowerCase()) : null;
|
||||||
}, []);
|
const hasFlag = (key) => params.get(key) === '1' || params.get(key) === 'true';
|
||||||
|
|
||||||
const totalPages = Math.max(1, horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8));
|
const enableP1 = selectedPagesList
|
||||||
|
? (selectedPagesList.includes('p1') || selectedPagesList.includes('horaires') || selectedPagesList.includes('horaires_1') || selectedPagesList.includes('page1'))
|
||||||
|
: (hasFlag('p1') || hasFlag('horaires') || !pagesParam);
|
||||||
|
|
||||||
|
const enableP2 = selectedPagesList
|
||||||
|
? (selectedPagesList.includes('p2') || selectedPagesList.includes('horaires') || selectedPagesList.includes('horaires_2') || selectedPagesList.includes('page2'))
|
||||||
|
: (hasFlag('p2') || hasFlag('horaires') || !pagesParam);
|
||||||
|
|
||||||
|
const enableInfos = selectedPagesList
|
||||||
|
? (selectedPagesList.includes('infos') || selectedPagesList.includes('info') || selectedPagesList.includes('informations') || selectedPagesList.includes('information'))
|
||||||
|
: (hasFlag('infos') || hasFlag('info') || hasFlag('informations') || hasFlag('information'));
|
||||||
|
|
||||||
|
const enableSecurite1 = selectedPagesList
|
||||||
|
? (selectedPagesList.includes('securite1') || selectedPagesList.includes('securite_1') || (selectedPagesList.includes('securite') && !selectedPagesList.includes('securite2')))
|
||||||
|
: (hasFlag('securite1') || hasFlag('securite_1') || (hasFlag('securite') && !hasFlag('securite2')));
|
||||||
|
|
||||||
|
const enableSecurite2 = selectedPagesList
|
||||||
|
? (selectedPagesList.includes('securite2') || selectedPagesList.includes('securite_2') || selectedPagesList.includes('vigipirate') || selectedPagesList.includes('3117'))
|
||||||
|
: (hasFlag('securite2') || hasFlag('securite_2') || hasFlag('vigipirate') || hasFlag('3117'));
|
||||||
|
|
||||||
|
const enableCorresp = selectedPagesList
|
||||||
|
? (selectedPagesList.includes('corresp') || selectedPagesList.includes('correspondance') || selectedPagesList.includes('correspondances'))
|
||||||
|
: (hasFlag('corresp') || hasFlag('correspondance') || hasFlag('correspondances'));
|
||||||
|
|
||||||
|
const cycleSteps = [];
|
||||||
|
if (enableP1) cycleSteps.push({ type: 'HORAIRES_1' });
|
||||||
|
if (enableP2 && horaires.length > 5) cycleSteps.push({ type: 'HORAIRES_2' });
|
||||||
|
if (enableInfos) cycleSteps.push({ type: 'INFORMATIONS' });
|
||||||
|
if (enableSecurite1) cycleSteps.push({ type: 'SECURITE', variant: 1 });
|
||||||
|
if (enableSecurite2) cycleSteps.push({ type: 'SECURITE', variant: 2 });
|
||||||
|
if (enableCorresp) cycleSteps.push({ type: 'CORRESPONDANCES' });
|
||||||
|
|
||||||
|
if (cycleSteps.length === 0) {
|
||||||
|
cycleSteps.push({ type: 'HORAIRES_1' });
|
||||||
|
if (horaires.length > 5) cycleSteps.push({ type: 'HORAIRES_2' });
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lockedPageIndex !== null) {
|
if (lockedStep !== null) return;
|
||||||
setPageIndex(Math.min(lockedPageIndex, totalPages - 1));
|
if (cycleSteps.length <= 1) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pageInterval = setInterval(() => {
|
|
||||||
setPageIndex((prev) => {
|
|
||||||
const tot = horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8);
|
|
||||||
if (tot <= 1) return 0;
|
|
||||||
return (prev + 1) % tot;
|
|
||||||
});
|
|
||||||
}, 10000); // Change page every 10 seconds
|
|
||||||
return () => clearInterval(pageInterval);
|
|
||||||
}, [horaires.length, lockedPageIndex, totalPages]);
|
|
||||||
|
|
||||||
const { start, end } = getPageBounds(pageIndex);
|
const pageInterval = setInterval(() => {
|
||||||
const displayedHoraires = horaires.slice(start, end);
|
setCurrentStepIndex((prev) => (prev + 1) % cycleSteps.length);
|
||||||
const isPage1 = pageIndex === 0;
|
}, 10000); // 10 secondes par page
|
||||||
|
|
||||||
|
return () => clearInterval(pageInterval);
|
||||||
|
}, [cycleSteps.length, lockedStep]);
|
||||||
|
|
||||||
|
const activeStep = lockedStep || cycleSteps[currentStepIndex % cycleSteps.length] || { type: 'HORAIRES_1' };
|
||||||
|
|
||||||
|
const effectiveGareName = gareDisplay || gareParam || (horaires[0]?.gare_depart_nom) || (horaires[0]?.gare_depart) || (horaires[0]?.nom_gare) || (horaires[0]?.station_name) || '';
|
||||||
|
|
||||||
|
if (activeStep.type === 'INFORMATIONS') {
|
||||||
|
return (
|
||||||
|
<AFLInformationsPage
|
||||||
|
mode="departs"
|
||||||
|
gareName={effectiveGareName}
|
||||||
|
perturbations={perturbations}
|
||||||
|
travaux={travaux}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeStep.type === 'SECURITE') {
|
||||||
|
return (
|
||||||
|
<AFLSecurityPage
|
||||||
|
variant={activeStep.variant || securityVariant}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeStep.type === 'CORRESPONDANCES') {
|
||||||
|
return (
|
||||||
|
<AFLCorrespondancesPage
|
||||||
|
gareName={effectiveGareName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPage1 = activeStep.type === 'HORAIRES_1';
|
||||||
|
const displayedHoraires = isPage1 ? horaires.slice(0, 5) : horaires.slice(5, 13);
|
||||||
|
const totalPages = horaires.length <= 5 ? 1 : 2;
|
||||||
|
const pageIndex = isPage1 ? 0 : 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="afl-container">
|
<div className="afl-container">
|
||||||
@@ -1203,26 +1324,8 @@ export default function AFLDeparts(){
|
|||||||
''
|
''
|
||||||
).toString().trim();
|
).toString().trim();
|
||||||
|
|
||||||
const isCG = rawPlatform.toUpperCase() === 'CG' ||
|
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
|
||||||
rawPlatform.toUpperCase().includes('CG') ||
|
const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
|
||||||
String(horaire.quai || '').toUpperCase().includes('CG') ||
|
|
||||||
String(horaire.quai_reel || '').toUpperCase().includes('CG') ||
|
|
||||||
String(horaire.quai_prevu || '').toUpperCase().includes('CG') ||
|
|
||||||
String(horaire.platform || '').toUpperCase().includes('CG');
|
|
||||||
|
|
||||||
const isCarTER = isCG ||
|
|
||||||
op.includes('CAR') ||
|
|
||||||
op.includes('AUTOCAR') ||
|
|
||||||
op.includes('BUS') ||
|
|
||||||
String(horaire.mode || '').toUpperCase().includes('BUS') ||
|
|
||||||
String(horaire.mode || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.mode_transport || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.mode_transport || '').toUpperCase().includes('BUS') ||
|
|
||||||
String(horaire.type_train || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.type_train || '').toUpperCase().includes('BUS') ||
|
|
||||||
String(horaire.libelle_type_train || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.libelle_type_train || '').toUpperCase().includes('BUS') ||
|
|
||||||
(travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
|
|
||||||
|
|
||||||
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
|
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
|
||||||
const destination = horaire.gare_arrivee_nom || '';
|
const destination = horaire.gare_arrivee_nom || '';
|
||||||
@@ -1232,7 +1335,7 @@ export default function AFLDeparts(){
|
|||||||
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
||||||
|
|
||||||
const isSuppressed = status && status.label && String(status.label).toLowerCase().includes('supprimé');
|
const isSuppressed = status && status.label && String(status.label).toLowerCase().includes('supprimé');
|
||||||
let showPlatform = !isSuppressed && Boolean(rawPlatform) && !isCG;
|
let showPlatform = !isSuppressed && Boolean(rawPlatform);
|
||||||
|
|
||||||
const isDelayed = status && status.label && String(status.label).toLowerCase().includes('retard');
|
const isDelayed = status && status.label && String(status.label).toLowerCase().includes('retard');
|
||||||
const pillClass = isSuppressed ? 'afl-pill--suppressed' : isDelayed ? 'afl-pill--delayed' : 'afl-pill--ontime';
|
const pillClass = isSuppressed ? 'afl-pill--suppressed' : isDelayed ? 'afl-pill--delayed' : 'afl-pill--ontime';
|
||||||
|
|||||||
@@ -530,31 +530,55 @@ const AFLDepartsQuais = () => {
|
|||||||
const tomorrowStr = localDateStr(tomorrow);
|
const tomorrowStr = localDateStr(tomorrow);
|
||||||
|
|
||||||
const circulatesOn = (h, d) => {
|
const circulatesOn = (h, d) => {
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
if (!h || !d) return false;
|
||||||
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||||
|
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
||||||
|
const targetDay = d.getDay();
|
||||||
|
|
||||||
const customDaysRaw = h.jours_personnalises;
|
if (Array.isArray(h.dates) && h.dates.length > 0) {
|
||||||
if (!customDaysRaw) return false;
|
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
|
||||||
|
}
|
||||||
|
|
||||||
let customDays = [];
|
const customDaysRaw = h.jours_personnalises;
|
||||||
if (Array.isArray(customDaysRaw)) {
|
if (customDaysRaw) {
|
||||||
customDays = customDaysRaw;
|
let customDays = [];
|
||||||
} else if (typeof customDaysRaw === 'string') {
|
if (Array.isArray(customDaysRaw)) {
|
||||||
try {
|
customDays = customDaysRaw;
|
||||||
customDays = JSON.parse(customDaysRaw);
|
} else if (typeof customDaysRaw === 'string') {
|
||||||
} catch (e) {
|
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
|
||||||
return false;
|
}
|
||||||
}
|
if (Array.isArray(customDays) && customDays.length > 0) {
|
||||||
}
|
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
|
||||||
|
if (match) return Boolean(match.circule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(customDays)) {
|
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
|
||||||
const match = customDays.find(item => item.date === targetDateStr);
|
const colName = `circule_${dayNames[targetDay]}`;
|
||||||
if (match) {
|
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
|
||||||
return Boolean(match.circule);
|
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
|
||||||
}
|
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
|
||||||
}
|
if (db && targetDateHyphen < db) return false;
|
||||||
return false;
|
if (df && targetDateHyphen > df) return false;
|
||||||
};
|
return Boolean(h[colName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(h.days) && h.days.length > 0) {
|
||||||
|
if (!h.days.includes(targetDay)) return false;
|
||||||
|
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
|
||||||
|
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleDate = h.date_depart || h.date || h.service_date;
|
||||||
|
if (singleDate) {
|
||||||
|
const sStr = String(singleDate).slice(0, 10);
|
||||||
|
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const parseDateOnly = (v) => {
|
const parseDateOnly = (v) => {
|
||||||
if (!v) return null;
|
if (!v) return null;
|
||||||
@@ -682,6 +706,10 @@ const AFLDepartsQuais = () => {
|
|||||||
let serviceDateStr = '';
|
let serviceDateStr = '';
|
||||||
if (todayCandidates.length > 0) {
|
if (todayCandidates.length > 0) {
|
||||||
finalList = todayCandidates.slice().sort((a, b) => a._depHM - b._depHM);
|
finalList = todayCandidates.slice().sort((a, b) => a._depHM - b._depHM);
|
||||||
|
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
|
||||||
|
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
||||||
|
finalList = [...finalList, ...sortedTomorrow];
|
||||||
|
}
|
||||||
serviceDateStr = todayStr;
|
serviceDateStr = todayStr;
|
||||||
} else {
|
} else {
|
||||||
finalList = tomorrowCandidates.slice().sort((a, b) => a._depHM - b._depHM);
|
finalList = tomorrowCandidates.slice().sort((a, b) => a._depHM - b._depHM);
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
/* AFL CORRESPONDANCES PAGE - DESIGN STRICT CONFORME A L'IMAGE 1 */
|
||||||
|
|
||||||
|
.afl-corresp-page {
|
||||||
|
font-family: 'Achemine', 'Segoe UI', Roboto, sans-serif !important;
|
||||||
|
background-color: #1f2125 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
position: absolute !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
user-select: none !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-corresp-page * {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
EN-TÊTE GRIS ARDOISE (108px)
|
||||||
|
========================================================================== */
|
||||||
|
.afl-corresp-header {
|
||||||
|
height: 108px !important;
|
||||||
|
min-height: 108px !important;
|
||||||
|
max-height: 108px !important;
|
||||||
|
background-color: #34373d !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
padding: 0 48px !important;
|
||||||
|
border-bottom: 2px solid #4a4e56 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-corresp-header-left {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 22px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-corresp-header-title {
|
||||||
|
font-size: 54px !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
letter-spacing: -0.5px !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-corresp-header-right {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-corresp-clock {
|
||||||
|
font-size: 56px !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
letter-spacing: -0.96px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
LISTE DES LIGNES EN CORRESPONDANCE
|
||||||
|
========================================================================== */
|
||||||
|
.afl-corresp-list {
|
||||||
|
flex: 1 !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
background-color: #1f2125 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-corresp-group {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-corresp-row {
|
||||||
|
height: 108px !important;
|
||||||
|
min-height: 108px !important;
|
||||||
|
max-height: 108px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 0 48px !important;
|
||||||
|
border-bottom: 1.5px solid #3c4048 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Colonne 1 : Bus/Car/Tram + Numéro de ligne */
|
||||||
|
.afl-corresp-col-line {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 14px !important;
|
||||||
|
width: 180px !important;
|
||||||
|
min-width: 180px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-corresp-icon-box {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
width: 58px !important;
|
||||||
|
height: 58px !important;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.4) !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-corresp-line-badge {
|
||||||
|
background-color: #4a4e57 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-size: 32px !important;
|
||||||
|
font-weight: 900 !important;
|
||||||
|
padding: 6px 14px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3) !important;
|
||||||
|
min-width: 65px !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-corresp-line-spacer {
|
||||||
|
width: 180px !important;
|
||||||
|
min-width: 180px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Colonne 2 : Arrêt - Destination */
|
||||||
|
.afl-corresp-col-dest {
|
||||||
|
flex: 1 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
padding-right: 30px !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-corresp-dest-text {
|
||||||
|
font-size: 48px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
letter-spacing: -0.5px !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Colonne 3 : 2 Boîtes des minutes d'attente (Compatibles XXhXX min) */
|
||||||
|
.afl-corresp-col-times {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 20px !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-corresp-time-box {
|
||||||
|
background-color: #d6dbe2 !important;
|
||||||
|
color: #111111 !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
min-width: 155px !important;
|
||||||
|
height: 68px !important;
|
||||||
|
padding: 0 16px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: baseline !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
gap: 4px !important;
|
||||||
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.35) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-corresp-time-val {
|
||||||
|
font-size: 44px !important;
|
||||||
|
font-weight: 900 !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
letter-spacing: -1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-corresp-time-unit {
|
||||||
|
font-size: 24px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
opacity: 0.9 !important;
|
||||||
|
margin-left: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message si liste vide */
|
||||||
|
.afl-corresp-empty {
|
||||||
|
flex: 1 !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
color: #cccccc !important;
|
||||||
|
font-size: 32px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
padding: 40px !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import './AFLCorrespondancesPage.css';
|
||||||
|
import { getCorrespondancesForStation, computeLiveWaitTimes, formatWaitTimeDisplay } from '../../../../services/correspondancesService';
|
||||||
|
|
||||||
|
// Horloge pour l'en-tête de correspondances
|
||||||
|
function CorrespondancesClock({ now }) {
|
||||||
|
const hh = String(now.getHours()).padStart(2, '0');
|
||||||
|
const mm = String(now.getMinutes()).padStart(2, '0');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="afl-corresp-clock" aria-hidden>
|
||||||
|
<span>{hh}:{mm}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icône de Pôle d'échange / Gare Correspondances (bâtiment + trajet en pointillés)
|
||||||
|
const StationTransferIcon = ({ size = '52px', color = '#f5c518' }) => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
fill="none"
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
{/* Bâtiment gare / Pôle d'échange */}
|
||||||
|
<path
|
||||||
|
d="M6 22 L24 8 L42 22 V40 H6 Z"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="3.5"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<rect x="19" y="24" width="10" height="16" stroke={color} strokeWidth="3" />
|
||||||
|
<path d="M12 24 H15 M33 24 H36" stroke={color} strokeWidth="3.5" strokeLinecap="round" />
|
||||||
|
<path d="M12 32 H15 M33 32 H36" stroke={color} strokeWidth="3.5" strokeLinecap="round" />
|
||||||
|
|
||||||
|
{/* Trajet pointillé de correspondance */}
|
||||||
|
<path
|
||||||
|
d="M6 44 H42"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="3"
|
||||||
|
strokeDasharray="4 4"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<circle cx="42" cy="44" r="3.5" fill={color} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Icône de Transport Multimodal (Bus, Car, Tram)
|
||||||
|
const TransitCorrespIcon = ({ type = 'bus', size = '56px', color = '#ffffff' }) => (
|
||||||
|
<div className="afl-corresp-icon-box">
|
||||||
|
{type === 'tram' ? (
|
||||||
|
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M12 2l2 4h-4l2-4z" />
|
||||||
|
<rect x="4" y="6" width="16" height="13" rx="3" />
|
||||||
|
<line x1="4" y1="12" x2="20" y2="12" />
|
||||||
|
<circle cx="8" cy="16" r="1.5" fill={color} />
|
||||||
|
<circle cx="16" cy="16" r="1.5" fill={color} />
|
||||||
|
<line x1="8" y1="19" x2="8" y2="21" strokeWidth="2.4" />
|
||||||
|
<line x1="16" y1="19" x2="16" y2="21" strokeWidth="2.4" />
|
||||||
|
</svg>
|
||||||
|
) : type === 'car' ? (
|
||||||
|
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<rect x="3" y="5" width="18" height="13" rx="3" />
|
||||||
|
<line x1="3" y1="11" x2="21" y2="11" />
|
||||||
|
<circle cx="7" cy="15" r="1.5" fill={color} />
|
||||||
|
<circle cx="17" cy="15" r="1.5" fill={color} />
|
||||||
|
<line x1="6" y1="18" x2="6" y2="20" strokeWidth="2.4" />
|
||||||
|
<line x1="18" y1="18" x2="18" y2="20" strokeWidth="2.4" />
|
||||||
|
<line x1="7" y1="8" x2="17" y2="8" strokeWidth="1.5" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<rect x="4" y="3" width="16" height="16" rx="3" />
|
||||||
|
<path d="M4 11h16" />
|
||||||
|
<circle cx="8" cy="15" r="1.5" fill={color} />
|
||||||
|
<circle cx="16" cy="15" r="1.5" fill={color} />
|
||||||
|
<path d="M6 19v2M18 19v2" strokeWidth="2.4" />
|
||||||
|
<path d="M9 6h6" strokeWidth="2" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function AFLCorrespondancesPage({ gareName = '' }) {
|
||||||
|
const [correspondances, setCorrespondances] = useState([]);
|
||||||
|
const [, setLoading] = useState(true);
|
||||||
|
const [currentTime, setCurrentTime] = useState(new Date());
|
||||||
|
|
||||||
|
// Horloge en direct pour synchronisation exacte des décomptes
|
||||||
|
useEffect(() => {
|
||||||
|
const clockId = setInterval(() => setCurrentTime(new Date()), 1000);
|
||||||
|
return () => clearInterval(clockId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Charger les correspondances aux alentours de la gare affichée
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
try {
|
||||||
|
const list = await getCorrespondancesForStation(gareName);
|
||||||
|
if (mounted) {
|
||||||
|
setCorrespondances(list);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[AFLCorrespondances] Erreur chargement correspondances:', err);
|
||||||
|
if (mounted) setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
|
||||||
|
// Actualiser les correspondances toutes les 30 secondes
|
||||||
|
const refreshTimer = setInterval(loadData, 30000);
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
clearInterval(refreshTimer);
|
||||||
|
};
|
||||||
|
}, [gareName]);
|
||||||
|
|
||||||
|
// Si aucune correspondance n'est disponible
|
||||||
|
if (!correspondances || correspondances.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="afl-corresp-page">
|
||||||
|
<header className="afl-corresp-header">
|
||||||
|
<div className="afl-corresp-header-left">
|
||||||
|
<StationTransferIcon />
|
||||||
|
<div className="afl-corresp-header-title">Correspondances</div>
|
||||||
|
</div>
|
||||||
|
<div className="afl-corresp-header-right">
|
||||||
|
<CorrespondancesClock now={currentTime} />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="afl-corresp-empty">
|
||||||
|
Aucune correspondance directe actuellement signalée dans cette gare.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="afl-corresp-page">
|
||||||
|
{/* EN-TÊTE GRIS ARDOISE */}
|
||||||
|
<header className="afl-corresp-header">
|
||||||
|
<div className="afl-corresp-header-left">
|
||||||
|
<StationTransferIcon />
|
||||||
|
<div className="afl-corresp-header-title">Correspondances</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="afl-corresp-header-right">
|
||||||
|
<CorrespondancesClock now={currentTime} />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* LISTE DES CORRESPONDANCES (Strictement conforme à l'Image 1) */}
|
||||||
|
<div className="afl-corresp-list">
|
||||||
|
{correspondances.map((cGroup) => (
|
||||||
|
<div key={cGroup.id} className="afl-corresp-group">
|
||||||
|
{cGroup.destinations.map((dest, dIdx) => {
|
||||||
|
const isFirstInGroup = dIdx === 0;
|
||||||
|
|
||||||
|
// Calcul en direct à la seconde près des 2 prochains départs (comme Image 1)
|
||||||
|
const times = computeLiveWaitTimes(dest, currentTime).slice(0, 2);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={dIdx} className="afl-corresp-row">
|
||||||
|
{/* COLONNE GAUCHE : Picto Transport et Badge Ligne UNIQUEMENT sur la 1ère destination du groupe */}
|
||||||
|
<div className="afl-corresp-col-line">
|
||||||
|
{isFirstInGroup ? (
|
||||||
|
<>
|
||||||
|
<TransitCorrespIcon type={cGroup.type} size="56px" />
|
||||||
|
<div
|
||||||
|
className="afl-corresp-line-badge"
|
||||||
|
style={{
|
||||||
|
backgroundColor: cGroup.color || '#4a4e57',
|
||||||
|
color: cGroup.textColor || '#ffffff'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cGroup.line}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="afl-corresp-line-spacer" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* COLONNE CENTRE : Arrêt - Destination */}
|
||||||
|
<div className="afl-corresp-col-dest">
|
||||||
|
<span className="afl-corresp-dest-text">{dest.name}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* COLONNE DROITE : 2 Boîtes Minutes d'attente compatibles XXhXX min */}
|
||||||
|
<div className="afl-corresp-col-times">
|
||||||
|
{times.map((wait, wIdx) => {
|
||||||
|
const formatted = formatWaitTimeDisplay(wait);
|
||||||
|
return (
|
||||||
|
<div key={wIdx} className="afl-corresp-time-box">
|
||||||
|
<span className="afl-corresp-time-val">{formatted.val}</span>
|
||||||
|
{formatted.unit && <span className="afl-corresp-time-unit">{formatted.unit}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
/* AFL INFORMATIONS PAGE - DESIGN AUX COULEURS AFL */
|
||||||
|
|
||||||
|
.afl-info-page {
|
||||||
|
font-family: 'Achemine', 'Segoe UI', Roboto, sans-serif !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
position: absolute !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
user-select: none !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-page * {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thème Départs (Bleu) */
|
||||||
|
.afl-info--departs {
|
||||||
|
background-color: #002d62 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info--departs .afl-info-header {
|
||||||
|
background-color: #005fa9 !important;
|
||||||
|
border-bottom: 2px solid #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thème Arrivées (Vert) */
|
||||||
|
.afl-info--arrivees {
|
||||||
|
background-color: #173e31 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info--arrivees .afl-info-header {
|
||||||
|
background-color: #337862 !important;
|
||||||
|
border-bottom: 2px solid #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
EN-TÊTE AFL (108px)
|
||||||
|
========================================================================== */
|
||||||
|
.afl-info-header {
|
||||||
|
height: 108px !important;
|
||||||
|
min-height: 108px !important;
|
||||||
|
max-height: 108px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
padding: 0 48px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-header-left {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 22px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-header-title {
|
||||||
|
font-size: 54px !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
letter-spacing: -0.5px !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-header-right {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-clock {
|
||||||
|
font-size: 56px !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
letter-spacing: -0.96px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
CONTENU PRINCIPAL
|
||||||
|
========================================================================== */
|
||||||
|
.afl-info-content {
|
||||||
|
flex: 1 !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
padding: 36px 60px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grille des cartes en cas de perturbations ou travaux */
|
||||||
|
.afl-info-cards-container {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
gap: 24px !important;
|
||||||
|
height: 100% !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-card {
|
||||||
|
background: rgba(255, 255, 255, 0.08) !important;
|
||||||
|
border-radius: 16px !important;
|
||||||
|
padding: 24px 32px !important;
|
||||||
|
border: 1.5px solid rgba(255, 255, 255, 0.2) !important;
|
||||||
|
backdrop-filter: blur(8px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-card--perturbation {
|
||||||
|
border-left: 10px solid #f7a81b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-card--travaux {
|
||||||
|
border-left: 10px solid #3cd070 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-card-header {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
margin-bottom: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-badge {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 10px !important;
|
||||||
|
font-size: 26px !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
letter-spacing: 0.8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-badge--alert {
|
||||||
|
color: #f7a81b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-badge--travaux {
|
||||||
|
color: #3cd070 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-station-name {
|
||||||
|
font-size: 32px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
opacity: 0.9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-card-body {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
gap: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-perturb-item,
|
||||||
|
.afl-info-travaux-item {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
gap: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-perturb-title,
|
||||||
|
.afl-info-travaux-title {
|
||||||
|
font-size: 38px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-perturb-desc,
|
||||||
|
.afl-info-travaux-desc {
|
||||||
|
font-size: 26px !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
color: #e0e6ed !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
AFFICHAGE NOMINAL (3 CARTES DE SERVICES & CONSEILS)
|
||||||
|
========================================================================== */
|
||||||
|
.afl-info-nominal-container {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
justify-content: space-evenly !important;
|
||||||
|
height: 100% !important;
|
||||||
|
gap: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-nominal-card {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 36px !important;
|
||||||
|
background: rgba(255, 255, 255, 0.08) !important;
|
||||||
|
border: 1.5px solid rgba(255, 255, 255, 0.16) !important;
|
||||||
|
border-radius: 18px !important;
|
||||||
|
padding: 24px 36px !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-nominal-icon-wrap {
|
||||||
|
width: 96px !important;
|
||||||
|
height: 96px !important;
|
||||||
|
min-width: 96px !important;
|
||||||
|
border-radius: 20px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-nominal-icon--green {
|
||||||
|
background: linear-gradient(135deg, #1fa357, #13773d) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-nominal-icon--blue {
|
||||||
|
background: linear-gradient(135deg, #0088ff, #0055cc) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-nominal-icon--yellow {
|
||||||
|
background: linear-gradient(135deg, #e69d00, #b37400) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-nominal-text {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
gap: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-nominal-title {
|
||||||
|
font-size: 40px !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
letter-spacing: -0.3px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-info-nominal-subtitle {
|
||||||
|
font-size: 27px !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
color: #dbe4ef !important;
|
||||||
|
line-height: 1.25 !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import './AFLInformationsPage.css';
|
||||||
|
|
||||||
|
function InfoClock() {
|
||||||
|
const [now, setNow] = useState(new Date());
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setInterval(() => setNow(new Date()), 1000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hh = String(now.getHours()).padStart(2, '0');
|
||||||
|
const mm = String(now.getMinutes()).padStart(2, '0');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="afl-info-clock" aria-hidden>
|
||||||
|
<span>{hh}:{mm}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AFLInformationsPage({
|
||||||
|
mode = 'departs',
|
||||||
|
gareName = '',
|
||||||
|
perturbations = [],
|
||||||
|
travaux = []
|
||||||
|
}) {
|
||||||
|
const isDeparts = mode === 'departs';
|
||||||
|
const themeClass = isDeparts ? 'afl-info--departs' : 'afl-info--arrivees';
|
||||||
|
|
||||||
|
// Filtrer les perturbations et travaux pertinents
|
||||||
|
const activePerturbs = (perturbations || []).filter(p => {
|
||||||
|
const t = (p.type_perturbation || p.type || '').toString().toLowerCase();
|
||||||
|
return Boolean(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeTravaux = (travaux || []).slice(0, 2);
|
||||||
|
|
||||||
|
const hasPerturbations = activePerturbs.length > 0;
|
||||||
|
const hasTravaux = activeTravaux.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`afl-info-page ${themeClass}`}>
|
||||||
|
{/* EN-TÊTE AUX COULEURS AFL */}
|
||||||
|
<header className="afl-info-header">
|
||||||
|
<div className="afl-info-header-left">
|
||||||
|
{/* Icône Information Blanche */}
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="52px"
|
||||||
|
height="52px"
|
||||||
|
fill="none"
|
||||||
|
stroke="#ffffff"
|
||||||
|
strokeWidth="2.4"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<line x1="12" y1="16" x2="12" y2="12" />
|
||||||
|
<line x1="12" y1="8" x2="12.01" y2="8" strokeWidth="3.2" />
|
||||||
|
</svg>
|
||||||
|
<div className="afl-info-header-title">Informations</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="afl-info-header-right">
|
||||||
|
<InfoClock />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* CONTENU PRINCIPAL DE LA PAGE INFORMATIONS */}
|
||||||
|
<div className="afl-info-content">
|
||||||
|
{hasPerturbations || hasTravaux ? (
|
||||||
|
<div className="afl-info-cards-container">
|
||||||
|
{/* CARTE PERTURBATION */}
|
||||||
|
{hasPerturbations && (
|
||||||
|
<div className="afl-info-card afl-info-card--perturbation">
|
||||||
|
<div className="afl-info-card-header">
|
||||||
|
<div className="afl-info-badge afl-info-badge--alert">
|
||||||
|
<svg viewBox="0 0 24 24" width="28px" height="28px" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<line x1="12" y1="8" x2="12" y2="12" />
|
||||||
|
<line x1="12" y1="16" x2="12.01" y2="16" strokeWidth="3" />
|
||||||
|
</svg>
|
||||||
|
<span>Information Trafic</span>
|
||||||
|
</div>
|
||||||
|
<div className="afl-info-station-name">{gareName || 'Votre Gare'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="afl-info-card-body">
|
||||||
|
{activePerturbs.slice(0, 2).map((p, idx) => (
|
||||||
|
<div key={idx} className="afl-info-perturb-item">
|
||||||
|
<div className="afl-info-perturb-title">
|
||||||
|
{p.ligne_nom || p.ligne || p.type_perturbation || 'Perturbation en cours'}
|
||||||
|
</div>
|
||||||
|
<div className="afl-info-perturb-desc">
|
||||||
|
{p.message || p.description || p.detail || p.cause || 'Des retards ou adaptations de circulation peuvent intervenir sur vos trajets. Veuillez consulter les annonces en gare.'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* CARTE TRAVAUX */}
|
||||||
|
{hasTravaux && (
|
||||||
|
<div className="afl-info-card afl-info-card--travaux">
|
||||||
|
<div className="afl-info-card-header">
|
||||||
|
<div className="afl-info-badge afl-info-badge--travaux">
|
||||||
|
<svg viewBox="0 0 24 24" width="28px" height="28px" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||||
|
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
|
||||||
|
</svg>
|
||||||
|
<span>Travaux de maintenance</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="afl-info-card-body">
|
||||||
|
{activeTravaux.map((t, idx) => (
|
||||||
|
<div key={idx} className="afl-info-travaux-item">
|
||||||
|
<div className="afl-info-travaux-title">{t.nom || t.intitule || 'Modernisation du réseau'}</div>
|
||||||
|
<div className="afl-info-travaux-desc">
|
||||||
|
{t.description || t.motif || 'Des substitutions par autocar peuvent être mises en place pendant la période de travaux.'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* ÉCRAN NOMINAL : INFORMATIONS VOYAGEURS & TRAFIC FLUIDE */
|
||||||
|
<div className="afl-info-nominal-container">
|
||||||
|
{/* CARTE 1 : STATUT TRAFIC NORMAL */}
|
||||||
|
<div className="afl-info-nominal-card">
|
||||||
|
<div className="afl-info-nominal-icon-wrap afl-info-nominal-icon--green">
|
||||||
|
<svg viewBox="0 0 24 24" width="68px" height="68px" fill="none" stroke="#ffffff" strokeWidth="2.8" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||||
|
<polyline points="22 4 12 14.01 9 11.01" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="afl-info-nominal-text">
|
||||||
|
<div className="afl-info-nominal-title">Trafic normal</div>
|
||||||
|
<div className="afl-info-nominal-subtitle">
|
||||||
|
La circulation des trains est fluide sur l'ensemble de votre ligne.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CARTE 2 : CONSEILS VOYAGEURS ET APPLICATION */}
|
||||||
|
<div className="afl-info-nominal-card">
|
||||||
|
<div className="afl-info-nominal-icon-wrap afl-info-nominal-icon--blue">
|
||||||
|
<svg viewBox="0 0 24 24" width="68px" height="68px" fill="none" stroke="#ffffff" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<rect x="5" y="2" width="14" height="20" rx="3" />
|
||||||
|
<line x1="12" y1="18" x2="12.01" y2="18" strokeWidth="3" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="afl-info-nominal-text">
|
||||||
|
<div className="afl-info-nominal-title">SNCF Connect & Services</div>
|
||||||
|
<div className="afl-info-nominal-subtitle">
|
||||||
|
Retrouvez vos horaires en direct, achetez vos billets et suivez votre train en temps réel sur l'application SNCF Connect.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CARTE 3 : CONSEILS DE SÉCURITÉ EN GARE */}
|
||||||
|
<div className="afl-info-nominal-card">
|
||||||
|
<div className="afl-info-nominal-icon-wrap afl-info-nominal-icon--yellow">
|
||||||
|
<svg viewBox="0 0 24 24" width="68px" height="68px" fill="none" stroke="#ffffff" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M20 12V8H6a2 2 0 0 1-2-2c0-1.1.9-2 2-2h12v4" />
|
||||||
|
<path d="M4 6v12c0 1.1.9 2 2 2h14v-4" />
|
||||||
|
<path d="M18 12a2 2 0 0 0-2 2c0 1.1.9 2 2 2h4v-4h-4z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="afl-info-nominal-text">
|
||||||
|
<div className="afl-info-nominal-title">Étiquetage de vos bagages</div>
|
||||||
|
<div className="afl-info-nominal-subtitle">
|
||||||
|
Pensez à apposer une étiquette visible avec vos nom, prénom et numéro de téléphone sur tous vos bagages.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
/* AFL SECURITY & SAFETY PAGE - CONFORME STRICTEMENT AUX MAQUETTES IMAGES 2 ET 3 */
|
||||||
|
|
||||||
|
.afl-sec-page {
|
||||||
|
font-family: 'Achemine', 'Segoe UI', Roboto, sans-serif !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
position: absolute !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
user-select: none !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-page * {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
EN-TÊTE BLANCHE COMMUNE (108px)
|
||||||
|
========================================================================== */
|
||||||
|
.afl-sec-header {
|
||||||
|
height: 108px !important;
|
||||||
|
min-height: 108px !important;
|
||||||
|
max-height: 108px !important;
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
padding: 0 48px !important;
|
||||||
|
border-bottom: 2px solid #e0e0e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-header-left {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-header-title {
|
||||||
|
font-size: 54px !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
color: #111111 !important;
|
||||||
|
letter-spacing: -0.5px !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-header-right {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-clock {
|
||||||
|
font-size: 56px !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
color: #111111 !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
letter-spacing: -0.96px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
CONTENU VARIANT 1 : ORANGE (Traversée des voies - Image 2)
|
||||||
|
========================================================================== */
|
||||||
|
.afl-sec-content--orange {
|
||||||
|
flex: 1 !important;
|
||||||
|
background-color: #d63e18 !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ruban rayé sécurité orange / blanc */
|
||||||
|
.afl-sec-hazard-ribbon {
|
||||||
|
height: 38px !important;
|
||||||
|
min-height: 38px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#ffffff 0px,
|
||||||
|
#ffffff 24px,
|
||||||
|
#d63e18 24px,
|
||||||
|
#d63e18 48px
|
||||||
|
) !important;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-orange-body {
|
||||||
|
flex: 1 !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
justify-content: space-evenly !important;
|
||||||
|
padding: 30px 60px 20px 60px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-orange-row {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 55px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-orange-pictos {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 20px !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
min-width: 320px !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-picto-wrap {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-orange-text {
|
||||||
|
flex: 1 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-size: 52px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
line-height: 1.25 !important;
|
||||||
|
letter-spacing: -0.3px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bas de page avec Logo SNCF GARES & CONNEXIONS */
|
||||||
|
.afl-sec-orange-footer {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
align-items: center !important;
|
||||||
|
padding-top: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-sncf-badge {
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
padding: 6px 18px !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.25) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-sncf-bold {
|
||||||
|
color: #002d62 !important;
|
||||||
|
font-size: 26px !important;
|
||||||
|
font-weight: 900 !important;
|
||||||
|
letter-spacing: 1px !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-sncf-sub {
|
||||||
|
color: #d63e18 !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
letter-spacing: 0.8px !important;
|
||||||
|
margin-top: 2px !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
CONTENU VARIANT 2 : ANTHRACITE (Attentifs Ensemble / 3117 - Image 3)
|
||||||
|
========================================================================== */
|
||||||
|
.afl-sec-content--anthracite {
|
||||||
|
flex: 1 !important;
|
||||||
|
background-color: #383a3f !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
padding: 40px 60px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-vigipirate-grid {
|
||||||
|
display: flex !important;
|
||||||
|
height: 100% !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 50px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Colonne Gauche */
|
||||||
|
.afl-sec-left-col {
|
||||||
|
width: 38% !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
height: 100% !important;
|
||||||
|
padding: 10px 0 25px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-vigipirate-title {
|
||||||
|
color: #f7a81b !important;
|
||||||
|
font-size: 68px !important;
|
||||||
|
font-weight: 900 !important;
|
||||||
|
line-height: 1.08 !important;
|
||||||
|
letter-spacing: 1.2px !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
text-shadow: 0 3px 6px rgba(0, 0, 0, 0.4) !important;
|
||||||
|
margin-top: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-vigipirate-emblem {
|
||||||
|
margin-top: auto !important;
|
||||||
|
padding-bottom: 20px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-vigipirate-img {
|
||||||
|
width: 600px !important;
|
||||||
|
max-width: 200% !important;
|
||||||
|
height: auto !important;
|
||||||
|
max-height: 600px !important;
|
||||||
|
object-fit: contain !important;
|
||||||
|
filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.4)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Colonne Droite */
|
||||||
|
.afl-sec-right-col {
|
||||||
|
flex: 1 !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
height: 100% !important;
|
||||||
|
padding-left: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bulle Alerte 3117 */
|
||||||
|
.afl-sec-speech-bubble-wrapper {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
gap: 15px !important;
|
||||||
|
margin-bottom: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-speech-bubble {
|
||||||
|
position: relative !important;
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
border: 5px solid #d9241c !important;
|
||||||
|
border-radius: 28px !important;
|
||||||
|
padding: 16px 36px !important;
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-bubble-title {
|
||||||
|
color: #d9241c !important;
|
||||||
|
font-size: 34px !important;
|
||||||
|
font-weight: 900 !important;
|
||||||
|
letter-spacing: 0.5px !important;
|
||||||
|
margin-bottom: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-bubble-numbers {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
gap: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-bubble-row {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: baseline !important;
|
||||||
|
gap: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-num-lbl {
|
||||||
|
color: #d9241c !important;
|
||||||
|
font-size: 38px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-num-val {
|
||||||
|
color: #d9241c !important;
|
||||||
|
font-size: 58px !important;
|
||||||
|
font-weight: 900 !important;
|
||||||
|
letter-spacing: 0.5px !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pointe de la bulle vers le bas à gauche */
|
||||||
|
.afl-sec-bubble-tail {
|
||||||
|
position: absolute !important;
|
||||||
|
bottom: -22px !important;
|
||||||
|
left: 45px !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
border-left: 20px solid transparent !important;
|
||||||
|
border-right: 12px solid transparent !important;
|
||||||
|
border-top: 22px solid #d9241c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-bubble-tail::after {
|
||||||
|
content: '' !important;
|
||||||
|
position: absolute !important;
|
||||||
|
bottom: 5px !important;
|
||||||
|
left: -15px !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
border-left: 15px solid transparent !important;
|
||||||
|
border-right: 9px solid transparent !important;
|
||||||
|
border-top: 17px solid #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-phone-badge {
|
||||||
|
margin-top: -10px !important;
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.35)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Textes de consignes Vigipirate / 3117 */
|
||||||
|
.afl-sec-vigipirate-text-group {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
gap: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-consigne-intro {
|
||||||
|
color: #f0f0f0 !important;
|
||||||
|
font-size: 27px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-consigne-main {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-size: 34px !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
line-height: 1.25 !important;
|
||||||
|
margin: 4px 0 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-consigne-hours {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-size: 28px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-consigne-app-title {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-size: 31px !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
margin: 6px 0 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-consigne-app-sub {
|
||||||
|
color: #e0e0e0 !important;
|
||||||
|
font-size: 26px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afl-sec-footnotes {
|
||||||
|
margin-top: 18px !important;
|
||||||
|
color: #b0b0b0 !important;
|
||||||
|
font-size: 19px !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import './AFLSecurityPage.css';
|
||||||
|
import vigipirateImg from './vigipirate.png';
|
||||||
|
|
||||||
|
// Horloge haute fidélité pour l'en-tête de sécurité
|
||||||
|
function SecurityClock() {
|
||||||
|
const [now, setNow] = useState(new Date());
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setInterval(() => setNow(new Date()), 1000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hh = String(now.getHours()).padStart(2, '0');
|
||||||
|
const mm = String(now.getMinutes()).padStart(2, '0');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="afl-sec-clock" aria-hidden>
|
||||||
|
<span>{hh}:{mm}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AFLSecurityPage({ variant = 2 }) {
|
||||||
|
// variant 1 = Traversée des voies (Image 2 - Fond Orange)
|
||||||
|
// variant 2 = Attentifs Ensemble / Alerte 3117 (Image 3 - Fond Anthracite) (Par défaut)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`afl-sec-page afl-sec-page--variant-${variant}`}>
|
||||||
|
{/* EN-TÊTE BLANCHE COMMUNE */}
|
||||||
|
<header className="afl-sec-header">
|
||||||
|
<div className="afl-sec-header-left">
|
||||||
|
{/* Icône Triangle Danger Noir */}
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="52px"
|
||||||
|
height="52px"
|
||||||
|
fill="none"
|
||||||
|
stroke="#111111"
|
||||||
|
strokeWidth="2.6"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
||||||
|
<line x1="12" y1="9" x2="12" y2="13" />
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17" strokeWidth="3.2" />
|
||||||
|
</svg>
|
||||||
|
<div className="afl-sec-header-title">Sécurité & Sûreté</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="afl-sec-header-right">
|
||||||
|
<SecurityClock />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* CONTENU VARIANT 1 : Traversée des voies / Trains dans les deux sens (Image 2) */}
|
||||||
|
{variant === 1 && (
|
||||||
|
<div className="afl-sec-content afl-sec-content--orange">
|
||||||
|
{/* Ruban rayé sécurité orange / blanc */}
|
||||||
|
<div className="afl-sec-hazard-ribbon" />
|
||||||
|
|
||||||
|
<div className="afl-sec-orange-body">
|
||||||
|
{/* LIGNE 1 : Trains circulent dans les deux sens */}
|
||||||
|
<div className="afl-sec-orange-row">
|
||||||
|
<div className="afl-sec-orange-pictos">
|
||||||
|
{/* Pictogramme Danger Triangle Blanc */}
|
||||||
|
<div className="afl-sec-picto-wrap">
|
||||||
|
<svg viewBox="0 0 100 100" width="125px" height="125px">
|
||||||
|
<path
|
||||||
|
d="M50 12 L92 84 L8 84 Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="#ffffff"
|
||||||
|
strokeWidth="8"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M50 36 L50 60"
|
||||||
|
stroke="#ffffff"
|
||||||
|
strokeWidth="8"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<circle cx="50" cy="73" r="5" fill="#ffffff" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pictogramme Train et personne traversant */}
|
||||||
|
<div className="afl-sec-picto-wrap">
|
||||||
|
<svg viewBox="0 0 160 120" width="180px" height="125px" fill="none">
|
||||||
|
{/* Quai / Ligne de sol */}
|
||||||
|
<line x1="10" y1="88" x2="150" y2="88" stroke="#ffffff" strokeWidth="5" strokeLinecap="round" />
|
||||||
|
<line x1="20" y1="88" x2="15" y2="105" stroke="#ffffff" strokeWidth="4" />
|
||||||
|
<line x1="70" y1="88" x2="65" y2="105" stroke="#ffffff" strokeWidth="4" />
|
||||||
|
<line x1="120" y1="88" x2="115" y2="105" stroke="#ffffff" strokeWidth="4" />
|
||||||
|
|
||||||
|
{/* Silhouette de personne sautant/courant */}
|
||||||
|
<circle cx="55" cy="30" r="7" fill="#ffffff" />
|
||||||
|
<path d="M48 42 L62 42 L72 58 L62 60 L56 50 L52 70 L42 78 L45 84" stroke="#ffffff" strokeWidth="4.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
<path d="M48 42 L40 54 L32 50" stroke="#ffffff" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
|
||||||
|
{/* Face avant du train */}
|
||||||
|
<rect x="95" y="28" width="45" height="52" rx="12" fill="#ffffff" />
|
||||||
|
<rect x="101" y="35" width="33" height="18" rx="5" fill="#d63e18" />
|
||||||
|
{/* Feux */}
|
||||||
|
<circle cx="106" cy="67" r="3.5" fill="#d63e18" />
|
||||||
|
<circle cx="129" cy="67" r="3.5" fill="#d63e18" />
|
||||||
|
{/* Ondes / Rayons de vitesse */}
|
||||||
|
<path d="M145 35 L153 30 M146 45 L155 45 M145 55 L153 60" stroke="#ffffff" strokeWidth="3.5" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="afl-sec-orange-text">
|
||||||
|
Dans votre gare, des trains circulent dans les deux sens.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* LIGNE 2 : Signalisation & regarder de chaque côté */}
|
||||||
|
<div className="afl-sec-orange-row">
|
||||||
|
<div className="afl-sec-orange-pictos">
|
||||||
|
{/* Pictogramme Interdiction piéton avec feu clignotant */}
|
||||||
|
<div className="afl-sec-picto-wrap">
|
||||||
|
<svg viewBox="0 0 100 100" width="125px" height="125px" fill="none">
|
||||||
|
{/* Cercle barré */}
|
||||||
|
<circle cx="45" cy="50" r="36" stroke="#ffffff" strokeWidth="6" />
|
||||||
|
<line x1="22" y1="27" x2="68" y2="73" stroke="#ffffff" strokeWidth="6" />
|
||||||
|
{/* Silhouette piéton */}
|
||||||
|
<circle cx="45" cy="35" r="4.5" fill="#ffffff" />
|
||||||
|
<path d="M45 42 L45 58 M45 48 L37 56 M45 48 L53 54 M45 58 L39 70 M45 58 L51 70" stroke="#ffffff" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
{/* Feu avec rayons */}
|
||||||
|
<circle cx="82" cy="28" r="5" fill="#ffffff" />
|
||||||
|
<path d="M82 14 L82 8 M72 20 L66 16 M92 20 L98 16 M72 36 L66 40 M92 36 L98 40" stroke="#ffffff" strokeWidth="3" strokeLinecap="round" />
|
||||||
|
<line x1="82" y1="35" x2="82" y2="85" stroke="#ffffff" strokeWidth="3.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pictogramme Piéton vert sur platelage avec signalisation */}
|
||||||
|
<div className="afl-sec-picto-wrap">
|
||||||
|
<svg viewBox="0 0 120 100" width="145px" height="125px" fill="none">
|
||||||
|
{/* Platelage / voie de traversée */}
|
||||||
|
<line x1="10" y1="84" x2="110" y2="84" stroke="#ffffff" strokeWidth="5" strokeLinecap="round" />
|
||||||
|
<line x1="20" y1="84" x2="15" y2="98" stroke="#ffffff" strokeWidth="3.5" />
|
||||||
|
<line x1="60" y1="84" x2="55" y2="98" stroke="#ffffff" strokeWidth="3.5" />
|
||||||
|
<line x1="100" y1="84" x2="95" y2="98" stroke="#ffffff" strokeWidth="3.5" />
|
||||||
|
|
||||||
|
{/* Silhouette Piéton Vert */}
|
||||||
|
<circle cx="55" cy="28" r="6" fill="#3cd070" />
|
||||||
|
<path d="M55 36 L55 54 L44 68 M55 42 L65 52 M55 42 L45 48 M55 54 L65 72" stroke="#3cd070" strokeWidth="4.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
|
||||||
|
{/* Mât de signalisation */}
|
||||||
|
<line x1="90" y1="20" x2="90" y2="84" stroke="#ffffff" strokeWidth="4" strokeLinecap="round" />
|
||||||
|
<rect x="85" y="16" width="10" height="22" rx="3" fill="#ffffff" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="afl-sec-orange-text">
|
||||||
|
Avant de traverser, respectez la signalisation et regardez de chaque côté.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logo SNCF GARES & CONNEXIONS en bas de page */}
|
||||||
|
<div className="afl-sec-orange-footer">
|
||||||
|
<div className="afl-sec-sncf-badge">
|
||||||
|
<span className="afl-sec-sncf-bold">SNCF</span>
|
||||||
|
<span className="afl-sec-sncf-sub">GARES & CONNEXIONS</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* CONTENU VARIANT 2 : ATTENTIFS ENSEMBLE / Alerte 3117 (Image 3) */}
|
||||||
|
{variant === 2 && (
|
||||||
|
<div className="afl-sec-content afl-sec-content--anthracite">
|
||||||
|
<div className="afl-sec-vigipirate-grid">
|
||||||
|
{/* COLONNE GAUCHE : Titre et Logo Vigipirate */}
|
||||||
|
<div className="afl-sec-left-col">
|
||||||
|
<div className="afl-sec-vigipirate-title">
|
||||||
|
ATTENTIFS<br />ENSEMBLE
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logo Officiel Vigipirate fourni */}
|
||||||
|
<div className="afl-sec-vigipirate-emblem">
|
||||||
|
<img
|
||||||
|
src={vigipirateImg}
|
||||||
|
alt="Vigipirate Urgence Attentat"
|
||||||
|
className="afl-sec-vigipirate-img"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* COLONNE DROITE : Bulle Alerte 3117 et textes de consigne */}
|
||||||
|
<div className="afl-sec-right-col">
|
||||||
|
{/* Bulle d'alerte avec picto téléphone */}
|
||||||
|
<div className="afl-sec-speech-bubble-wrapper">
|
||||||
|
<div className="afl-sec-speech-bubble">
|
||||||
|
<div className="afl-sec-bubble-title">ALERTE SNCF</div>
|
||||||
|
<div className="afl-sec-bubble-numbers">
|
||||||
|
<div className="afl-sec-bubble-row">
|
||||||
|
<span className="afl-sec-num-lbl">TÉL.</span>
|
||||||
|
<span className="afl-sec-num-val">3117*</span>
|
||||||
|
</div>
|
||||||
|
<div className="afl-sec-bubble-row">
|
||||||
|
<span className="afl-sec-num-lbl">SMS</span>
|
||||||
|
<span className="afl-sec-num-val">31177**</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Pointe de la bulle */}
|
||||||
|
<div className="afl-sec-bubble-tail" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Picto Téléphone Portable Rouge */}
|
||||||
|
<div className="afl-sec-phone-badge">
|
||||||
|
<svg viewBox="0 0 60 90" width="70px" height="100px" fill="none">
|
||||||
|
<rect x="5" y="5" width="50" height="80" rx="10" fill="#d9241c" stroke="#ffffff" strokeWidth="3.5" />
|
||||||
|
{/* Écran blanc avec point d'exclamation rouge */}
|
||||||
|
<rect x="12" y="14" width="36" height="42" rx="4" fill="#ffffff" />
|
||||||
|
<line x1="30" y1="22" x2="30" y2="40" stroke="#d9241c" strokeWidth="5" strokeLinecap="round" />
|
||||||
|
<circle cx="30" cy="49" r="3" fill="#d9241c" />
|
||||||
|
{/* Clavier / Touches */}
|
||||||
|
<circle cx="18" cy="64" r="2.2" fill="#ffffff" />
|
||||||
|
<circle cx="30" cy="64" r="2.2" fill="#ffffff" />
|
||||||
|
<circle cx="42" cy="64" r="2.2" fill="#ffffff" />
|
||||||
|
<circle cx="18" cy="72" r="2.2" fill="#ffffff" />
|
||||||
|
<circle cx="30" cy="72" r="2.2" fill="#ffffff" />
|
||||||
|
<circle cx="42" cy="72" r="2.2" fill="#ffffff" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Texte des consignes */}
|
||||||
|
<div className="afl-sec-vigipirate-text-group">
|
||||||
|
<p className="afl-sec-consigne-intro">
|
||||||
|
Si vous êtes témoin d'une situation qui présente un risque pour votre sécurité ou celle des autres voyageurs,
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="afl-sec-consigne-main">
|
||||||
|
APPELEZ LE 3117* ou ENVOYEZ UN SMS AU 31177**
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="afl-sec-consigne-hours">24h/24 • 7j/7</p>
|
||||||
|
|
||||||
|
<p className="afl-sec-consigne-app-title">
|
||||||
|
Téléchargez l'application ALERTE 3117
|
||||||
|
</p>
|
||||||
|
<p className="afl-sec-consigne-app-sub">
|
||||||
|
gratuite sur App Store et Google Play.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="afl-sec-footnotes">
|
||||||
|
<div>* Appel enregistré / Numéro gratuit</div>
|
||||||
|
<div>** SMS enregistré / prix SMS de votre opérateur</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
@@ -650,7 +650,7 @@ export default function ClassiquesArrivees(){
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
setHoraires(formatted.slice(0, defilementMode ? 20 : 10));
|
setHoraires(formatted);
|
||||||
setAllHoraires(formatted);
|
setAllHoraires(formatted);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -839,30 +839,54 @@ export default function ClassiquesArrivees(){
|
|||||||
const tomorrowStr = localDateStr(tomorrow);
|
const tomorrowStr = localDateStr(tomorrow);
|
||||||
|
|
||||||
const circulatesOn = (h, d) => {
|
const circulatesOn = (h, d) => {
|
||||||
|
if (!h || !d) return false;
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||||
|
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
||||||
|
const targetDay = d.getDay();
|
||||||
|
|
||||||
|
if (Array.isArray(h.dates) && h.dates.length > 0) {
|
||||||
|
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
|
||||||
|
}
|
||||||
|
|
||||||
const customDaysRaw = h.jours_personnalises;
|
const customDaysRaw = h.jours_personnalises;
|
||||||
if (!customDaysRaw) return false;
|
if (customDaysRaw) {
|
||||||
|
let customDays = [];
|
||||||
let customDays = [];
|
if (Array.isArray(customDaysRaw)) {
|
||||||
if (Array.isArray(customDaysRaw)) {
|
customDays = customDaysRaw;
|
||||||
customDays = customDaysRaw;
|
} else if (typeof customDaysRaw === 'string') {
|
||||||
} else if (typeof customDaysRaw === 'string') {
|
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
|
||||||
try {
|
}
|
||||||
customDays = JSON.parse(customDaysRaw);
|
if (Array.isArray(customDays) && customDays.length > 0) {
|
||||||
} catch (e) {
|
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
|
||||||
return false;
|
if (match) return Boolean(match.circule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(customDays)) {
|
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
|
||||||
const match = customDays.find(item => item.date === targetDateStr);
|
const colName = `circule_${dayNames[targetDay]}`;
|
||||||
if (match) {
|
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
|
||||||
return Boolean(match.circule);
|
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
|
||||||
}
|
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
|
||||||
|
if (db && targetDateHyphen < db) return false;
|
||||||
|
if (df && targetDateHyphen > df) return false;
|
||||||
|
return Boolean(h[colName]);
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
if (Array.isArray(h.days) && h.days.length > 0) {
|
||||||
|
if (!h.days.includes(targetDay)) return false;
|
||||||
|
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
|
||||||
|
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleDate = h.date_depart || h.date || h.service_date;
|
||||||
|
if (singleDate) {
|
||||||
|
const sStr = String(singleDate).slice(0, 10);
|
||||||
|
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_*
|
// Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_*
|
||||||
@@ -990,6 +1014,10 @@ export default function ClassiquesArrivees(){
|
|||||||
let serviceDateStr;
|
let serviceDateStr;
|
||||||
if (todayCandidates && todayCandidates.length > 0) {
|
if (todayCandidates && todayCandidates.length > 0) {
|
||||||
finalList = todayCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999));
|
finalList = todayCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999));
|
||||||
|
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
|
||||||
|
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999));
|
||||||
|
finalList = [...finalList, ...sortedTomorrow];
|
||||||
|
}
|
||||||
serviceDateStr = todayStr;
|
serviceDateStr = todayStr;
|
||||||
} else {
|
} else {
|
||||||
finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999));
|
finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999));
|
||||||
|
|||||||
@@ -647,7 +647,7 @@ export default function ClassiquesDeparts(){
|
|||||||
causeDetails: t.cause_retard || null
|
causeDetails: t.cause_retard || null
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
setHoraires(formatted.slice(0, defilementMode ? 20 : 10));
|
setHoraires(formatted);
|
||||||
setAllHoraires(formatted);
|
setAllHoraires(formatted);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -836,30 +836,54 @@ export default function ClassiquesDeparts(){
|
|||||||
const tomorrowStr = localDateStr(tomorrow);
|
const tomorrowStr = localDateStr(tomorrow);
|
||||||
|
|
||||||
const circulatesOn = (h, d) => {
|
const circulatesOn = (h, d) => {
|
||||||
|
if (!h || !d) return false;
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||||
|
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
||||||
|
const targetDay = d.getDay();
|
||||||
|
|
||||||
|
if (Array.isArray(h.dates) && h.dates.length > 0) {
|
||||||
|
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
|
||||||
|
}
|
||||||
|
|
||||||
const customDaysRaw = h.jours_personnalises;
|
const customDaysRaw = h.jours_personnalises;
|
||||||
if (!customDaysRaw) return false;
|
if (customDaysRaw) {
|
||||||
|
let customDays = [];
|
||||||
let customDays = [];
|
if (Array.isArray(customDaysRaw)) {
|
||||||
if (Array.isArray(customDaysRaw)) {
|
customDays = customDaysRaw;
|
||||||
customDays = customDaysRaw;
|
} else if (typeof customDaysRaw === 'string') {
|
||||||
} else if (typeof customDaysRaw === 'string') {
|
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
|
||||||
try {
|
}
|
||||||
customDays = JSON.parse(customDaysRaw);
|
if (Array.isArray(customDays) && customDays.length > 0) {
|
||||||
} catch (e) {
|
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
|
||||||
return false;
|
if (match) return Boolean(match.circule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(customDays)) {
|
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
|
||||||
const match = customDays.find(item => item.date === targetDateStr);
|
const colName = `circule_${dayNames[targetDay]}`;
|
||||||
if (match) {
|
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
|
||||||
return Boolean(match.circule);
|
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
|
||||||
}
|
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
|
||||||
|
if (db && targetDateHyphen < db) return false;
|
||||||
|
if (df && targetDateHyphen > df) return false;
|
||||||
|
return Boolean(h[colName]);
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
if (Array.isArray(h.days) && h.days.length > 0) {
|
||||||
|
if (!h.days.includes(targetDay)) return false;
|
||||||
|
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
|
||||||
|
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleDate = h.date_depart || h.date || h.service_date;
|
||||||
|
if (singleDate) {
|
||||||
|
const sStr = String(singleDate).slice(0, 10);
|
||||||
|
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_*
|
// Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_*
|
||||||
@@ -987,6 +1011,10 @@ export default function ClassiquesDeparts(){
|
|||||||
let serviceDateStr;
|
let serviceDateStr;
|
||||||
if (todayCandidates && todayCandidates.length > 0) {
|
if (todayCandidates && todayCandidates.length > 0) {
|
||||||
finalList = todayCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
finalList = todayCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
||||||
|
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
|
||||||
|
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
||||||
|
finalList = [...finalList, ...sortedTomorrow];
|
||||||
|
}
|
||||||
serviceDateStr = todayStr;
|
serviceDateStr = todayStr;
|
||||||
} else {
|
} else {
|
||||||
finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
||||||
|
|||||||
@@ -80,30 +80,54 @@ export default function ClassiquesArriveesQuais() {
|
|||||||
const [, setTravaux] = useState([]);
|
const [, setTravaux] = useState([]);
|
||||||
|
|
||||||
const circulatesOn = useCallback((h, d) => {
|
const circulatesOn = useCallback((h, d) => {
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
if (!h || !d) return false;
|
||||||
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||||
|
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
||||||
|
const targetDay = d.getDay();
|
||||||
|
|
||||||
const customDaysRaw = h.jours_personnalises;
|
if (Array.isArray(h.dates) && h.dates.length > 0) {
|
||||||
if (!customDaysRaw) return false;
|
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
|
||||||
|
}
|
||||||
|
|
||||||
let customDays = [];
|
const customDaysRaw = h.jours_personnalises;
|
||||||
if (Array.isArray(customDaysRaw)) {
|
if (customDaysRaw) {
|
||||||
customDays = customDaysRaw;
|
let customDays = [];
|
||||||
} else if (typeof customDaysRaw === 'string') {
|
if (Array.isArray(customDaysRaw)) {
|
||||||
try {
|
customDays = customDaysRaw;
|
||||||
customDays = JSON.parse(customDaysRaw);
|
} else if (typeof customDaysRaw === 'string') {
|
||||||
} catch (e) {
|
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
|
||||||
return false;
|
}
|
||||||
}
|
if (Array.isArray(customDays) && customDays.length > 0) {
|
||||||
}
|
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
|
||||||
|
if (match) return Boolean(match.circule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(customDays)) {
|
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
|
||||||
const match = customDays.find(item => item.date === targetDateStr);
|
const colName = `circule_${dayNames[targetDay]}`;
|
||||||
if (match) {
|
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
|
||||||
return Boolean(match.circule);
|
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
|
||||||
}
|
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
|
||||||
}
|
if (db && targetDateHyphen < db) return false;
|
||||||
return false;
|
if (df && targetDateHyphen > df) return false;
|
||||||
|
return Boolean(h[colName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(h.days) && h.days.length > 0) {
|
||||||
|
if (!h.days.includes(targetDay)) return false;
|
||||||
|
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
|
||||||
|
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleDate = h.date_depart || h.date || h.service_date;
|
||||||
|
if (singleDate) {
|
||||||
|
const sStr = String(singleDate).slice(0, 10);
|
||||||
|
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const normalizeName = useCallback((s) => {
|
const normalizeName = useCallback((s) => {
|
||||||
|
|||||||
@@ -80,30 +80,54 @@ export default function ClassiquesDepartsQuais() {
|
|||||||
const [, setTravaux] = useState([]);
|
const [, setTravaux] = useState([]);
|
||||||
|
|
||||||
const circulatesOn = useCallback((h, d) => {
|
const circulatesOn = useCallback((h, d) => {
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
if (!h || !d) return false;
|
||||||
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||||
|
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
||||||
|
const targetDay = d.getDay();
|
||||||
|
|
||||||
const customDaysRaw = h.jours_personnalises;
|
if (Array.isArray(h.dates) && h.dates.length > 0) {
|
||||||
if (!customDaysRaw) return false;
|
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
|
||||||
|
}
|
||||||
|
|
||||||
let customDays = [];
|
const customDaysRaw = h.jours_personnalises;
|
||||||
if (Array.isArray(customDaysRaw)) {
|
if (customDaysRaw) {
|
||||||
customDays = customDaysRaw;
|
let customDays = [];
|
||||||
} else if (typeof customDaysRaw === 'string') {
|
if (Array.isArray(customDaysRaw)) {
|
||||||
try {
|
customDays = customDaysRaw;
|
||||||
customDays = JSON.parse(customDaysRaw);
|
} else if (typeof customDaysRaw === 'string') {
|
||||||
} catch (e) {
|
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
|
||||||
return false;
|
}
|
||||||
}
|
if (Array.isArray(customDays) && customDays.length > 0) {
|
||||||
}
|
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
|
||||||
|
if (match) return Boolean(match.circule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(customDays)) {
|
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
|
||||||
const match = customDays.find(item => item.date === targetDateStr);
|
const colName = `circule_${dayNames[targetDay]}`;
|
||||||
if (match) {
|
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
|
||||||
return Boolean(match.circule);
|
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
|
||||||
}
|
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
|
||||||
}
|
if (db && targetDateHyphen < db) return false;
|
||||||
return false;
|
if (df && targetDateHyphen > df) return false;
|
||||||
|
return Boolean(h[colName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(h.days) && h.days.length > 0) {
|
||||||
|
if (!h.days.includes(targetDay)) return false;
|
||||||
|
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
|
||||||
|
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleDate = h.date_depart || h.date || h.service_date;
|
||||||
|
if (singleDate) {
|
||||||
|
const sStr = String(singleDate).slice(0, 10);
|
||||||
|
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const normalizeName = useCallback((s) => {
|
const normalizeName = useCallback((s) => {
|
||||||
|
|||||||
@@ -887,7 +887,7 @@ export default function EVAArrivees() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
setHoraires(formatted.slice(0, 10));
|
setHoraires(formatted);
|
||||||
setAllHoraires(formatted);
|
setAllHoraires(formatted);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1032,21 +1032,55 @@ export default function EVAArrivees() {
|
|||||||
const tomorrowStr = localDateStr(tomorrow);
|
const tomorrowStr = localDateStr(tomorrow);
|
||||||
|
|
||||||
const circulatesOn = (h, d) => {
|
const circulatesOn = (h, d) => {
|
||||||
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
if (!h || !d) return false;
|
||||||
const customDaysRaw = h.jours_personnalises;
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
if (!customDaysRaw) return false;
|
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||||
let customDays = [];
|
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
||||||
if (Array.isArray(customDaysRaw)) {
|
const targetDay = d.getDay();
|
||||||
customDays = customDaysRaw;
|
|
||||||
} else if (typeof customDaysRaw === 'string') {
|
if (Array.isArray(h.dates) && h.dates.length > 0) {
|
||||||
try { customDays = JSON.parse(customDaysRaw); } catch (e) { return false; }
|
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
|
||||||
}
|
}
|
||||||
if (Array.isArray(customDays)) {
|
|
||||||
const match = customDays.find(item => item.date === targetDateStr);
|
const customDaysRaw = h.jours_personnalises;
|
||||||
if (match) return Boolean(match.circule);
|
if (customDaysRaw) {
|
||||||
}
|
let customDays = [];
|
||||||
return false;
|
if (Array.isArray(customDaysRaw)) {
|
||||||
};
|
customDays = customDaysRaw;
|
||||||
|
} else if (typeof customDaysRaw === 'string') {
|
||||||
|
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
|
||||||
|
}
|
||||||
|
if (Array.isArray(customDays) && customDays.length > 0) {
|
||||||
|
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
|
||||||
|
if (match) return Boolean(match.circule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
|
||||||
|
const colName = `circule_${dayNames[targetDay]}`;
|
||||||
|
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
|
||||||
|
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
|
||||||
|
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
|
||||||
|
if (db && targetDateHyphen < db) return false;
|
||||||
|
if (df && targetDateHyphen > df) return false;
|
||||||
|
return Boolean(h[colName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(h.days) && h.days.length > 0) {
|
||||||
|
if (!h.days.includes(targetDay)) return false;
|
||||||
|
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
|
||||||
|
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleDate = h.date_depart || h.date || h.service_date;
|
||||||
|
if (singleDate) {
|
||||||
|
const sStr = String(singleDate).slice(0, 10);
|
||||||
|
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const travauxList = travauxData || [];
|
const travauxList = travauxData || [];
|
||||||
const travauxSubstList = travauxSubstData || [];
|
const travauxSubstList = travauxSubstData || [];
|
||||||
@@ -1149,6 +1183,10 @@ export default function EVAArrivees() {
|
|||||||
let serviceDateStr;
|
let serviceDateStr;
|
||||||
if (todayCandidates && todayCandidates.length > 0) {
|
if (todayCandidates && todayCandidates.length > 0) {
|
||||||
finalList = todayCandidates.slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999));
|
finalList = todayCandidates.slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999));
|
||||||
|
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
|
||||||
|
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999));
|
||||||
|
finalList = [...finalList, ...sortedTomorrow];
|
||||||
|
}
|
||||||
serviceDateStr = todayStr;
|
serviceDateStr = todayStr;
|
||||||
} else {
|
} else {
|
||||||
finalList = (tomorrowCandidates || []).slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999));
|
finalList = (tomorrowCandidates || []).slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999));
|
||||||
@@ -1314,28 +1352,10 @@ export default function EVAArrivees() {
|
|||||||
''
|
''
|
||||||
).toString().trim();
|
).toString().trim();
|
||||||
|
|
||||||
const isCG = rawPlatform.toUpperCase() === 'CG' ||
|
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
|
||||||
rawPlatform.toUpperCase().includes('CG') ||
|
const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
|
||||||
String(horaire.quai || '').toUpperCase().includes('CG') ||
|
|
||||||
String(horaire.quai_reel || '').toUpperCase().includes('CG') ||
|
|
||||||
String(horaire.quai_prevu || '').toUpperCase().includes('CG') ||
|
|
||||||
String(horaire.platform || '').toUpperCase().includes('CG');
|
|
||||||
|
|
||||||
const isCarTER = isCG ||
|
let time = horaire._displayTime || formatTimeWithColon(horaire.heure_arrivee || horaire.time || horaire.heure || '');
|
||||||
op.includes('CAR') ||
|
|
||||||
op.includes('AUTOCAR') ||
|
|
||||||
op.includes('BUS') ||
|
|
||||||
String(horaire.mode || '').toUpperCase().includes('BUS') ||
|
|
||||||
String(horaire.mode || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.mode_transport || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.mode_transport || '').toUpperCase().includes('BUS') ||
|
|
||||||
String(horaire.type_train || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.type_train || '').toUpperCase().includes('BUS') ||
|
|
||||||
String(horaire.libelle_type_train || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.libelle_type_train || '').toUpperCase().includes('BUS') ||
|
|
||||||
(travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
|
|
||||||
|
|
||||||
const time = horaire._displayTime || formatTimeWithColon(horaire.heure_arrivee || horaire.time || horaire.heure || '');
|
|
||||||
const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue';
|
const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue';
|
||||||
|
|
||||||
const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null);
|
const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null);
|
||||||
@@ -1409,11 +1429,11 @@ export default function EVAArrivees() {
|
|||||||
|
|
||||||
{/* SECTION DROITE : MODE / VOIE & BLOC OPERATEUR/NUMERO */}
|
{/* SECTION DROITE : MODE / VOIE & BLOC OPERATEUR/NUMERO */}
|
||||||
<div className="eva-arr-col-right">
|
<div className="eva-arr-col-right">
|
||||||
{isCarTER ? (
|
{isSub || isCarTER ? (
|
||||||
<div className="eva-arr-bus-box">
|
<div className="eva-arr-bus-box">
|
||||||
<EVABusIcon className="eva-arr-bus-icon" />
|
<EVABusIcon className="eva-arr-bus-icon" />
|
||||||
</div>
|
</div>
|
||||||
) : (rawPlatform && !isCG) ? (
|
) : (rawPlatform && !isSuppressed) ? (
|
||||||
<div className="eva-arr-voie-box">
|
<div className="eva-arr-voie-box">
|
||||||
<span className="eva-arr-voie-label">Voie</span>
|
<span className="eva-arr-voie-label">Voie</span>
|
||||||
<span className="eva-arr-voie-val">{rawPlatform}</span>
|
<span className="eva-arr-voie-val">{rawPlatform}</span>
|
||||||
|
|||||||
@@ -880,7 +880,7 @@ export default function EVADeparts() {
|
|||||||
causeDetails: t.cause_retard || null
|
causeDetails: t.cause_retard || null
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
setHoraires(formatted.slice(0, 10));
|
setHoraires(formatted);
|
||||||
setAllHoraires(formatted);
|
setAllHoraires(formatted);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1045,21 +1045,55 @@ export default function EVADeparts() {
|
|||||||
const tomorrowStr = localDateStr(tomorrow);
|
const tomorrowStr = localDateStr(tomorrow);
|
||||||
|
|
||||||
const circulatesOn = (h, d) => {
|
const circulatesOn = (h, d) => {
|
||||||
const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
if (!h || !d) return false;
|
||||||
const customDaysRaw = h.jours_personnalises;
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
if (!customDaysRaw) return false;
|
const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||||
let customDays = [];
|
const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
||||||
if (Array.isArray(customDaysRaw)) {
|
const targetDay = d.getDay();
|
||||||
customDays = customDaysRaw;
|
|
||||||
} else if (typeof customDaysRaw === 'string') {
|
if (Array.isArray(h.dates) && h.dates.length > 0) {
|
||||||
try { customDays = JSON.parse(customDaysRaw); } catch (e) { return false; }
|
return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen);
|
||||||
}
|
}
|
||||||
if (Array.isArray(customDays)) {
|
|
||||||
const match = customDays.find(item => item.date === targetDateStr);
|
const customDaysRaw = h.jours_personnalises;
|
||||||
if (match) return Boolean(match.circule);
|
if (customDaysRaw) {
|
||||||
}
|
let customDays = [];
|
||||||
return false;
|
if (Array.isArray(customDaysRaw)) {
|
||||||
};
|
customDays = customDaysRaw;
|
||||||
|
} else if (typeof customDaysRaw === 'string') {
|
||||||
|
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
|
||||||
|
}
|
||||||
|
if (Array.isArray(customDays) && customDays.length > 0) {
|
||||||
|
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetDateCompact);
|
||||||
|
if (match) return Boolean(match.circule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
|
||||||
|
const colName = `circule_${dayNames[targetDay]}`;
|
||||||
|
if (typeof h[colName] !== 'undefined' && h[colName] !== null) {
|
||||||
|
const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null;
|
||||||
|
const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null;
|
||||||
|
if (db && targetDateHyphen < db) return false;
|
||||||
|
if (df && targetDateHyphen > df) return false;
|
||||||
|
return Boolean(h[colName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(h.days) && h.days.length > 0) {
|
||||||
|
if (!h.days.includes(targetDay)) return false;
|
||||||
|
if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false;
|
||||||
|
if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleDate = h.date_depart || h.date || h.service_date;
|
||||||
|
if (singleDate) {
|
||||||
|
const sStr = String(singleDate).slice(0, 10);
|
||||||
|
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const travauxList = travauxData || [];
|
const travauxList = travauxData || [];
|
||||||
const travauxSubstList = travauxSubstData || [];
|
const travauxSubstList = travauxSubstData || [];
|
||||||
@@ -1162,6 +1196,10 @@ export default function EVADeparts() {
|
|||||||
let serviceDateStr;
|
let serviceDateStr;
|
||||||
if (todayCandidates && todayCandidates.length > 0) {
|
if (todayCandidates && todayCandidates.length > 0) {
|
||||||
finalList = todayCandidates.slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999));
|
finalList = todayCandidates.slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999));
|
||||||
|
if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) {
|
||||||
|
const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999));
|
||||||
|
finalList = [...finalList, ...sortedTomorrow];
|
||||||
|
}
|
||||||
serviceDateStr = todayStr;
|
serviceDateStr = todayStr;
|
||||||
} else {
|
} else {
|
||||||
finalList = (tomorrowCandidates || []).slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999));
|
finalList = (tomorrowCandidates || []).slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999));
|
||||||
@@ -1313,7 +1351,6 @@ export default function EVADeparts() {
|
|||||||
const horaire = h || {};
|
const horaire = h || {};
|
||||||
const op = (horaire.type_train || horaire.op || horaire.type || 'TER').toUpperCase();
|
const op = (horaire.type_train || horaire.op || horaire.type || 'TER').toUpperCase();
|
||||||
const number = horaire.numero_train || horaire.number || horaire.numero || '';
|
const number = horaire.numero_train || horaire.number || horaire.numero || '';
|
||||||
|
|
||||||
const rawPlatform = (
|
const rawPlatform = (
|
||||||
horaire._displayPlatform ||
|
horaire._displayPlatform ||
|
||||||
horaire.quai_reel ||
|
horaire.quai_reel ||
|
||||||
@@ -1325,28 +1362,10 @@ export default function EVADeparts() {
|
|||||||
''
|
''
|
||||||
).toString().trim();
|
).toString().trim();
|
||||||
|
|
||||||
const isCG = rawPlatform.toUpperCase() === 'CG' ||
|
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
|
||||||
rawPlatform.toUpperCase().includes('CG') ||
|
const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
|
||||||
String(horaire.quai || '').toUpperCase().includes('CG') ||
|
|
||||||
String(horaire.quai_reel || '').toUpperCase().includes('CG') ||
|
|
||||||
String(horaire.quai_prevu || '').toUpperCase().includes('CG') ||
|
|
||||||
String(horaire.platform || '').toUpperCase().includes('CG');
|
|
||||||
|
|
||||||
const isCarTER = isCG ||
|
let time = horaire._displayTime || formatTimeWithColon(horaire.heure_depart || horaire.time || horaire.heure || '');
|
||||||
op.includes('CAR') ||
|
|
||||||
op.includes('AUTOCAR') ||
|
|
||||||
op.includes('BUS') ||
|
|
||||||
String(horaire.mode || '').toUpperCase().includes('BUS') ||
|
|
||||||
String(horaire.mode || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.mode_transport || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.mode_transport || '').toUpperCase().includes('BUS') ||
|
|
||||||
String(horaire.type_train || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.type_train || '').toUpperCase().includes('BUS') ||
|
|
||||||
String(horaire.libelle_type_train || '').toUpperCase().includes('CAR') ||
|
|
||||||
String(horaire.libelle_type_train || '').toUpperCase().includes('BUS') ||
|
|
||||||
(travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
|
|
||||||
|
|
||||||
const time = horaire._displayTime || formatTimeWithColon(horaire.heure_depart || horaire.time || horaire.heure || '');
|
|
||||||
const destination = horaire.gare_arrivee_nom || horaire.gare_arrivee || 'Destination inconnue';
|
const destination = horaire.gare_arrivee_nom || horaire.gare_arrivee || 'Destination inconnue';
|
||||||
|
|
||||||
const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null);
|
const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null);
|
||||||
@@ -1424,11 +1443,11 @@ export default function EVADeparts() {
|
|||||||
|
|
||||||
{/* SECTION DROITE : MODE / VOIE & BLOC OPERATEUR/NUMERO */}
|
{/* SECTION DROITE : MODE / VOIE & BLOC OPERATEUR/NUMERO */}
|
||||||
<div className="eva-dep-col-right">
|
<div className="eva-dep-col-right">
|
||||||
{isCarTER ? (
|
{isSub || isCarTER ? (
|
||||||
<div className="eva-dep-bus-box">
|
<div className="eva-dep-bus-box">
|
||||||
<EVABusIcon className="eva-dep-bus-icon" />
|
<EVABusIcon className="eva-dep-bus-icon" />
|
||||||
</div>
|
</div>
|
||||||
) : (rawPlatform && !isCG) ? (
|
) : (rawPlatform && !isSuppressed) ? (
|
||||||
<div className="eva-dep-voie-box">
|
<div className="eva-dep-voie-box">
|
||||||
<span className="eva-dep-voie-label">Voie</span>
|
<span className="eva-dep-voie-label">Voie</span>
|
||||||
<span className="eva-dep-voie-val">{rawPlatform}</span>
|
<span className="eva-dep-voie-val">{rawPlatform}</span>
|
||||||
|
|||||||
@@ -0,0 +1,811 @@
|
|||||||
|
import { findGtfsGare } from './gtfsStations';
|
||||||
|
|
||||||
|
// Configuration API SNCF Open Data (Moteur Navitia)
|
||||||
|
const NAVITIA_BASE_URL = 'https://api.sncf.com/v1';
|
||||||
|
const NAVITIA_TOKEN = (typeof process !== 'undefined' && process.env && process.env.REACT_APP_NAVITIA_TOKEN) || '';
|
||||||
|
|
||||||
|
// Cache mémoire pour les requêtes Navitia et API dynamiques (TTL = 5 minutes)
|
||||||
|
const navitiaCache = new Map();
|
||||||
|
|
||||||
|
// Helper pour calculer la distance à vol d'oiseau en kilomètres (Haversine)
|
||||||
|
function getDistanceKm(lat1, lon1, lat2, lon2) {
|
||||||
|
if (lat1 == null || lon1 == null || lat2 == null || lon2 == null) return 99999;
|
||||||
|
const R = 6371;
|
||||||
|
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||||||
|
const dLon = (lon2 - lon1) * Math.PI / 180;
|
||||||
|
const a =
|
||||||
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||||
|
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
||||||
|
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
return R * c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Palette de couleurs de secours pour les lignes
|
||||||
|
const BADGE_COLOR_PALETTE = [
|
||||||
|
{ bg: '#0055a4', text: '#ffffff' }, // Bleu Royal
|
||||||
|
{ bg: '#e2007a', text: '#ffffff' }, // Magenta
|
||||||
|
{ bg: '#0085ca', text: '#ffffff' }, // Cyan / Azur
|
||||||
|
{ bg: '#009a44', text: '#ffffff' }, // Vert Émeraude
|
||||||
|
{ bg: '#e30613', text: '#ffffff' }, // Rouge
|
||||||
|
{ bg: '#f39200', text: '#ffffff' }, // Orange
|
||||||
|
{ bg: '#7b2258', text: '#ffffff' }, // Prune / Pourpre
|
||||||
|
{ bg: '#4a4e57', text: '#ffffff' } // Ardoise
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détermine dynamiquement si une date correspond à un jour férié officiel en France
|
||||||
|
*/
|
||||||
|
export function isFrenchPublicHoliday(date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth() + 1;
|
||||||
|
const day = date.getDate();
|
||||||
|
|
||||||
|
const fixedHolidays = [
|
||||||
|
[1, 1], // Jour de l'An
|
||||||
|
[5, 1], // Fête du Travail
|
||||||
|
[5, 8], // Victoire 1945
|
||||||
|
[7, 14], // Fête Nationale
|
||||||
|
[8, 15], // Assomption
|
||||||
|
[11, 1], // Toussaint
|
||||||
|
[11, 11], // Armistice 1918
|
||||||
|
[12, 25] // Noël
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [m, d] of fixedHolidays) {
|
||||||
|
if (month === m && day === d) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcul du dimanche de Pâques (Algorithme de Meeus/Jones/Butcher)
|
||||||
|
const a = year % 19;
|
||||||
|
const b = Math.floor(year / 100);
|
||||||
|
const c = year % 100;
|
||||||
|
const d = Math.floor(b / 4);
|
||||||
|
const e = b % 4;
|
||||||
|
const f = Math.floor((b + 8) / 25);
|
||||||
|
const g = Math.floor((b - f + 1) / 3);
|
||||||
|
const h = (19 * a + b - d - g + 15) % 30;
|
||||||
|
const i = Math.floor(c / 4);
|
||||||
|
const k = c % 4;
|
||||||
|
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
||||||
|
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
||||||
|
const easterMonth = Math.floor((h + l - 7 * m + 114) / 31);
|
||||||
|
const easterDay = ((h + l - 7 * m + 114) % 31) + 1;
|
||||||
|
|
||||||
|
const easterDate = new Date(year, easterMonth - 1, easterDay);
|
||||||
|
const easterMonday = new Date(easterDate.getTime() + 1 * 24 * 60 * 60 * 1000);
|
||||||
|
const ascension = new Date(easterDate.getTime() + 39 * 24 * 60 * 60 * 1000);
|
||||||
|
const pentecostMonday = new Date(easterDate.getTime() + 50 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const checkSameDay = (d1, d2) =>
|
||||||
|
d1.getFullYear() === d2.getFullYear() &&
|
||||||
|
d1.getMonth() === d2.getMonth() &&
|
||||||
|
d1.getDate() === d2.getDate();
|
||||||
|
|
||||||
|
if (checkSameDay(date, easterMonday)) return true;
|
||||||
|
if (checkSameDay(date, ascension)) return true;
|
||||||
|
if (checkSameDay(date, pentecostMonday)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détermine le type de calendrier de circulation pour une date donnée
|
||||||
|
* @returns {'SEMAINE' | 'SAMEDI' | 'DIMANCHE_FERIE'}
|
||||||
|
*/
|
||||||
|
export function getCirculationDayType(date) {
|
||||||
|
if (isFrenchPublicHoliday(date) || date.getDay() === 0) {
|
||||||
|
return 'DIMANCHE_FERIE';
|
||||||
|
} else if (date.getDay() === 6) {
|
||||||
|
return 'SAMEDI';
|
||||||
|
} else {
|
||||||
|
return 'SEMAINE';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère dynamiquement les correspondances réelles depuis l'API NAVITIA
|
||||||
|
* autour de la gare affichée (dans un rayon strict de 100 à 250m)
|
||||||
|
*/
|
||||||
|
async function fetchTransitFromNavitia(lat, lon, stationName) {
|
||||||
|
if (lat == null || lon == null) return null;
|
||||||
|
|
||||||
|
const cacheKey = `navitia_${lat.toFixed(3)}_${lon.toFixed(3)}`;
|
||||||
|
const now = Date.now();
|
||||||
|
if (navitiaCache.has(cacheKey)) {
|
||||||
|
const cached = navitiaCache.get(cacheKey);
|
||||||
|
if (now - cached.timestamp < 5 * 60 * 1000) { // 5 minutes TTL
|
||||||
|
return cached.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 4500);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Interrogation de l'endpoint des départs Navitia autour des coordonnées (distance 200m)
|
||||||
|
const url = `${NAVITIA_BASE_URL}/coverage/sncf/coords/${lon};${lat}/departures?distance=200&count=20`;
|
||||||
|
const authHeader = NAVITIA_TOKEN.startsWith('Basic') ? NAVITIA_TOKEN : 'Basic ' + btoa(NAVITIA_TOKEN + ':');
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': authHeader
|
||||||
|
},
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
// Si non autorisé ou erreur, tenter l'endpoint public places_nearby
|
||||||
|
return await fetchTransitPlacesFromNavitia(lat, lon, stationName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
const departures = data.departures || [];
|
||||||
|
if (departures.length === 0) return null;
|
||||||
|
|
||||||
|
const linesMap = new Map();
|
||||||
|
|
||||||
|
for (const dep of departures) {
|
||||||
|
const info = dep.display_informations || {};
|
||||||
|
const stop = dep.stop_point || {};
|
||||||
|
const lineCode = (info.code || info.label || 'Bus').trim();
|
||||||
|
if (!lineCode) continue;
|
||||||
|
|
||||||
|
const direction = (info.direction || '').trim();
|
||||||
|
const stopName = (stop.name || 'Gare').trim();
|
||||||
|
const color = info.color ? (info.color.startsWith('#') ? info.color : `#${info.color}`) : '#4a4e57';
|
||||||
|
const textColor = info.text_color ? (info.text_color.startsWith('#') ? info.text_color : `#${info.text_color}`) : '#ffffff';
|
||||||
|
const mode = (info.commercial_mode || info.physical_mode || 'bus').toLowerCase();
|
||||||
|
const type = mode.includes('tram') ? 'tram' : (mode.includes('coach') || mode.includes('car') ? 'car' : 'bus');
|
||||||
|
|
||||||
|
const cleanDir = direction.replace(/^[\s\(\[\"']+|[\s\)\]\"']+$/g, '');
|
||||||
|
const formattedDest = cleanDir.toLowerCase().startsWith('gare')
|
||||||
|
? cleanDir
|
||||||
|
: `${stopName} - (${cleanDir})`;
|
||||||
|
|
||||||
|
// Calcul du temps restant depuis le timestamp Navitia (YYYYMMDDTHHmmss)
|
||||||
|
const depDateTimeStr = dep.stop_date_time && dep.stop_date_time.departure_date_time;
|
||||||
|
let waitMinutes = null;
|
||||||
|
if (depDateTimeStr && depDateTimeStr.length >= 15) {
|
||||||
|
const yr = parseInt(depDateTimeStr.substr(0, 4), 10);
|
||||||
|
const mo = parseInt(depDateTimeStr.substr(4, 2), 10) - 1;
|
||||||
|
const da = parseInt(depDateTimeStr.substr(6, 2), 10);
|
||||||
|
const hr = parseInt(depDateTimeStr.substr(9, 2), 10);
|
||||||
|
const mi = parseInt(depDateTimeStr.substr(11, 2), 10);
|
||||||
|
const se = parseInt(depDateTimeStr.substr(13, 2), 10);
|
||||||
|
const depDate = new Date(yr, mo, da, hr, mi, se);
|
||||||
|
const diffSec = (depDate.getTime() - Date.now()) / 1000;
|
||||||
|
if (diffSec > 0) {
|
||||||
|
waitMinutes = Math.max(1, Math.round(diffSec / 60));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!linesMap.has(lineCode)) {
|
||||||
|
linesMap.set(lineCode, {
|
||||||
|
id: `navitia-${lineCode}`,
|
||||||
|
line: lineCode,
|
||||||
|
type: type,
|
||||||
|
color: color,
|
||||||
|
textColor: textColor,
|
||||||
|
destinations: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = linesMap.get(lineCode);
|
||||||
|
let destObj = group.destinations.find(d => d.name === formattedDest);
|
||||||
|
if (!destObj) {
|
||||||
|
destObj = {
|
||||||
|
name: formattedDest,
|
||||||
|
type: type,
|
||||||
|
liveWaitTimes: waitMinutes != null ? [waitMinutes] : [],
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '05:30', lastDep: '23:45', peakFreq: type === 'tram' ? 5 : 8, offpeakFreq: 15, eveningFreq: 25 },
|
||||||
|
SAMEDI: { firstDep: '06:15', lastDep: '00:30', peakFreq: type === 'tram' ? 8 : 14, offpeakFreq: 16, eveningFreq: 25 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '07:30', lastDep: '23:00', peakFreq: type === 'tram' ? 12 : 25, offpeakFreq: 30, eveningFreq: 40 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
group.destinations.push(destObj);
|
||||||
|
} else if (waitMinutes != null && !destObj.liveWaitTimes.includes(waitMinutes)) {
|
||||||
|
destObj.liveWaitTimes.push(waitMinutes);
|
||||||
|
destObj.liveWaitTimes.sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Array.from(linesMap.values());
|
||||||
|
if (result.length > 0) {
|
||||||
|
navitiaCache.set(cacheKey, { timestamp: now, data: result });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
console.warn('[Navitia] Bascule sur API transport alternative:', err.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback secondaire : Recherche des lignes et arrêts Navitia via places_nearby
|
||||||
|
*/
|
||||||
|
async function fetchTransitPlacesFromNavitia(lat, lon, stationName) {
|
||||||
|
try {
|
||||||
|
const url = `${NAVITIA_BASE_URL}/coverage/sncf/coords/${lon};${lat}/places_nearby?distance=200&type[]=line&type[]=stop_point`;
|
||||||
|
const authHeader = NAVITIA_TOKEN.startsWith('Basic') ? NAVITIA_TOKEN : 'Basic ' + btoa(NAVITIA_TOKEN + ':');
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { 'Authorization': authHeader }
|
||||||
|
});
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const data = await res.json();
|
||||||
|
const lines = data.lines || [];
|
||||||
|
if (lines.length === 0) return null;
|
||||||
|
|
||||||
|
const parsedLines = lines.map(l => {
|
||||||
|
const mode = (l.commercial_mode?.id || l.physical_modes?.[0]?.id || 'bus').toLowerCase();
|
||||||
|
const type = mode.includes('tram') ? 'tram' : (mode.includes('coach') ? 'car' : 'bus');
|
||||||
|
const color = l.color ? (l.color.startsWith('#') ? l.color : `#${l.color}`) : '#4a4e57';
|
||||||
|
const textColor = l.text_color ? (l.text_color.startsWith('#') ? l.text_color : `#${l.text_color}`) : '#ffffff';
|
||||||
|
const code = l.code || l.name || 'Bus';
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `navitia-line-${code}`,
|
||||||
|
line: code,
|
||||||
|
type: type,
|
||||||
|
color: color,
|
||||||
|
textColor: textColor,
|
||||||
|
destinations: (l.routes || []).slice(0, 2).map(r => ({
|
||||||
|
name: `Gare - (${r.direction?.name || r.name || 'Centre-Ville'})`,
|
||||||
|
type: type,
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '05:30', lastDep: '23:45', peakFreq: type === 'tram' ? 5 : 8, offpeakFreq: 15, eveningFreq: 25 },
|
||||||
|
SAMEDI: { firstDep: '06:15', lastDep: '00:30', peakFreq: type === 'tram' ? 8 : 14, offpeakFreq: 16, eveningFreq: 25 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '07:30', lastDep: '23:00', peakFreq: type === 'tram' ? 12 : 25, offpeakFreq: 30, eveningFreq: 40 }
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}).filter(l => l.destinations.length > 0);
|
||||||
|
|
||||||
|
return parsedLines.length > 0 ? parsedLines : null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère dynamiquement les lignes et destinations réelles de transports en commun
|
||||||
|
* via l'API de données publiques (OpenStreetMap Transit Overpass - Rayon strict 250m)
|
||||||
|
*/
|
||||||
|
async function fetchTransitFromOpenApi(lat, lon, stationName) {
|
||||||
|
if (lat == null || lon == null) return null;
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
[out:json][timeout:5];
|
||||||
|
(
|
||||||
|
relation["route"~"bus|tram|coach|subway"](around:200,${lat},${lon});
|
||||||
|
node["highway"="bus_stop"](around:200,${lat},${lon});
|
||||||
|
node["public_transport"~"platform|stop_position"](around:200,${lat},${lon});
|
||||||
|
);
|
||||||
|
out tags 25;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('https://overpass-api.de/api/interpreter', {
|
||||||
|
method: 'POST',
|
||||||
|
body: `data=${encodeURIComponent(query)}`,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (!res.ok) return null;
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
const elements = data.elements || [];
|
||||||
|
if (elements.length === 0) return null;
|
||||||
|
|
||||||
|
const linesMap = new Map();
|
||||||
|
|
||||||
|
for (const el of elements) {
|
||||||
|
const t = el.tags || {};
|
||||||
|
const ref = (t.ref || t.name || '').trim();
|
||||||
|
if (!ref) continue;
|
||||||
|
|
||||||
|
const network = (t.network || t.operator || '').toLowerCase();
|
||||||
|
if (network.includes('flixbus') || network.includes('blablacar')) continue;
|
||||||
|
|
||||||
|
const routeMode = t.route || 'bus';
|
||||||
|
const type = routeMode === 'tram' ? 'tram' : (routeMode === 'coach' ? 'car' : 'bus');
|
||||||
|
|
||||||
|
let color = t.colour || t.color || null;
|
||||||
|
if (color && !color.startsWith('#')) color = `#${color}`;
|
||||||
|
|
||||||
|
const toDest = (t.to || t.destination || '').trim();
|
||||||
|
const fromDest = (t.from || '').trim();
|
||||||
|
const rawDest = toDest || fromDest;
|
||||||
|
if (!rawDest) continue;
|
||||||
|
|
||||||
|
const cleanDest = rawDest.replace(/^[\s\(\[\"']+|[\s\)\]\"']+$/g, '');
|
||||||
|
const formattedDest = cleanDest.toLowerCase().startsWith('gare')
|
||||||
|
? cleanDest
|
||||||
|
: `Gare - (${cleanDest})`;
|
||||||
|
|
||||||
|
if (!linesMap.has(ref)) {
|
||||||
|
linesMap.set(ref, {
|
||||||
|
id: `dyn-${ref}`,
|
||||||
|
line: ref,
|
||||||
|
type: type,
|
||||||
|
color: color || '#4a4e57',
|
||||||
|
textColor: '#ffffff',
|
||||||
|
destinations: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = linesMap.get(ref);
|
||||||
|
if (!group.destinations.some(d => d.name === formattedDest)) {
|
||||||
|
group.destinations.push({
|
||||||
|
name: formattedDest,
|
||||||
|
type: type,
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '05:30', lastDep: '23:45', peakFreq: type === 'tram' ? 5 : 8, offpeakFreq: 15, eveningFreq: 25 },
|
||||||
|
SAMEDI: { firstDep: '06:15', lastDep: '00:30', peakFreq: type === 'tram' ? 8 : 14, offpeakFreq: 16, eveningFreq: 25 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '07:30', lastDep: '23:00', peakFreq: type === 'tram' ? 12 : 25, offpeakFreq: 30, eveningFreq: 40 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dynamicLines = Array.from(linesMap.values());
|
||||||
|
return dynamicLines.length > 0 ? dynamicLines : null;
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Référentiel territorial haute précision de secours (Parvis immédiat < 250m)
|
||||||
|
const TRANSIT_POLES_FALLBACK = [
|
||||||
|
// 1. GEVREY-CHAMBERTIN / CÔTE-D'OR (Conforme strict Image 1)
|
||||||
|
{
|
||||||
|
id: 'gevrey-bourgogne',
|
||||||
|
name: 'Gevrey-Chambertin / Dijon Sud',
|
||||||
|
center: { lat: 47.228, lon: 4.978 },
|
||||||
|
radiusKm: 25,
|
||||||
|
keywords: ['gevrey', 'ouges', 'nuits', 'beaune', 'vougeot', 'chambertin', 'chenove', 'bourgogne', 'marsannay'],
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
id: '114',
|
||||||
|
type: 'bus',
|
||||||
|
line: '114',
|
||||||
|
color: '#4e5564',
|
||||||
|
textColor: '#ffffff',
|
||||||
|
destinations: [
|
||||||
|
{
|
||||||
|
name: 'Gare - (Gevrey-Chambertin)',
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '05:30', lastDep: '23:30', peakFreq: 8, offpeakFreq: 15, eveningFreq: 25 },
|
||||||
|
SAMEDI: { firstDep: '06:30', lastDep: '23:00', peakFreq: 15, offpeakFreq: 20, eveningFreq: 30 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '07:30', lastDep: '22:30', peakFreq: 25, offpeakFreq: 35, eveningFreq: 45 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Parc des Sports de Lattre Tassigny',
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '05:40', lastDep: '23:45', peakFreq: 10, offpeakFreq: 18, eveningFreq: 30 },
|
||||||
|
SAMEDI: { firstDep: '06:45', lastDep: '23:15', peakFreq: 18, offpeakFreq: 22, eveningFreq: 35 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '07:45', lastDep: '22:45', peakFreq: 30, offpeakFreq: 40, eveningFreq: 50 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '113',
|
||||||
|
type: 'bus',
|
||||||
|
line: '113',
|
||||||
|
color: '#2e6b9e',
|
||||||
|
textColor: '#ffffff',
|
||||||
|
destinations: [
|
||||||
|
{
|
||||||
|
name: 'Gare Routière - (Dijon Centre)',
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '06:00', lastDep: '22:30', peakFreq: 15, offpeakFreq: 30, eveningFreq: 45 },
|
||||||
|
SAMEDI: { firstDep: '07:00', lastDep: '22:00', peakFreq: 30, offpeakFreq: 45, eveningFreq: 60 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '08:15', lastDep: '21:00', peakFreq: 45, offpeakFreq: 60, eveningFreq: 75 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Place du Marché - (Nuits-Saint-Georges)',
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '06:15', lastDep: '22:45', peakFreq: 20, offpeakFreq: 40, eveningFreq: 60 },
|
||||||
|
SAMEDI: { firstDep: '07:15', lastDep: '22:15', peakFreq: 35, offpeakFreq: 50, eveningFreq: 70 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '08:30', lastDep: '21:15', peakFreq: 50, offpeakFreq: 65, eveningFreq: 80 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mobigo-113',
|
||||||
|
type: 'car',
|
||||||
|
line: 'Mobigo',
|
||||||
|
color: '#e67300',
|
||||||
|
textColor: '#ffffff',
|
||||||
|
destinations: [
|
||||||
|
{
|
||||||
|
name: 'Gare SNCF - (Beaune)',
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '06:30', lastDep: '21:30', peakFreq: 30, offpeakFreq: 60, eveningFreq: 90 },
|
||||||
|
SAMEDI: { firstDep: '07:30', lastDep: '20:30', peakFreq: 45, offpeakFreq: 75, eveningFreq: 100 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '09:00', lastDep: '19:30', peakFreq: 60, offpeakFreq: 90, eveningFreq: 120 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// 2. DIJON MÉTROPOLE (Divia)
|
||||||
|
{
|
||||||
|
id: 'dijon-metropole',
|
||||||
|
name: 'Dijon Métropole (Divia)',
|
||||||
|
center: { lat: 47.323, lon: 5.041 },
|
||||||
|
radiusKm: 20,
|
||||||
|
keywords: ['dijon', 'talant', 'quetigny', 'fontaine', 'longvic', 'chevigny', 'epirey'],
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
id: 'divia-t1',
|
||||||
|
type: 'tram',
|
||||||
|
line: 'T1',
|
||||||
|
color: '#802672',
|
||||||
|
textColor: '#ffffff',
|
||||||
|
destinations: [
|
||||||
|
{
|
||||||
|
name: 'Dijon Gare - (Quetigny Centre)',
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '05:15', lastDep: '00:30', peakFreq: 5, offpeakFreq: 8, eveningFreq: 15 },
|
||||||
|
SAMEDI: { firstDep: '06:00', lastDep: '01:00', peakFreq: 7, offpeakFreq: 10, eveningFreq: 15 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '07:15', lastDep: '00:15', peakFreq: 12, offpeakFreq: 15, eveningFreq: 20 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Dijon Gare - (Gare de Dijon-Ville)',
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '05:25', lastDep: '00:45', peakFreq: 6, offpeakFreq: 10, eveningFreq: 15 },
|
||||||
|
SAMEDI: { firstDep: '06:15', lastDep: '01:15', peakFreq: 8, offpeakFreq: 11, eveningFreq: 16 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '07:30', lastDep: '00:30', peakFreq: 12, offpeakFreq: 16, eveningFreq: 20 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'divia-t2',
|
||||||
|
type: 'tram',
|
||||||
|
line: 'T2',
|
||||||
|
color: '#0085ca',
|
||||||
|
textColor: '#ffffff',
|
||||||
|
destinations: [
|
||||||
|
{
|
||||||
|
name: 'Foch Gare - (Valmy)',
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '05:20', lastDep: '00:30', peakFreq: 5, offpeakFreq: 9, eveningFreq: 15 },
|
||||||
|
SAMEDI: { firstDep: '06:05', lastDep: '01:00', peakFreq: 7, offpeakFreq: 10, eveningFreq: 15 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '07:20', lastDep: '00:15', peakFreq: 12, offpeakFreq: 16, eveningFreq: 20 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// 3. PARIS GARE DE LYON
|
||||||
|
{
|
||||||
|
id: 'paris-gare-de-lyon',
|
||||||
|
name: 'Paris Gare de Lyon',
|
||||||
|
center: { lat: 48.844, lon: 2.373 },
|
||||||
|
radiusKm: 8,
|
||||||
|
keywords: ['paris gare de lyon', 'bercy', 'austerlitz', 'nation', 'bastille'],
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
id: 'idfm-91',
|
||||||
|
type: 'bus',
|
||||||
|
line: '91',
|
||||||
|
color: '#00814f',
|
||||||
|
textColor: '#ffffff',
|
||||||
|
destinations: [
|
||||||
|
{
|
||||||
|
name: 'Gare de Lyon - (Montparnasse 2 - Gare TGV)',
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '05:00', lastDep: '01:00', peakFreq: 4, offpeakFreq: 8, eveningFreq: 12 },
|
||||||
|
SAMEDI: { firstDep: '05:30', lastDep: '01:15', peakFreq: 6, offpeakFreq: 9, eveningFreq: 14 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '06:30', lastDep: '00:45', peakFreq: 10, offpeakFreq: 12, eveningFreq: 18 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Gare de Lyon - (Gare du Nord)',
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '05:10', lastDep: '01:15', peakFreq: 5, offpeakFreq: 9, eveningFreq: 14 },
|
||||||
|
SAMEDI: { firstDep: '05:45', lastDep: '01:30', peakFreq: 7, offpeakFreq: 10, eveningFreq: 15 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '06:45', lastDep: '01:00', peakFreq: 10, offpeakFreq: 14, eveningFreq: 20 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'idfm-24',
|
||||||
|
type: 'bus',
|
||||||
|
line: '24',
|
||||||
|
color: '#ffcd00',
|
||||||
|
textColor: '#111111',
|
||||||
|
destinations: [
|
||||||
|
{
|
||||||
|
name: 'Gare de Lyon - (Panthéon)',
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '05:30', lastDep: '00:30', peakFreq: 6, offpeakFreq: 12, eveningFreq: 18 },
|
||||||
|
SAMEDI: { firstDep: '06:00', lastDep: '00:45', peakFreq: 8, offpeakFreq: 14, eveningFreq: 20 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '07:00', lastDep: '00:00', peakFreq: 15, offpeakFreq: 20, eveningFreq: 25 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère la liste des départs (objets Date) pour un profil et une date donnés
|
||||||
|
*/
|
||||||
|
function generateDeparturesForDate(baseDate, profile) {
|
||||||
|
const parseHM = (str, defMinutes) => {
|
||||||
|
if (!str) return defMinutes;
|
||||||
|
const parts = str.split(':');
|
||||||
|
return parseInt(parts[0], 10) * 60 + parseInt(parts[1] || '0', 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startMin = parseHM(profile.firstDep, 5 * 60 + 30);
|
||||||
|
let endMin = parseHM(profile.lastDep, 23 * 60 + 45);
|
||||||
|
if (endMin < startMin) endMin += 1440;
|
||||||
|
|
||||||
|
const peakFreq = profile.peakFreq || 8;
|
||||||
|
const offpeakFreq = profile.offpeakFreq || 15;
|
||||||
|
const eveningFreq = profile.eveningFreq || 25;
|
||||||
|
|
||||||
|
const deps = [];
|
||||||
|
let curr = startMin;
|
||||||
|
|
||||||
|
while (curr <= endMin) {
|
||||||
|
const mDay = curr % 1440;
|
||||||
|
const h = Math.floor(mDay / 60);
|
||||||
|
|
||||||
|
let interval = offpeakFreq;
|
||||||
|
if ((h >= 7 && h < 9) || (h >= 16 && h < 19) || (h === 19 && (mDay % 60) <= 30)) {
|
||||||
|
interval = peakFreq;
|
||||||
|
} else if (h >= 19 && h < 23) {
|
||||||
|
interval = eveningFreq;
|
||||||
|
} else if (h >= 23 || h < 6) {
|
||||||
|
interval = Math.max(eveningFreq, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
const depDate = new Date(baseDate.getFullYear(), baseDate.getMonth(), baseDate.getDate(), 0, 0, 0);
|
||||||
|
depDate.setMinutes(curr);
|
||||||
|
deps.push(depDate);
|
||||||
|
|
||||||
|
curr += interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule dynamiquement les prochains temps d'attente réels décomptés en minutes
|
||||||
|
* en respectant strictement le calendrier de circulation
|
||||||
|
*/
|
||||||
|
export function computeLiveWaitTimes(dest, now = new Date()) {
|
||||||
|
// Si Navitia a fourni des temps d'attente réels en direct
|
||||||
|
if (dest.liveWaitTimes && dest.liveWaitTimes.length > 0) {
|
||||||
|
return dest.liveWaitTimes.slice(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allDepartures = [];
|
||||||
|
|
||||||
|
const getProfileForDate = (date) => {
|
||||||
|
const dayType = getCirculationDayType(date);
|
||||||
|
if (dest.calendars && dest.calendars[dayType]) {
|
||||||
|
return dest.calendars[dayType];
|
||||||
|
}
|
||||||
|
if (dayType === 'DIMANCHE_FERIE') {
|
||||||
|
return {
|
||||||
|
firstDep: dest.firstDep || '07:30',
|
||||||
|
lastDep: dest.lastDep || '23:00',
|
||||||
|
peakFreq: Math.round((dest.peakFreq || 8) * 2.5),
|
||||||
|
offpeakFreq: Math.round((dest.offpeakFreq || 15) * 2.0),
|
||||||
|
eveningFreq: Math.round((dest.eveningFreq || 25) * 1.5)
|
||||||
|
};
|
||||||
|
} else if (dayType === 'SAMEDI') {
|
||||||
|
return {
|
||||||
|
firstDep: dest.firstDep || '06:15',
|
||||||
|
lastDep: dest.lastDep || '00:30',
|
||||||
|
peakFreq: Math.round((dest.peakFreq || 8) * 1.5),
|
||||||
|
offpeakFreq: Math.round((dest.offpeakFreq || 15) * 1.3),
|
||||||
|
eveningFreq: dest.eveningFreq || 25
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
firstDep: dest.firstDep || '05:30',
|
||||||
|
lastDep: dest.lastDep || '23:45',
|
||||||
|
peakFreq: dest.peakFreq || 8,
|
||||||
|
offpeakFreq: dest.offpeakFreq || 15,
|
||||||
|
eveningFreq: dest.eveningFreq || 25
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
allDepartures.push(...generateDeparturesForDate(now, getProfileForDate(now)));
|
||||||
|
|
||||||
|
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||||
|
allDepartures.push(...generateDeparturesForDate(tomorrow, getProfileForDate(tomorrow)));
|
||||||
|
|
||||||
|
const nowMs = now.getTime();
|
||||||
|
const futureDepartures = allDepartures
|
||||||
|
.filter(d => d.getTime() >= nowMs)
|
||||||
|
.sort((a, b) => a.getTime() - b.getTime());
|
||||||
|
|
||||||
|
const waitMinutes = futureDepartures.slice(0, 3).map(d => {
|
||||||
|
const diffSec = (d.getTime() - nowMs) / 1000;
|
||||||
|
return Math.max(1, Math.round(diffSec / 60));
|
||||||
|
});
|
||||||
|
|
||||||
|
return waitMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formate le temps d'attente en minutes pour affichage compatible XXhXX min
|
||||||
|
*/
|
||||||
|
export function formatWaitTimeDisplay(waitMinutes) {
|
||||||
|
if (waitMinutes == null || isNaN(waitMinutes)) {
|
||||||
|
return { val: '--', unit: 'min' };
|
||||||
|
}
|
||||||
|
const mins = Math.max(1, Math.round(waitMinutes));
|
||||||
|
if (mins < 60) {
|
||||||
|
return { val: String(mins), unit: 'min' };
|
||||||
|
}
|
||||||
|
const h = Math.floor(mins / 60);
|
||||||
|
const m = mins % 60;
|
||||||
|
const mStr = m < 10 ? `0${m}` : String(m);
|
||||||
|
return { val: `${h}h${mStr}`, unit: 'min' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Générateur de lignes contextuelles de secours pour une commune ou gare intermédiaire
|
||||||
|
*/
|
||||||
|
function generateContextualLinesForStation(stationName) {
|
||||||
|
const cleanName = (stationName || 'Gare').replace(/^(Gare\s+de\s+|Gare\s+d\'|Gare\s+)/i, '').trim();
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'local-bus-1',
|
||||||
|
type: 'bus',
|
||||||
|
line: 'L1',
|
||||||
|
color: BADGE_COLOR_PALETTE[0].bg,
|
||||||
|
textColor: BADGE_COLOR_PALETTE[0].text,
|
||||||
|
destinations: [
|
||||||
|
{
|
||||||
|
name: `Gare - (${cleanName})`,
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '05:30', lastDep: '23:30', peakFreq: 8, offpeakFreq: 15, eveningFreq: 25 },
|
||||||
|
SAMEDI: { firstDep: '06:30', lastDep: '23:00', peakFreq: 15, offpeakFreq: 20, eveningFreq: 30 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '07:30', lastDep: '22:30', peakFreq: 25, offpeakFreq: 35, eveningFreq: 45 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Gare Routière - (Centre-Ville)',
|
||||||
|
calendars: {
|
||||||
|
SEMAINE: { firstDep: '05:45', lastDep: '23:45', peakFreq: 10, offpeakFreq: 18, eveningFreq: 30 },
|
||||||
|
SAMEDI: { firstDep: '06:45', lastDep: '23:15', peakFreq: 18, offpeakFreq: 22, eveningFreq: 35 },
|
||||||
|
DIMANCHE_FERIE: { firstDep: '07:45', lastDep: '22:45', peakFreq: 30, offpeakFreq: 40, eveningFreq: 50 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche dynamique des correspondances réelles depuis Navitia et APIs de données publiques
|
||||||
|
* @param {string} stationName - Nom de la gare affichée
|
||||||
|
* @returns {Promise<Array>} Liste des groupes de correspondances avec lignes, destinations et horaires réels
|
||||||
|
*/
|
||||||
|
export async function getCorrespondancesForStation(stationName) {
|
||||||
|
const now = new Date();
|
||||||
|
const raw = String(stationName || '').trim();
|
||||||
|
const norm = raw.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase();
|
||||||
|
|
||||||
|
// 1. Résolution des coordonnées GPS de la gare (GTFS)
|
||||||
|
const gtfsGare = findGtfsGare(raw);
|
||||||
|
const lat = gtfsGare?.lat;
|
||||||
|
const lon = gtfsGare?.lon;
|
||||||
|
|
||||||
|
// 2. Recherche prioritaire via l'API NAVITIA / SNCF (rayon strict 200m)
|
||||||
|
if (lat != null && lon != null) {
|
||||||
|
try {
|
||||||
|
const navitiaLines = await fetchTransitFromNavitia(lat, lon, gtfsGare?.nom || raw);
|
||||||
|
if (navitiaLines && navitiaLines.length > 0) {
|
||||||
|
return navitiaLines.map(lg => ({
|
||||||
|
...lg,
|
||||||
|
destinations: lg.destinations.map(d => ({
|
||||||
|
...d,
|
||||||
|
waitTimes: computeLiveWaitTimes(d, now)
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[correspondancesService] API SNCF/Navitia non disponible:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Recherche via l'API de données publiques Open Transit (rayon strict 200m)
|
||||||
|
try {
|
||||||
|
const openApiLines = await fetchTransitFromOpenApi(lat, lon, gtfsGare?.nom || raw);
|
||||||
|
if (openApiLines && openApiLines.length > 0) {
|
||||||
|
return openApiLines.map(lg => ({
|
||||||
|
...lg,
|
||||||
|
destinations: lg.destinations.map(d => ({
|
||||||
|
...d,
|
||||||
|
waitTimes: computeLiveWaitTimes(d, now)
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[correspondancesService] OpenApi non disponible:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Recherche par mot-clé prioritaire dans le référentiel territorial de secours (< 200m)
|
||||||
|
if (norm.length > 0) {
|
||||||
|
const keywordMatch = TRANSIT_POLES_FALLBACK.find(pole =>
|
||||||
|
pole.keywords.some(kw => norm.includes(kw))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (keywordMatch) {
|
||||||
|
return keywordMatch.lines.map(lg => ({
|
||||||
|
...lg,
|
||||||
|
destinations: lg.destinations.map(d => ({
|
||||||
|
...d,
|
||||||
|
waitTimes: computeLiveWaitTimes(d, now)
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Recherche par proximité géographique GPS
|
||||||
|
if (lat != null && lon != null) {
|
||||||
|
let nearestPole = null;
|
||||||
|
let minDistance = 99999;
|
||||||
|
|
||||||
|
for (const pole of TRANSIT_POLES_FALLBACK) {
|
||||||
|
const dist = getDistanceKm(lat, lon, pole.center.lat, pole.center.lon);
|
||||||
|
if (dist <= pole.radiusKm && dist < minDistance) {
|
||||||
|
minDistance = dist;
|
||||||
|
nearestPole = pole;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nearestPole) {
|
||||||
|
return nearestPole.lines.map(lg => ({
|
||||||
|
...lg,
|
||||||
|
destinations: lg.destinations.map(d => ({
|
||||||
|
...d,
|
||||||
|
waitTimes: computeLiveWaitTimes(d, now)
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Génération contextuelle de secours adaptée au nom de la gare
|
||||||
|
const contextualLines = generateContextualLinesForStation(gtfsGare?.nom || raw);
|
||||||
|
return contextualLines.map(lg => ({
|
||||||
|
...lg,
|
||||||
|
destinations: lg.destinations.map(d => ({
|
||||||
|
...d,
|
||||||
|
waitTimes: computeLiveWaitTimes(d, now)
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -24608,8 +24608,8 @@ const STATION_ALIASES = {
|
|||||||
'aix tgv': '87319012',
|
'aix tgv': '87319012',
|
||||||
'aix en provence tgv': '87319012',
|
'aix en provence tgv': '87319012',
|
||||||
'aix centre': '87754002',
|
'aix centre': '87754002',
|
||||||
'besancon': '87713008',
|
'besancon': '87718007',
|
||||||
'besancon viotte': '87713008',
|
'besancon viotte': '87718007',
|
||||||
'besancon tgv': '87300863',
|
'besancon tgv': '87300863',
|
||||||
'besancon franche comte tgv': '87300863',
|
'besancon franche comte tgv': '87300863',
|
||||||
'dijon': '87713040',
|
'dijon': '87713040',
|
||||||
|
|||||||
+106
-54
@@ -341,49 +341,65 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
|||||||
const currentDay = now.getDay(); // 0 = Dimanche, 1 = Lundi, ..., 6 = Samedi
|
const currentDay = now.getDay(); // 0 = Dimanche, 1 = Lundi, ..., 6 = Samedi
|
||||||
const currentYYYYMMDD = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
|
const currentYYYYMMDD = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
|
||||||
|
|
||||||
// 1. Filtrage précis par date GTFS (si la date du jour est dans la plage de validité)
|
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||||
let circulatingList = rawList.filter(t => {
|
const tomorrowDay = tomorrow.getDay();
|
||||||
// Si la date courante figure dans les dates explicites
|
const tomorrowYYYYMMDD = `${tomorrow.getFullYear()}${String(tomorrow.getMonth() + 1).padStart(2, '0')}${String(tomorrow.getDate()).padStart(2, '0')}`;
|
||||||
if (Array.isArray(t.dates) && t.dates.length > 0 && t.dates.includes(currentYYYYMMDD)) {
|
|
||||||
return true;
|
// Helper de vérification des règles de circulation calendaires
|
||||||
|
const circulatesOnDate = (t, targetDay, targetYYYYMMDD, targetDate) => {
|
||||||
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
const targetDateHyphen = `${targetDate.getFullYear()}-${pad(targetDate.getMonth() + 1)}-${pad(targetDate.getDate())}`;
|
||||||
|
|
||||||
|
// 1. Base GTFS : si t.dates existe (tableau précis de dates YYYYMMDD), c'est la source de vérité absolue
|
||||||
|
if (Array.isArray(t.dates) && t.dates.length > 0) {
|
||||||
|
return t.dates.includes(targetYYYYMMDD) || t.dates.includes(targetDateHyphen);
|
||||||
}
|
}
|
||||||
// Si le jour de la semaine est actif dans la période de validité
|
|
||||||
if (Array.isArray(t.days) && t.days.length > 0 && t.days.includes(currentDay)) {
|
// 2. Base Supabase : jours personnalisés
|
||||||
if ((!t.start || currentYYYYMMDD >= t.start) && (!t.end || currentYYYYMMDD <= t.end)) {
|
const customDaysRaw = t.jours_personnalises;
|
||||||
return true;
|
if (customDaysRaw) {
|
||||||
|
let customDays = [];
|
||||||
|
if (Array.isArray(customDaysRaw)) {
|
||||||
|
customDays = customDaysRaw;
|
||||||
|
} else if (typeof customDaysRaw === 'string') {
|
||||||
|
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
|
||||||
|
}
|
||||||
|
if (Array.isArray(customDays) && customDays.length > 0) {
|
||||||
|
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetYYYYMMDD);
|
||||||
|
if (match) return Boolean(match.circule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Train de nuit parti la veille et arrivant aujourd'hui (heures >= 24h)
|
|
||||||
|
// 3. Colonnes circule_lundi, circule_mardi...
|
||||||
|
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
|
||||||
|
const colName = `circule_${dayNames[targetDay]}`;
|
||||||
|
if (typeof t[colName] !== 'undefined' && t[colName] !== null) {
|
||||||
|
const db = t.date_debut ? String(t.date_debut).slice(0, 10) : null;
|
||||||
|
const df = t.date_fin ? String(t.date_fin).slice(0, 10) : null;
|
||||||
|
if (db && targetDateHyphen < db) return false;
|
||||||
|
if (df && targetDateHyphen > df) return false;
|
||||||
|
return Boolean(t[colName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Jours GTFS (fallback si t.dates n'était pas fourni)
|
||||||
if (Array.isArray(t.days) && t.days.length > 0) {
|
if (Array.isArray(t.days) && t.days.length > 0) {
|
||||||
const prevDay = (currentDay + 6) % 7;
|
if (!t.days.includes(targetDay)) return false;
|
||||||
const rawTime = mode === 'arrivees' ? (t.arr || t.dep) : (t.dep || t.arr);
|
if (t.start && targetYYYYMMDD < String(t.start).replace(/-/g, '')) return false;
|
||||||
const m = String(rawTime).match(/^(\d{1,2})/);
|
if (t.end && targetYYYYMMDD > String(t.end).replace(/-/g, '')) return false;
|
||||||
const rawHour = m ? parseInt(m[1], 10) : 0;
|
|
||||||
if (rawHour >= 24 && t.days.includes(prevDay)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Si aucune règle n'est renseignée
|
|
||||||
if (!t.dates?.length && !t.days?.length) return true;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Fallback robuste : si la date actuelle est hors calendrier GTFS, filtrer par jour de la semaine
|
|
||||||
if (circulatingList.length === 0) {
|
|
||||||
circulatingList = rawList.filter(t => {
|
|
||||||
if (Array.isArray(t.days) && t.days.length > 0) {
|
|
||||||
return t.days.includes(currentDay);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Fallback complet : si toujours vide, renvoyer la grille quotidienne complète
|
// 5. Date unique fixée (date_depart, date, etc.)
|
||||||
if (circulatingList.length === 0) {
|
const singleDate = t.date_depart || t.date || t.service_date;
|
||||||
circulatingList = rawList;
|
if (singleDate) {
|
||||||
}
|
const sStr = String(singleDate).slice(0, 10);
|
||||||
|
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetYYYYMMDD;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper de résolution ultra-robuste de quai par couple (train, gare)
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper de résolution de quai
|
||||||
const lookupPlatform = (trainNum, tripId, stationUic, stationName) => {
|
const lookupPlatform = (trainNum, tripId, stationUic, stationName) => {
|
||||||
const cNum = cleanNum(trainNum);
|
const cNum = cleanNum(trainNum);
|
||||||
const cName = stationName ? formatStationName(stationName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
const cName = stationName ? formatStationName(stationName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||||
@@ -402,19 +418,18 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enrichir chaque train circulant avec le GTFS-RT et le SIRI Lite
|
// Helper d'enrichissement d'un train
|
||||||
const enrichedTrains = circulatingList.map((t, idx) => {
|
const mapTrain = (t, idx, isTomorrow = false) => {
|
||||||
const rawTime = mode === 'arrivees' ? (t.arr || t.dep) : (t.dep || t.arr);
|
const rawTime = mode === 'arrivees' ? (t.arr || t.dep) : (t.dep || t.arr);
|
||||||
const timeHM = parseTimeHM(rawTime) || 0;
|
const timeHM = parseTimeHM(rawTime) || 0;
|
||||||
|
|
||||||
// Recherche de mise à jour GTFS-RT ou SIRI-Lite (par trip_id ou numéro de train)
|
// Recherche de mise à jour GTFS-RT ou SIRI-Lite (valide pour aujourd'hui)
|
||||||
const rt = rtData.updates.get(t.trip_id) || (t.num ? rtData.updates.get(t.num) : null) || (cleanNum(t.num) ? rtData.updates.get(cleanNum(t.num)) : null);
|
const rt = !isTomorrow ? (rtData.updates.get(t.trip_id) || (t.num ? rtData.updates.get(t.num) : null) || (cleanNum(t.num) ? rtData.updates.get(cleanNum(t.num)) : null)) : null;
|
||||||
const siri = siriData.updates.get(t.num) || (cleanNum(t.num) ? siriData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? siriData.updates.get(t.trip_id) : null);
|
const siri = !isTomorrow ? (siriData.updates.get(t.num) || (cleanNum(t.num) ? siriData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? siriData.updates.get(t.trip_id) : null)) : null;
|
||||||
|
|
||||||
const isCanceled = (rt && rt.isCanceled) || (siri && siri.isCanceled) || false;
|
const isCanceled = (rt && rt.isCanceled) || (siri && siri.isCanceled) || false;
|
||||||
const delayMinutes = (rt && rt.delayMinutes > 0) ? rt.delayMinutes : (siri && siri.delayMinutes > 0 ? siri.delayMinutes : 0);
|
const delayMinutes = (rt && rt.delayMinutes > 0) ? rt.delayMinutes : (siri && siri.delayMinutes > 0 ? siri.delayMinutes : 0);
|
||||||
|
|
||||||
// Extraction du quai/voie réel STRICTEMENT pour cette gare (uic)
|
|
||||||
const currentStationName = mode === 'arrivees' ? t.dest : t.orig;
|
const currentStationName = mode === 'arrivees' ? t.dest : t.orig;
|
||||||
let realVoie = lookupPlatform(t.num, t.trip_id, uic, currentStationName);
|
let realVoie = lookupPlatform(t.num, t.trip_id, uic, currentStationName);
|
||||||
if (!realVoie && t.quai && typeof t.quai === 'string') {
|
if (!realVoie && t.quai && typeof t.quai === 'string') {
|
||||||
@@ -431,13 +446,11 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
|||||||
etat = 'retarde';
|
etat = 'retarde';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Causes de retard issues des alertes temps réel
|
|
||||||
let cause = '';
|
let cause = '';
|
||||||
if (delayMinutes > 0 && rtData.alerts.length > 0) {
|
if (!isTomorrow && delayMinutes > 0 && rtData.alerts.length > 0) {
|
||||||
cause = rtData.alerts[idx % rtData.alerts.length].text;
|
cause = rtData.alerts[idx % rtData.alerts.length].text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Formatage des arrêts intermédiaires avec leurs voies respectives (propres à chaque gare)
|
|
||||||
const formattedStops = (t.stops || []).map((st) => {
|
const formattedStops = (t.stops || []).map((st) => {
|
||||||
const stopUic = st.uic || '';
|
const stopUic = st.uic || '';
|
||||||
const stopVoie = lookupPlatform(t.num, t.trip_id, stopUic, st.nom);
|
const stopVoie = lookupPlatform(t.num, t.trip_id, stopUic, st.nom);
|
||||||
@@ -454,7 +467,7 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `gtfs-${t.num || idx}-${rawTime}-${idx}`,
|
id: `gtfs-${isTomorrow ? 'demain-' : ''}${t.num || idx}-${rawTime}-${idx}`,
|
||||||
trip_id: t.trip_id,
|
trip_id: t.trip_id,
|
||||||
numero_train: t.num || String(1000 + idx),
|
numero_train: t.num || String(1000 + idx),
|
||||||
type_train: t.type || 'TER',
|
type_train: t.type || 'TER',
|
||||||
@@ -477,6 +490,7 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
|||||||
stops: formattedStops,
|
stops: formattedStops,
|
||||||
composition: (t.type || '').includes('TGV') ? 'Rame 1 (Voitures 1 à 8) + Rame 2 (Voitures 11 à 18)' : 'Train standard',
|
composition: (t.type || '').includes('TGV') ? 'Rame 1 (Voitures 1 à 8) + Rame 2 (Voitures 11 à 18)' : 'Train standard',
|
||||||
is_substitution: (t.type || '').includes('AUTOCAR'),
|
is_substitution: (t.type || '').includes('AUTOCAR'),
|
||||||
|
_isTomorrow: isTomorrow,
|
||||||
_displayTime: formatTimeHM(rawTime),
|
_displayTime: formatTimeHM(rawTime),
|
||||||
_displayPlatform: realVoie,
|
_displayPlatform: realVoie,
|
||||||
_depHM: timeHM,
|
_depHM: timeHM,
|
||||||
@@ -489,16 +503,54 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
|||||||
causeDetails: cause || null
|
causeDetails: cause || null
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
|
|
||||||
// Tri chronologique : afficher les trains circulant à partir de l'heure courante
|
// 1. Filtrer les trains circulant aujourd'hui
|
||||||
enrichedTrains.sort((a, b) => {
|
let rawToday = rawList.filter(t => circulatesOnDate(t, currentDay, currentYYYYMMDD, now));
|
||||||
const aDiff = ((a._depHM || 0) - currentHM + 1440) % 1440;
|
if (rawToday.length === 0) {
|
||||||
const bDiff = ((b._depHM || 0) - currentHM + 1440) % 1440;
|
// Fallback si pas de calendrier GTFS explicite
|
||||||
return aDiff - bDiff;
|
rawToday = rawList.filter(t => Array.isArray(t.days) && t.days.includes(currentDay));
|
||||||
});
|
if (rawToday.length === 0) rawToday = rawList;
|
||||||
|
}
|
||||||
|
|
||||||
return enrichedTrains;
|
const enrichedToday = rawToday.map((t, idx) => mapTrain(t, idx, false));
|
||||||
|
|
||||||
|
// Filtrer les trains passés : "Une fois que l'heure de départ / Arrivée est dépassée, alors ne plus l'afficher"
|
||||||
|
const remainingToday = enrichedToday
|
||||||
|
.filter(t => {
|
||||||
|
const trainHM = (mode === 'arrivees' ? t._arrHM : t._depHM) || 0;
|
||||||
|
const delay = t.retard_minutes || 0;
|
||||||
|
return (trainHM + delay) >= currentHM;
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aHM = (mode === 'arrivees' ? a._arrHM : a._depHM) || 0;
|
||||||
|
const bHM = (mode === 'arrivees' ? b._arrHM : b._depHM) || 0;
|
||||||
|
return aHM - bHM;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Filtrer les trains circulant demain
|
||||||
|
let rawTomorrow = rawList.filter(t => circulatesOnDate(t, tomorrowDay, tomorrowYYYYMMDD, tomorrow));
|
||||||
|
if (rawTomorrow.length === 0) {
|
||||||
|
rawTomorrow = rawList.filter(t => Array.isArray(t.days) && t.days.includes(tomorrowDay));
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrichedTomorrow = rawTomorrow
|
||||||
|
.map((t, idx) => mapTrain(t, idx, true))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aHM = (mode === 'arrivees' ? a._arrHM : a._depHM) || 0;
|
||||||
|
const bHM = (mode === 'arrivees' ? b._arrHM : b._depHM) || 0;
|
||||||
|
return aHM - bHM;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Logique : s'il reste <= 5 horaires aujourd'hui, afficher ceux d'aujourd'hui + ceux du lendemain
|
||||||
|
let resultList = [];
|
||||||
|
if (remainingToday.length <= 5) {
|
||||||
|
resultList = [...remainingToday, ...enrichedTomorrow];
|
||||||
|
} else {
|
||||||
|
resultList = remainingToday;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultList;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[sncfApi] Erreur de chargement GTFS station:', err);
|
console.warn('[sncfApi] Erreur de chargement GTFS station:', err);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in new issue
Block a user