diff --git a/README.md b/README.md index 58beeac..f3cdde6 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,80 @@ -# Getting Started with Create React App +# 🚅 Afficheurs Ferrovia -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +Application web de tĂ©lĂ©affichage ferroviaire et d'information voyageurs en temps rĂ©el (PIDS - Passenger Information Display Systems), reproduisant fidĂšlement les Ă©crans d'affichage des gares françaises (SNCF). -## Available Scripts +--- -In the project directory, you can run: +## ✹ FonctionnalitĂ©s -### `npm start` +- **📡 DonnĂ©es RĂ©elles & ComplĂštes :** + - Horaires thĂ©oriques issus de l'archive officielle **GTFS SNCF** (3 507 gares, 41 847 circulations : TGV INOUI, OUIGO, TER, INTERCITÉS, ICE, TGV Lyria). + - Retards, perturbations et suppressions en direct via **GTFS-RT**. + - DĂ©tection et affichage des **voies et quais rĂ©els** via les flux **SIRI-Lite**. +- **đŸ–„ïž Multiples GĂ©nĂ©rations d'Afficheurs :** + - **AFL (Afficheurs Fluides & Lumineux) :** Écrans modernes pour halls et quais (DĂ©parts / ArrivĂ©es). + - **Classiques :** Écrans d'affichage historiques (DĂ©parts, ArrivĂ©es, Écrans de Quai avec liste des arrĂȘts desservis). + - **EVA :** Écrans vidĂ©o voyageurs enrichis. +- **🔊 Module d'Annonces Sonores :** Jingles et annonces synthĂ©tiques en temps rĂ©el. +- **🔍 Recherche Intelligente des Gares :** Reconnaissance instantanĂ©e des gares, codes UIC, variantes d'Ă©criture et abrĂ©viations (`Valence-TGV`, `Paris-Montparnasse`, `Lyon-Part-Dieu`, etc.). -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. +--- -The page will reload when you make changes.\ -You may also see any lint errors in the console. +## 📋 Patchnotes / Historique des Versions -### `npm test` +Pour consulter le journal dĂ©taillĂ© de toutes les versions, consultez le fichier **[CHANGELOG.md](./CHANGELOG.md)**. -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +
+

🚀 Version 4.0.0 (V4) - Derniùre version

-### `npm run build` +- **IntĂ©gration GTFS & GTFS-RT Exhaustive :** 3 507 gares et 41 847 circulations rĂ©elles intĂ©grĂ©es sans donnĂ©es mockĂ©es. +- **Quais RĂ©els SIRI-Lite :** Extraction en continu des quais rĂ©els avec isolation stricte par gare sĂ©lectionnĂ©e et par arrĂȘt. +- **Recherche Intelligente :** Algorithme tolĂ©rant aux tirets, accents et alias pour tous les noms de gares. +- **Normalisation des Horaires :** Prise en charge des trains de nuit et horaires modulo 24h. +- **Codebase Propre :** RĂ©solution complĂšte de tous les avertissements ESLint (`Compiled successfully`). -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +
-The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! +
+

🚆 Version 3.0.0 (V3)

-See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. +- Nouveaux types d'afficheurs AFL, Classiques et EVA. +- Module d'annonces sonores en temps réel. +- Gestion avancée des trains combinés / multi-tranches et des substitutions par autocar. -### `npm run eject` +
-**Note: this is a one-way operation. Once you `eject`, you can't go back!** +
+

🚉 Version 2.0.0 (V2)

-If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. +- Écrans dĂ©diĂ©s par voie et quai avec liste dĂ©roulante des arrĂȘts aval. +- Alternance dynamique des Ă©tats de circulation et synchronisation automatique. -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. +
-You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. +
+

🏁 Version 1.0.0 (V1)

-## Learn More +- Lancement initial de la plateforme avec horloge temps réel et reproduction de la signalétique ferroviaire. -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). +
-To learn React, check out the [React documentation](https://reactjs.org/). +--- -### Code Splitting +## đŸ› ïž Installation & DĂ©marrage -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) +```bash +# Installer les dĂ©pendances +npm install -### Analyzing the Bundle Size +# Lancer le serveur de dĂ©veloppement (http://localhost:3000) +npm start -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) +# Compiler pour la production +npm run build +``` -### Making a Progressive Web App +--- -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) +## 📄 Licence -### Advanced Configuration - -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) - -### Deployment - -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) - -### `npm run build` fails to minify - -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) +Projet dĂ©veloppĂ© pour la simulation et la restitution d'Ă©crans d'information ferroviaire. DonnĂ©es fournies sous licence ouverte via transport.data.gouv.fr et SNCF Open Data. diff --git a/package-lock.json b/package-lock.json index a37e355..456cf6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,8 @@ "@testing-library/user-event": "^13.5.0", "autoprefixer": "^10.4.27", "eslint-plugin-react-hooks": "^7.0.1", + "gtfs-realtime-bindings": "^2.2.0", + "protobufjs": "^8.8.0", "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^6.30.3", @@ -2872,6 +2874,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdoc/salty": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.12.tgz", + "integrity": "sha512-TuB0x50EoAvEX/UEWITd8Mkn3WhiTjSvbTMCLj0BhsQEl5iUzjXdA0bETEVpTk+5TGTLR6QktI9H4hLviVeaAQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.18.1" + }, + "engines": { + "node": ">=v12.0.0" + } + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -3778,6 +3792,28 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -5606,6 +5642,18 @@ "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "license": "Apache-2.0" }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8872,6 +8920,18 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "license": "MIT" }, + "node_modules/gtfs-realtime-bindings": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gtfs-realtime-bindings/-/gtfs-realtime-bindings-2.2.0.tgz", + "integrity": "sha512-JX6aADdPWtKMXA3Z/G2TK2kgR/gXZFFpfL7cdlR2fYvnJK5JDiyfEGC4+/65dF591d8nooUjhM387OSdesAIDQ==", + "dependencies": { + "protobufjs": "^8.2.1", + "protobufjs-cli": "^2.2.1" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -11079,12 +11139,71 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "license": "Apache-2.0", + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "license": "MIT" }, + "node_modules/jsdoc": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz", + "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", + "license": "Apache-2.0", + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jsdoc/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jsdom": { "version": "16.7.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", @@ -11283,6 +11402,15 @@ "node": ">=0.10.0" } }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -11366,6 +11494,25 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/loader-runner": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", @@ -11406,9 +11553,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash-es": { @@ -11447,6 +11594,12 @@ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -11528,6 +11681,73 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-it": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.1.tgz", + "integrity": "sha512-4Ej49aYTDFIQ+uBkfX8GBvJGccoARxxPep+7aWTs55ozbjQJpW9M26Fe53vnGgvLeVzva/amzjQQaQu9w0vMhA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "license": "Unlicense", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -11543,6 +11763,12 @@ "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", "license": "CC0-1.0" }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "license": "MIT" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -13818,6 +14044,85 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/protobufjs": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz", + "integrity": "sha512-N3xhQ5yyBx3vQq4gubBfASzYhJGNzeDbjqBpu61g7UVylsN/qyffU96TKWD3GbbLOKF82VGNRNvv1+BFgE31Eg==", + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs-cli": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-2.7.0.tgz", + "integrity": "sha512-wjrZPibf/U73dDJMh9jCqjyeDH7t6+OMD16vKp/Z1CY7yyVE/pX5a9EvCq89p3ADHo+KyaLknA5lxyaaM252KA==", + "license": "BSD-3-Clause", + "dependencies": { + "escodegen": "^2.0.0", + "espree": "^9.6.1", + "estraverse": "^5.1.0", + "glob": "^8.1.0", + "jsdoc": "^4.0.5", + "minimist": "^1.2.8", + "tmp": "^0.2.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts", + "protoc-gen-pbjs": "bin/protoc-gen-pbjs" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^8.8.0" + } + }, + "node_modules/protobufjs-cli/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/protobufjs-cli/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/protobufjs-cli/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -13861,6 +14166,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -14527,6 +14841,15 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "license": "MIT" }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -16330,6 +16653,15 @@ "@popperjs/core": "^2.9.0" } }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -16626,6 +16958,12 @@ "node": ">=4.2.0" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -17772,6 +18110,12 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "license": "Apache-2.0" + }, "node_modules/xstate": { "version": "4.38.3", "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.38.3.tgz", diff --git a/package.json b/package.json index 722197f..2ad3735 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "autoprefixer": "^10.4.27", "dependencies": "^0.0.1", "eslint-plugin-react-hooks": "^7.0.1", + "gtfs-realtime-bindings": "^2.2.0", + "protobufjs": "^8.8.0", "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^6.30.3", diff --git a/src/App.js b/src/App.js index 88e4378..0053e8f 100644 --- a/src/App.js +++ b/src/App.js @@ -8,8 +8,7 @@ import Displays from './pages/Displays'; import ClassiquesDeparts from './pages/afficheurs/classiques/ClassiquesDeparts'; import ClassiquesArrivees from './pages/afficheurs/classiques/ClassiquesArrivees'; import ClassiquesDepartsQuais from './pages/afficheurs/classiques/quais/ClassiquesDepartsQuais'; -import sessionMemory from './lib/sessionMemory'; -import ClassiquesArriveesQuais from "./pages/afficheurs/classiques/quais/ClassiquesArriveesQuais"; +import ClassiquesArriveesQuais from './pages/afficheurs/classiques/quais/ClassiquesArriveesQuais'; import AFLDeparts from './pages/afficheurs/afl/AFLDeparts'; import AFLArrivees from './pages/afficheurs/afl/AFLArrivees'; import AFLDepartsQuais from './pages/afficheurs/afl/AFLDepartsQuais'; @@ -18,19 +17,19 @@ import EVAArrivees from './pages/afficheurs/eva/EVAArrivees'; import EVADeparts from './pages/afficheurs/eva/EVADeparts'; import SelectAfficheur from './pages/SelectAfficheur'; import AfficheurViewportWrapper from './components/AfficheurViewportWrapper'; +import sessionMemory from './lib/sessionMemory'; +import { getSourceFromPath } from './services/railwayService'; + applyPolyfills().then(() => defineCustomElements(window)); // Petit routeur client sans dĂ©pendances externes function useSimpleRouter() { function normalizePath(p) { if (!p) return '/'; - // retirer query string et hash pour normaliser le chemin utilisĂ© en internal route + // retirer query string et hash p = String(p).split(/[?#]/)[0]; - // traiter les cas comme /index.html if (p.indexOf('index.html') !== -1) return '/'; - // supprimer espaces en dĂ©but/fin p = p.trim(); - // supprimer slashs finaux sauf pour la racine if (p !== '/') p = p.replace(/\/+$/, ''); return p || '/'; } @@ -49,24 +48,16 @@ function useSimpleRouter() { function navigate(path) { const target = normalizePath(path); - // toujours pousser l'URL complĂšte (path peut contenir query/hash), - // mais stocker en internal state la version normalisĂ©e (sans query/hash) - // comparer l'URL complĂšte (pathname + search + hash) afin de permettre - // des navigations qui ne changent que la querystring (ex. ?gare=...) const currentFull = window.location.pathname + (window.location.search || '') + (window.location.hash || ''); if (currentFull !== path) { console.debug('[router] navigate ->', path, '(normalized ->', target, ')'); try { window.history.pushState({}, '', path); } catch (e) { - // fallback si path est invalide - try { window.history.pushState({}, '', target); } catch (err) { /* ignore */ } + try { window.history.pushState({}, '', target); } catch (err) {} } - // mettre Ă  jour la route interne (sans query/hash) setRoute(target); } else { - // si l'URL complĂšte est identique, forcer un update de state pour - // dĂ©clencher un rendu si besoin (rare) setRoute(target); } } @@ -77,122 +68,160 @@ function useSimpleRouter() { function App() { const { route, navigate } = useSimpleRouter(); + const source = getSourceFromPath(route); + const prefix = source === 'supabase' ? '/ferrovia' : ''; + const cleanRoute = (route.replace(/^\/ferrovia/, '') || '/'); + // Composant Home explicite (panels cliquables) - function Home({ navigate }) { - useEffect(() => { - console.debug('[Home] montĂ©, pathname =', window.location.pathname); - }, []); + function Home({ navigate, prefix, source }) { const panelStyle = { - width: '40%', + width: '42%', cursor: 'pointer', padding: 24, - borderRadius: 8, - boxShadow: '0 2px 8px rgba(0,0,0,0.12)', + borderRadius: 10, + boxShadow: '0 4px 12px rgba(0,0,0,0.08)', background: '#fff', - textAlign: 'center' + textAlign: 'center', + border: '1px solid #e0e8f6', + transition: 'transform 0.15s ease' }; return ( -
-
-

Accueil

-

Choisissez une section ci-dessous

-
-
-
{ try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate('/departs'); }} - onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate('/departs'); })() : null)} - style={panelStyle} - aria-label="Accéder aux départs" - > -

Départs

-

Consultez les prochains départs d'une gare. Cliquez pour choisir une gare et voir les horaires.

+
+
+
+ {source === 'supabase' ? 'Mode Base de Données (Ferrovia)' : 'Mode Officiel SNCF (GTFS / Temps Réel)'} +
+

Accueil

+

+ {source === 'supabase' + ? "Consultez les afficheurs basés sur les données de votre BDD Supabase." + : "Consultez les afficheurs connectés aux flux réels GTFS et Open Data SNCF."} +

-
{ try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} navigate('/arrivees'); }} - onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} navigate('/arrivees'); })() : null)} - style={panelStyle} - aria-label="Accéder aux arrivées" - > -

Arrivées

-

Consultez les prochaines arrivées d'une gare. Cliquez pour choisir une gare et voir les informations.

-
+
+
{ try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate(`${prefix}/departs`); }} + onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate(`${prefix}/departs`); })() : null)} + style={panelStyle} + aria-label="Accéder aux départs" + > +

🚆 DĂ©parts

+

Consultez les prochains départs d'une gare. Choisissez une gare et le style d'afficheur.

+
+ +
{ try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} navigate(`${prefix}/arrivees`); }} + onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} navigate(`${prefix}/arrivees`); })() : null)} + style={panelStyle} + aria-label="Accéder aux arrivées" + > +

🏁 ArrivĂ©es

+

Consultez les prochaines arrivées d'une gare en temps réel.

+
); } - // Routes pour afficheurs : format /afficheurs/{mode}/{type}?gare=... - // route plus spécifique pour l'afficheur "quais" - if (route.startsWith('/afficheurs/departs/classiques/quais')) { + // Routes pour afficheurs : supporte le mode racine ET le mode /ferrovia + if (cleanRoute.startsWith('/afficheurs/departs/classiques/quais')) { return ; } - if (route.startsWith('/afficheurs/arrivees/classiques/quais')) { + if (cleanRoute.startsWith('/afficheurs/arrivees/classiques/quais')) { return ; } - if (route.startsWith('/afficheurs/departs/classiques')) { + if (cleanRoute.startsWith('/afficheurs/departs/classiques')) { return ; } - if (route.startsWith('/afficheurs/departs/afl/quais')) { + if (cleanRoute.startsWith('/afficheurs/departs/afl/quais')) { return ; } - if (route.startsWith('/afficheurs/departs/afl')) { + if (cleanRoute.startsWith('/afficheurs/departs/afl')) { return ; } - if (route.startsWith('/afficheurs/departs/eva')) { + if (cleanRoute.startsWith('/afficheurs/departs/eva')) { return ; } - if (route.startsWith('/afficheurs/arrivees/afl/quais')) { + if (cleanRoute.startsWith('/afficheurs/arrivees/afl/quais')) { return ; } - if (route.startsWith('/afficheurs/arrivees/afl')) { + if (cleanRoute.startsWith('/afficheurs/arrivees/afl')) { return ; } - if (route.startsWith('/afficheurs/arrivees/eva')) { + if (cleanRoute.startsWith('/afficheurs/arrivees/eva')) { return ; } - if (route.startsWith('/afficheurs/arrivees/classiques')) { + if (cleanRoute.startsWith('/afficheurs/arrivees/classiques')) { return ; } let content; - if (route === '/departs') { - content = navigate('/')} navigate={navigate} />; - } else if (route === '/arrivees') { - content = navigate('/')} navigate={navigate} />; - } else if (route === '/select-afficheur') { + if (cleanRoute === '/departs') { + content = navigate(prefix || '/')} navigate={navigate} />; + } else if (cleanRoute === '/arrivees') { + content = navigate(prefix || '/')} navigate={navigate} />; + } else if (cleanRoute === '/select-afficheur') { content = { const params = new URLSearchParams(window.location.search); const gare = params.get('gare') || ''; const mode = params.get('mode') || 'departs'; - navigate(`/afficheurs/${encodeURIComponent(gare)}?mode=${mode}`); + navigate(`${prefix}/afficheurs/${encodeURIComponent(gare)}?mode=${mode}`); }} navigate={navigate} />; - } else if (route.startsWith('/afficheurs')) { - content = navigate('/')} navigate={navigate} />; + } else if (cleanRoute.startsWith('/afficheurs')) { + content = navigate(prefix || '/')} navigate={navigate} />; } else { - content = ; + content = ; } return (
- { e.preventDefault(); navigate('/departs'); }}>DĂ©parts + { e.preventDefault(); navigate(`${prefix}/departs`); }}>DĂ©parts - { e.preventDefault(); navigate('/arrivees'); }}>ArrivĂ©es + { e.preventDefault(); navigate(`${prefix}/arrivees`); }}>ArrivĂ©es + + + {source === 'supabase' ? ( + { e.preventDefault(); navigate('/'); }} + style={{ color: '#008b47', fontWeight: 600 }} + title="Basculer vers le mode SNCF GTFS" + > + 🌐 Passer en mode SNCF Live + + ) : ( + { e.preventDefault(); navigate('/ferrovia'); }} + style={{ color: '#00439e', fontWeight: 600 }} + title="Basculer vers la Base de DonnĂ©es Supabase" + > + đŸ—„ïž Passer en mode BDD Ferrovia + + )} -
-
-

Affichage Ferroviaire — Accueil

-
Route actuelle: {route}
+
+
+
+

+ {source === 'supabase' ? 'Affichage Ferroviaire — BDD Supabase (/ferrovia)' : 'Affichage Ferroviaire — SNCF GTFS / Temps RĂ©el'} +

+
Route actuelle: {route}
+
+
+ Source: {source === 'supabase' ? 'Supabase Database' : 'SNCF Open Data / GTFS'} +
diff --git a/src/pages/Arrivees.js b/src/pages/Arrivees.js index cf13063..c5957b2 100644 --- a/src/pages/Arrivees.js +++ b/src/pages/Arrivees.js @@ -1,102 +1,168 @@ -import React, { useEffect, useState } from 'react'; -import supabase from '../lib/supabase'; +import React, { useEffect, useState, useMemo } from 'react'; import sessionMemory from '../lib/sessionMemory'; +import { getSourceFromPath, searchGares } from '../services/railwayService'; export default function Arrivees({ onBack, navigate }) { - const [gares, setGares] = useState([]); - const [loading, setLoading] = useState(true); + const source = getSourceFromPath(window.location.pathname); + const prefix = source === 'supabase' ? '/ferrovia' : ''; + + const [allGares, setAllGares] = useState([]); + const [sncfResults, setSncfResults] = useState([]); + const [loading, setLoading] = useState(source === 'supabase'); const [error, setError] = useState(null); const [selected, setSelected] = useState(''); const [search, setSearch] = useState(''); + // Pour Supabase : charger les gares une seule fois au montage useEffect(() => { let mounted = true; - - async function loadGares() { - setLoading(true); - setError(null); - if (!supabase) { - if (mounted) { - setError( - "Supabase non initialisé : vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY dans votre .env et redémarrez le serveur de développement (npm start)." - ); - setLoading(false); + if (source === 'supabase') { + async function loadSupabaseGares() { + setLoading(true); + setError(null); + try { + const data = await searchGares('', 'supabase'); + if (mounted) setAllGares(Array.isArray(data) ? data.map(r => r.nom || r) : []); + } catch (err) { + console.error('Supabase error:', err); + if (mounted) setError(err.message || String(err)); + } finally { + if (mounted) setLoading(false); } + } + loadSupabaseGares(); + } + return () => { mounted = false; }; + }, [source]); + + // Pour SNCF : recherche dynamique avec debounce dÚs 2 caractÚres + useEffect(() => { + let active = true; + if (source === 'sncf') { + const q = search.trim(); + if (q.length < 2) { + setSncfResults([]); return; } - try { - const { data, error } = await supabase.from('gares').select('nom'); - if (error) throw error; - if (mounted) setGares(Array.isArray(data) ? data.map((r) => r.nom) : []); - } catch (err) { - console.error('Supabase error:', err); - if (mounted) setError(err.message || String(err)); - } finally { - if (mounted) setLoading(false); - } + const timer = setTimeout(async () => { + try { + const results = await searchGares(q, 'sncf'); + if (active) setSncfResults(results); + } catch (e) { + if (active) setSncfResults([]); + } + }, 200); + + return () => { + active = false; + clearTimeout(timer); + }; } + }, [search, source]); - loadGares(); + // Liste finale affichée + const displayedGares = useMemo(() => { + if (source === 'supabase') { + const q = search.toLowerCase().trim(); + return allGares.filter(nom => String(nom).toLowerCase().includes(q)).map(nom => ({ nom })); + } + return sncfResults; + }, [source, allGares, sncfResults, search]); - return () => { - mounted = false; - }; - }, []); + const handleSelectGare = (nom) => { + setSelected(nom); + try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} + if (navigate) { + navigate(`${prefix}/afficheurs/${encodeURIComponent(nom)}?mode=arrivees`); + } + }; return (
-

Arrivées

+
+

Arrivées

+ + {source === 'supabase' ? 'đŸ—„ïž BDD Ferrovia' : '🌐 SNCF (GTFS)'} + +
{loading &&

Chargement des gares...

} {error &&

Erreur : {error}

} {!loading && !error && (
- {/* Barre de recherche centrée */} + {/* Barre de recherche */}
setSearch(e.target.value)} style={{ width: '60%', padding: '10px 12px', borderRadius: 6, border: '1px solid #ccc' }} aria-label="Rechercher une gare" + autoFocus />
- {/* Résultats sous forme de lignes */} -
- {gares - .filter((nom) => nom.toLowerCase().includes(search.trim().toLowerCase())) - .map((nom) => ( -
+ {/* Invitation Ă  la recherche en mode SNCF */} + {source === 'sncf' && search.trim().length < 2 && ( +
+

+ 🔍 Saisissez au moins 2 caractĂšres pour rechercher parmi les gares du rĂ©fĂ©rentiel GTFS SNCF. +

+
+ )} + + {/* Aucun résultat */} + {source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && ( +
+ Aucune gare SNCF trouvée pour "{search}". +
+ )} + + {/* Résultats */} + {displayedGares.length > 0 && ( +
+ {displayedGares.map((g) => { + const nom = g.nom; + return ( +
{ - setSelected(nom); - try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} - if (navigate) navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=arrivees`); - }} - onKeyDown={(e) => (e.key === 'Enter' ? (navigate ? navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=arrivees`) : setSelected(nom)) : null)} - tabIndex={0} - > -
{nom}
- {selected === nom &&
Sélectionnée
} + style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 0', cursor: 'pointer' }} + onClick={() => handleSelectGare(nom)} + onKeyDown={(e) => (e.key === 'Enter' ? handleSelectGare(nom) : null)} + tabIndex={0} + > +
+ {nom} + {g.commune && ({g.commune} - {g.departement})} +
+
+ {g.uic && UIC {g.uic}} + {selected === nom &&
Sélectionnée
} +
+
+
-
-
- ))} -
+ ); + })} +
+ )} {selected && ( -
+

Gare sélectionnée : {selected}

@@ -110,5 +176,3 @@ export default function Arrivees({ onBack, navigate }) {
); } - - diff --git a/src/pages/Departs.js b/src/pages/Departs.js index f5a5059..148aa4f 100644 --- a/src/pages/Departs.js +++ b/src/pages/Departs.js @@ -1,103 +1,168 @@ -import React, { useEffect, useState } from 'react'; -import supabase from '../lib/supabase'; +import React, { useEffect, useState, useMemo } from 'react'; import sessionMemory from '../lib/sessionMemory'; +import { getSourceFromPath, searchGares } from '../services/railwayService'; export default function Departs({ onBack, navigate }) { - const [gares, setGares] = useState([]); - const [loading, setLoading] = useState(true); + const source = getSourceFromPath(window.location.pathname); + const prefix = source === 'supabase' ? '/ferrovia' : ''; + + const [allGares, setAllGares] = useState([]); + const [sncfResults, setSncfResults] = useState([]); + const [loading, setLoading] = useState(source === 'supabase'); const [error, setError] = useState(null); const [selected, setSelected] = useState(''); const [search, setSearch] = useState(''); + // Pour Supabase : charger les gares une seule fois au montage useEffect(() => { let mounted = true; - - async function loadGares() { - setLoading(true); - setError(null); - if (!supabase) { - // Le client Supabase n'a pas pu ĂȘtre initialisĂ© (variables d'env manquantes) - if (mounted) { - setError( - "Supabase non initialisĂ© : vĂ©rifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY dans votre .env et redĂ©marrez le serveur de dĂ©veloppement (npm start)." - ); - setLoading(false); + if (source === 'supabase') { + async function loadSupabaseGares() { + setLoading(true); + setError(null); + try { + const data = await searchGares('', 'supabase'); + if (mounted) setAllGares(Array.isArray(data) ? data.map(r => r.nom || r) : []); + } catch (err) { + console.error('Supabase error:', err); + if (mounted) setError(err.message || String(err)); + } finally { + if (mounted) setLoading(false); } + } + loadSupabaseGares(); + } + return () => { mounted = false; }; + }, [source]); + + // Pour SNCF : recherche dynamique avec debounce dĂšs 2 caractĂšres + useEffect(() => { + let active = true; + if (source === 'sncf') { + const q = search.trim(); + if (q.length < 2) { + setSncfResults([]); return; } - try { - const { data, error } = await supabase.from('gares').select('nom'); - if (error) throw error; - if (mounted) setGares(Array.isArray(data) ? data.map((r) => r.nom) : []); - } catch (err) { - console.error('Supabase error:', err); - if (mounted) setError(err.message || String(err)); - } finally { - if (mounted) setLoading(false); - } + const timer = setTimeout(async () => { + try { + const results = await searchGares(q, 'sncf'); + if (active) setSncfResults(results); + } catch (e) { + if (active) setSncfResults([]); + } + }, 200); + + return () => { + active = false; + clearTimeout(timer); + }; } + }, [search, source]); - loadGares(); + // Liste finale affichĂ©e + const displayedGares = useMemo(() => { + if (source === 'supabase') { + const q = search.toLowerCase().trim(); + return allGares.filter(nom => String(nom).toLowerCase().includes(q)).map(nom => ({ nom })); + } + return sncfResults; + }, [source, allGares, sncfResults, search]); - return () => { - mounted = false; - }; - }, []); + const handleSelectGare = (nom) => { + setSelected(nom); + try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} + if (navigate) { + navigate(`${prefix}/afficheurs/${encodeURIComponent(nom)}?mode=departs`); + } + }; return (
-

Départs

+
+

Départs

+ + {source === 'supabase' ? 'đŸ—„ïž BDD Ferrovia' : '🌐 SNCF (GTFS)'} + +
{loading &&

Chargement des gares...

} {error &&

Erreur : {error}

} {!loading && !error && (
- {/* Barre de recherche centrée */} + {/* Barre de recherche */}
setSearch(e.target.value)} style={{ width: '60%', padding: '10px 12px', borderRadius: 6, border: '1px solid #ccc' }} aria-label="Rechercher une gare" + autoFocus />
- {/* Résultats sous forme de lignes */} -
- {gares - .filter((nom) => nom.toLowerCase().includes(search.trim().toLowerCase())) - .map((nom) => ( -
-
{ - setSelected(nom); - try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} - if (navigate) navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=departs`); - }} - onKeyDown={(e) => (e.key === 'Enter' ? (navigate ? navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=departs`) : setSelected(nom)) : null)} - tabIndex={0} - > -
{nom}
- {selected === nom &&
Sélectionnée
} + {/* Invitation Ă  la recherche en mode SNCF */} + {source === 'sncf' && search.trim().length < 2 && ( +
+

+ 🔍 Saisissez au moins 2 caractĂšres pour rechercher parmi les gares du rĂ©fĂ©rentiel GTFS SNCF. +

+
+ )} + + {/* Aucun résultat */} + {source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && ( +
+ Aucune gare SNCF trouvée pour "{search}". +
+ )} + + {/* Résultats */} + {displayedGares.length > 0 && ( +
+ {displayedGares.map((g) => { + const nom = g.nom; + return ( +
+
handleSelectGare(nom)} + onKeyDown={(e) => (e.key === 'Enter' ? handleSelectGare(nom) : null)} + tabIndex={0} + > +
+ {nom} + {g.commune && ({g.commune} - {g.departement})} +
+
+ {g.uic && UIC {g.uic}} + {selected === nom &&
Sélectionnée
} +
+
+
-
-
- ))} -
+ ); + })} +
+ )} {selected && ( -
+

Gare sélectionnée : {selected}

@@ -111,5 +176,3 @@ export default function Departs({ onBack, navigate }) {
); } - - diff --git a/src/pages/Displays.js b/src/pages/Displays.js index cf6c0e8..d1f0e8c 100644 --- a/src/pages/Displays.js +++ b/src/pages/Displays.js @@ -1,8 +1,12 @@ import React from 'react'; import WcsButton from '../components/WcsButton'; import sessionMemory from '../lib/sessionMemory'; +import { getSourceFromPath } from '../services/railwayService'; export default function Displays({ onBack, navigate }) { + const source = getSourceFromPath(window.location.pathname); + const prefix = source === 'supabase' ? '/ferrovia' : ''; + const types = [ { id: 'classiques', label: 'Classiques' }, { id: 'eva', label: 'EVA' }, @@ -10,25 +14,23 @@ export default function Displays({ onBack, navigate }) { { id: 'transilien', label: 'TRANSILIEN' } ]; - // RĂ©cupĂ©rer la gare : soit dans le pathname (/afficheurs/), - // soit via la querystring ?gare=... si la route est dĂ©jĂ  /afficheurs// - const parts = typeof window !== 'undefined' ? window.location.pathname.split('/').filter(Boolean) : []; + // Nettoyage du chemin pour extraire la gare mĂȘme avec /ferrovia + const cleanPath = (typeof window !== 'undefined' ? window.location.pathname : '').replace(/^\/ferrovia/, ''); + const parts = cleanPath.split('/').filter(Boolean); const typeIds = types.map(t => t.id); let gare = null; + if (parts[0] === 'afficheurs' && parts[1] && !typeIds.includes(parts[1])) { - // cas /afficheurs/ try { gare = decodeURIComponent(parts.slice(1).join('/')); } catch (e) { gare = parts.slice(1).join('/'); } } else { - // si l'URL est /afficheurs//, regarder dans la querystring ?gare=... const paramsForGare = new URLSearchParams(window.location.search || ''); const g = paramsForGare.get('gare'); if (g) { try { - // dĂ©coder puis remplacer les tirets par des espaces pour l'affichage gare = decodeURIComponent(String(g).replace(/\+/g, ' ')).replace(/-/g, ' '); } catch (e) { gare = String(g).replace(/-/g, ' '); @@ -36,37 +38,52 @@ export default function Displays({ onBack, navigate }) { } } - // dĂ©terminer le mode (departs/arrivees) : querystring > localStorage > default('departs') + // DĂ©terminer le mode (departs/arrivees) const params = new URLSearchParams(window.location.search || ''); let mode = params.get('mode'); if (!mode) { try { const stored = sessionMemory.get('afficheurMode'); if (stored) mode = stored; - } catch (e) { - // ignore - } + } catch (e) {} } if (!mode) mode = 'departs'; return (
-

Choisissez un type d'afficheur

+
+

Choisissez un type d'afficheur

+ + {source === 'supabase' ? 'đŸ—„ïž BDD Ferrovia' : '🌐 SNCF (GTFS)'} + +
+ {gare &&

Gare sélectionnée : {gare}

}

Mode sélectionné : {mode === 'arrivees' ? 'Arrivées' : 'Départs'}

- {/* Boutons directs pour afficher les afficheurs classiques supprimés */} -
{types.map((t) => ( -
{ - if (typeof navigate === 'function') { - const kebab = gare ? String(gare).trim().replace(/\s+/g, '-').replace(/\//g, '/') : ''; - navigate(`/select-afficheur?gare=${encodeURIComponent(kebab)}&mode=${mode}&type=${t.id}`); - return; - } - alert(`${t.label} pour ${gare || '—'}`); - }}> +
{ + if (typeof navigate === 'function') { + const kebab = gare ? String(gare).trim().replace(/\s+/g, '-').replace(/\//g, '/') : ''; + navigate(`${prefix}/select-afficheur?gare=${encodeURIComponent(kebab)}&mode=${mode}&type=${t.id}`); + return; + } + alert(`${t.label} pour ${gare || '—'}`); + }} + >

{t.label}

Afficher le style {t.label} pour la gare {gare || ''}.

@@ -74,29 +91,21 @@ export default function Displays({ onBack, navigate }) {
- {/* Bouton de retour au style WCS SNCF */} { - // Appelle le callback fourni par le routeur simple si disponible try { if (typeof onBack === 'function') { onBack(); return; } - } catch (e) { - // ignore - } + } catch (e) {} - // Fallback : si l'historique du navigateur permet un back, on l'utilise if (typeof window !== 'undefined' && window.history && window.history.length > 1) { window.history.back(); } else { - // Redirection sûre vers la racine - window.location.href = '/'; + window.location.href = prefix || '/'; } }} label="Retour" @@ -105,4 +114,3 @@ export default function Displays({ onBack, navigate }) {
); } - diff --git a/src/pages/SelectAfficheur.js b/src/pages/SelectAfficheur.js index 0246b06..9b754da 100644 --- a/src/pages/SelectAfficheur.js +++ b/src/pages/SelectAfficheur.js @@ -1,8 +1,11 @@ import React, { useEffect, useState } from 'react'; -import supabase from '../lib/supabase'; import WcsButton from '../components/WcsButton'; +import { getSourceFromPath, getPlatformsForStation } from '../services/railwayService'; export default function SelectAfficheur({ onBack, navigate }) { + const source = getSourceFromPath(window.location.pathname); + const prefix = source === 'supabase' ? '/ferrovia' : ''; + const params = new URLSearchParams(window.location.search || ''); const gareParam = params.get('gare') || ''; const modeParam = params.get('mode') || 'departs'; @@ -21,50 +24,13 @@ export default function SelectAfficheur({ onBack, navigate }) { useEffect(() => { let mounted = true; async function loadPlatforms() { - if (!supabase) { - setLoading(false); - return; - } try { setLoading(true); - const { data, error } = await supabase.from('horaires').select('quai_reel, voie, quai, gare_depart_nom, gare_arrivee_nom, gares_desservies'); - if (error) throw error; + const sortedPlatforms = await getPlatformsForStation(decodedGare || gareParam, source); if (!mounted) return; - const platformSet = new Set(); - const normGare = decodedGare.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim(); - - (data || []).forEach(h => { - let stopsList = []; - if (Array.isArray(h.gares_desservies)) { - stopsList = h.gares_desservies; - } else if (typeof h.gares_desservies === 'string') { - try { stopsList = JSON.parse(h.gares_desservies); } catch (e) {} - } - if (!Array.isArray(stopsList)) stopsList = []; - - let foundStop = stopsList.find(s => { - const name = (s.gare_nom || s.nom || s.name || '').toString().normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim(); - return name === normGare || name.includes(normGare) || normGare.includes(name); - }); - - if (foundStop) { - const platform = (foundStop.quai_reel || foundStop.quai || foundStop.platform || foundStop.quay || '').toString().trim(); - if (platform) platformSet.add(platform); - } else { - const origin = (h.gare_depart_nom || '').toString().normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim(); - const dest = (h.gare_arrivee_nom || '').toString().normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim(); - if (origin === normGare || origin.includes(normGare) || normGare.includes(origin) || - dest === normGare || dest.includes(normGare) || normGare.includes(dest)) { - const platform = (h.quai_reel || h.voie || h.quai || '').toString().trim(); - if (platform) platformSet.add(platform); - } - } - }); - - const sortedPlatforms = Array.from(platformSet).sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' })); - setPlatforms(sortedPlatforms); - if (sortedPlatforms.length > 0) { + setPlatforms(sortedPlatforms || []); + if (sortedPlatforms && sortedPlatforms.length > 0) { setSelectedPlatform(sortedPlatforms[0]); } } catch (err) { @@ -78,12 +44,12 @@ export default function SelectAfficheur({ onBack, navigate }) { return () => { mounted = false; }; - }, [gareParam, decodedGare]); + }, [gareParam, decodedGare, source]); const handleGeneral = () => { if (typeof navigate === 'function') { const kebab = gareParam ? String(gareParam).trim() : ''; - navigate(`/afficheurs/${modeParam}/${typeParam}?gare=${encodeURIComponent(kebab)}`); + navigate(`${prefix}/afficheurs/${modeParam}/${typeParam}?gare=${encodeURIComponent(kebab)}`); } }; @@ -95,7 +61,7 @@ export default function SelectAfficheur({ onBack, navigate }) { } if (typeof navigate === 'function') { const kebab = gareParam ? String(gareParam).trim() : ''; - navigate(`/afficheurs/${modeParam}/${typeParam}/quais?gare=${encodeURIComponent(kebab)}&quai=${encodeURIComponent(quai)}`); + navigate(`${prefix}/afficheurs/${modeParam}/${typeParam}/quais?gare=${encodeURIComponent(kebab)}&quai=${encodeURIComponent(quai)}`); } }; @@ -109,15 +75,27 @@ export default function SelectAfficheur({ onBack, navigate }) { boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif' }}> -

- Sélection de l'Afficheur -

+
+

+ Sélection de l'Afficheur +

+ + {source === 'supabase' ? 'đŸ—„ïž BDD Ferrovia' : '🌐 SNCF (GTFS)'} + +
+

{ if (v == null) return ''; const tryDate = new Date(v); if (!isNaN(tryDate.getTime())) { - const hh = String(tryDate.getHours()).padStart(2, '0'); + const hh = String(tryDate.getHours() % 24).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0'); return `${hh}h${mm}`; } if (typeof v === 'string') { const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); - if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`; + if (m) { + const h = parseInt(m[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m[2]}`; + } const m2 = v.match(/T(\d{2}):(\d{2})/); - if (m2) return `${m2[1]}h${m2[2]}`; + if (m2) { + const h = parseInt(m2[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m2[2]}`; + } + const m3 = v.match(/(\d{1,2})h(\d{2})/i); + if (m3) { + const h = parseInt(m3[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m3[2]}`; + } return v.slice(0,5).replace(':','h'); } return String(v).slice(0,5).replace(':','h'); @@ -155,10 +168,17 @@ export default function AFLArrivees(){ useAnnonceSonoreRealtime(resolvedGareId, 'AFLArrivees'); useAutoAnnonce(horaires, currentTime, true); - // RĂ©cupĂ©rer le type de gare une seule fois au montage ou quand les paramĂštres changent useEffect(() => { let mounted = true; + const source = getSourceFromPath(window.location.pathname); const fetchStationType = async () => { + if (source === 'sncf') { + if (mounted) { + setStationType('interurbaine'); + setResolvedGareId(null); + } + return; + } try { let fetchedType = null; let fetchedId = gareIdParam || null; @@ -188,9 +208,7 @@ export default function AFLArrivees(){ } } catch (e) { /* ignore */ } }; - if (supabase) { - fetchStationType(); - } + fetchStationType(); return () => { mounted = false; }; }, [gareIdParam, gareDisplay]); @@ -315,11 +333,14 @@ export default function AFLArrivees(){ // eslint-disable-next-line no-unused-vars const parseTimeHM = useCallback((s) => { if (!s) return null; - if (typeof s === 'number') return s; - const m = String(s).match(/(\d{1,2}):(\d{2})/); - if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10); + if (typeof s === 'number') return s % 1440; + const m = String(s).match(/(\d{1,2})[:h](\d{2})/i); + if (m) { + const h = parseInt(m[1], 10) % 24; + return h * 60 + parseInt(m[2], 10); + } const d = new Date(s); - if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes(); + if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes(); return null; }, []); @@ -626,18 +647,57 @@ export default function AFLArrivees(){ }); }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]); - // RĂ©cupĂ©ration Supabase — maintenant avec polling pour synchroniser sans recharger la page + // RĂ©cupĂ©ration des donnĂ©es (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu useEffect(() => { let mounted = true; let inFlight = false; // empĂȘche les fetchs concurrents + const source = getSourceFromPath(window.location.pathname); const fetchData = async () => { if (!mounted) return; - if (inFlight) return; // Ă©viter les appels simultanĂ©s si le fetch prĂ©cĂ©dent n'est pas terminĂ© + if (inFlight) return; inFlight = true; + + if (source === 'sncf') { + setLoading(true); + setError(null); + try { + const { data: sncfData, error: sncfErr } = await getSncfTimetable({ + gareName: gareDisplay || gareParam, + mode: 'arrivees' + }); + if (!mounted) { inFlight = false; return; } + if (sncfErr) { + setError(sncfErr); + } else { + const formatted = (sncfData || []).map(t => { + const timeStr = t.heure_arrivee || t.heure_depart || t.time || ''; + return { + ...t, + _displayTime: formatTimeNoSeconds(timeStr), + _displayPlatform: t.quai || t.voie || '', + _arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr), + _status: t._status || { + label: t.etat === 'supprime' ? 'supprimĂ©' : (t.retard_minutes > 0 ? `retardĂ© +${t.retard_minutes}min.` : "Ă  l'heure"), + detail: '', + delayMinutes: t.retard_minutes || null, + causeDetails: t.cause_retard || null + } + }; + }); + setHoraires(formatted.slice(0, 10)); + setAllHoraires(formatted); + } + } catch (e) { + if (mounted) setError(e.message || String(e)); + } finally { + if (mounted) setLoading(false); + inFlight = false; + } + return; + } + if (!supabase) { - // Supabase non initialisĂ© — avertir et dĂ©finir un message d'erreur pour l'UI - // eslint-disable-next-line no-console console.warn('Supabase non initialisĂ© — vĂ©rifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY'); setError('Supabase non initialisĂ© — vĂ©rifiez les variables d\'environnement'); setLoading(false); @@ -1022,7 +1082,7 @@ export default function AFLArrivees(){ const intervalId = setInterval(fetchData, intervalMs); return () => { mounted = false; clearInterval(intervalId); }; - }, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]); + }, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]); // Helpers pour perturbations (memoized plus haut) @@ -1301,6 +1361,10 @@ export default function AFLArrivees(){

+ ) : showPlatform ? ( +
+
{rawPlatform}
+
) : null )}
diff --git a/src/pages/afficheurs/afl/AFLArriveesQuais.js b/src/pages/afficheurs/afl/AFLArriveesQuais.js index a0e035e..c000de4 100644 --- a/src/pages/afficheurs/afl/AFLArriveesQuais.js +++ b/src/pages/afficheurs/afl/AFLArriveesQuais.js @@ -4,19 +4,22 @@ import { fetchAllRows } from '../../../lib/fetchAllRows'; import './AFLArriveesQuais.css'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import Logo from '../../../components/Logo'; +import { getSourceFromPath } from '../../../services/railwayService'; +import { getSncfTimetable } from '../../../services/sncfApi'; // Helper utilitaire : format HH:MM sans secondes (ex: 15:57) const formatTimeNoSeconds = (v) => { if (v == null) return ''; const tryDate = new Date(v); if (!isNaN(tryDate.getTime())) { - const hh = String(tryDate.getHours()).padStart(2, '0'); + const hh = String(tryDate.getHours() % 24).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0'); return `${hh}:${mm}`; } - const parts = String(v).split(':'); - if (parts.length >= 2) { - return `${parts[0].padStart(2, '0')}:${parts[1].padStart(2, '0')}`; + const m = String(v).match(/(\d{1,2})[:h](\d{2})/i); + if (m) { + const h = parseInt(m[1], 10) % 24; + return `${String(h).padStart(2, '0')}:${m[2]}`; } return v; }; @@ -137,9 +140,12 @@ const AFLArriveesQuais = () => { const parseTimeHM = useCallback((s) => { if (!s) return null; - if (typeof s === 'number') return s; - const m = String(s).match(/(\d{1,2})[h:](\d{2})/); - if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10); + if (typeof s === 'number') return s % 1440; + const m = String(s).match(/(\d{1,2})[h:](\d{2})/i); + if (m) { + const h = parseInt(m[1], 10) % 24; + return h * 60 + parseInt(m[2], 10); + } const d = new Date(s); if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes(); return null; @@ -351,6 +357,39 @@ const AFLArriveesQuais = () => { const fetchData = useCallback(async () => { try { + const source = getSourceFromPath(window.location.pathname); + if (source === 'sncf') { + const { data: sncfData } = await getSncfTimetable({ + gareName: gareDisplay || gareParam, + mode: 'arrivees', + quai: quaiParam + }); + if (sncfData && sncfData.length > 0) { + const train = sncfData[0]; + const enriched = { + ...train, + _rawTime: train.heure_arrivee || train.heure_depart, + _displayTime: formatTimeNoSeconds(train.heure_arrivee || train.heure_depart), + _status: { + label: train.etat === 'supprime' ? 'supprimĂ©' : (train.retard_minutes > 0 ? `retardĂ© +${train.retard_minutes}min.` : "Ă  l'heure"), + detail: '', + delayMinutes: train.retard_minutes || null + }, + _nextStationStatus: { + label: train.etat === 'supprime' ? 'supprimĂ©' : (train.retard_minutes > 0 ? `retardĂ© +${train.retard_minutes}min.` : "Ă  l'heure") + }, + _isToday: true + }; + setNextArrival(enriched); + } else { + setNextArrival(null); + } + setLoading(false); + return; + } + + if (!supabase) return; + const [{ data: horairesData }, { data: subData }, { data: perturbData }, { data: travauxData }] = await Promise.all([ fetchAllRows(supabase.from('horaires').select('*')), supabase.from('travaux_substitutions').select('*'), @@ -615,7 +654,7 @@ const AFLArriveesQuais = () => { console.error(err); setLoading(false); } - }, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]); + }, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, gareParam, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]); useEffect(() => { fetchData(); @@ -706,7 +745,7 @@ const AFLArriveesQuais = () => { if (gareDisplay && Array.isArray(stopsList)) { stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)); } - const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || trainData.quai_reel || trainData.voie || trainData.quai || quaiParam || '').toString().trim(); + const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || trainData.quai_reel || trainData.voie || trainData.quai || trainData._displayPlatform || quaiParam || '').toString().trim() || quaiParam || '1'; // Extraction des origines (si multi-origines / tranches) let origins = []; diff --git a/src/pages/afficheurs/afl/AFLDeparts.js b/src/pages/afficheurs/afl/AFLDeparts.js index d85888d..795540a 100644 --- a/src/pages/afficheurs/afl/AFLDeparts.js +++ b/src/pages/afficheurs/afl/AFLDeparts.js @@ -5,21 +5,34 @@ import { fetchAllRows } from '../../../lib/fetchAllRows'; import MarqueeText from '../../../components/MarqueeText'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; +import { getSourceFromPath } from '../../../services/railwayService'; +import { getSncfTimetable } from '../../../services/sncfApi'; // Helper utilitaire : format HHhMM sans secondes (ex: 06h44) const formatTimeNoSeconds = (v) => { if (v == null) return ''; const tryDate = new Date(v); if (!isNaN(tryDate.getTime())) { - const hh = String(tryDate.getHours()).padStart(2, '0'); + const hh = String(tryDate.getHours() % 24).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0'); return `${hh}h${mm}`; } if (typeof v === 'string') { const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); - if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`; + if (m) { + const h = parseInt(m[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m[2]}`; + } const m2 = v.match(/T(\d{2}):(\d{2})/); - if (m2) return `${m2[1]}h${m2[2]}`; + if (m2) { + const h = parseInt(m2[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m2[2]}`; + } + const m3 = v.match(/(\d{1,2})h(\d{2})/i); + if (m3) { + const h = parseInt(m3[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m3[2]}`; + } return v.slice(0,5).replace(':','h'); } return String(v).slice(0,5).replace(':','h'); @@ -165,7 +178,15 @@ export default function AFLDeparts(){ // RĂ©cupĂ©rer le type de gare une seule fois au montage ou quand les paramĂštres changent useEffect(() => { let mounted = true; + const source = getSourceFromPath(window.location.pathname); const fetchStationType = async () => { + if (source === 'sncf') { + if (mounted) { + setStationType('interurbaine'); + setResolvedGareId(null); + } + return; + } try { let fetchedType = null; let fetchedId = gareIdParam || null; @@ -195,9 +216,7 @@ export default function AFLDeparts(){ } } catch (e) { /* ignore */ } }; - if (supabase) { - fetchStationType(); - } + fetchStationType(); return () => { mounted = false; }; }, [gareIdParam, gareDisplay]); @@ -322,11 +341,14 @@ export default function AFLDeparts(){ // eslint-disable-next-line no-unused-vars const parseTimeHM = useCallback((s) => { if (!s) return null; - if (typeof s === 'number') return s; - const m = String(s).match(/(\d{1,2}):(\d{2})/); - if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10); + if (typeof s === 'number') return s % 1440; + const m = String(s).match(/(\d{1,2})[:h](\d{2})/i); + if (m) { + const h = parseInt(m[1], 10) % 24; + return h * 60 + parseInt(m[2], 10); + } const d = new Date(s); - if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes(); + if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes(); return null; }, []); @@ -638,18 +660,54 @@ export default function AFLDeparts(){ return () => clearInterval(id); }, []); - // RĂ©cupĂ©ration Supabase — maintenant avec polling pour synchroniser sans recharger la page + // RĂ©cupĂ©ration des donnĂ©es (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu useEffect(() => { let mounted = true; let inFlight = false; // empĂȘche les fetchs concurrents + const source = getSourceFromPath(window.location.pathname); const fetchData = async () => { if (!mounted) return; - if (inFlight) return; // Ă©viter les appels simultanĂ©s si le fetch prĂ©cĂ©dent n'est pas terminĂ© + if (inFlight) return; inFlight = true; + + if (source === 'sncf') { + setLoading(true); + setError(null); + try { + const { data: sncfData, error: sncfErr } = await getSncfTimetable({ + gareName: gareDisplay || gareParam, + mode: 'departs' + }); + if (!mounted) { inFlight = false; return; } + if (sncfErr) { + setError(sncfErr); + } else { + const formatted = (sncfData || []).map(t => ({ + ...t, + _displayTime: formatTimeNoSeconds(t.heure_depart), + _displayPlatform: t.quai || t.voie || '', + _depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart), + _status: t._status || { + label: t.etat === 'supprime' ? 'supprimĂ©' : (t.retard_minutes > 0 ? `retardĂ© +${t.retard_minutes}min.` : "Ă  l'heure"), + detail: '', + delayMinutes: t.retard_minutes || null, + causeDetails: t.cause_retard || null + } + })); + setHoraires(formatted.slice(0, 10)); + setAllHoraires(formatted); + } + } catch (e) { + if (mounted) setError(e.message || String(e)); + } finally { + if (mounted) setLoading(false); + inFlight = false; + } + return; + } + if (!supabase) { - // Supabase non initialisĂ© — avertir et dĂ©finir un message d'erreur pour l'UI - // eslint-disable-next-line no-console console.warn('Supabase non initialisĂ© — vĂ©rifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY'); setError('Supabase non initialisĂ© — vĂ©rifiez les variables d\'environnement'); setLoading(false); @@ -1049,7 +1107,7 @@ export default function AFLDeparts(){ const intervalId = setInterval(fetchData, intervalMs); return () => { mounted = false; clearInterval(intervalId); }; - }, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]); + }, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]); // Helpers pour perturbations (memoized plus haut) @@ -1194,10 +1252,18 @@ export default function AFLDeparts(){ // Calcul des vias const stopsList = getStops(horaire); const viaNames = []; - if (Array.isArray(stopsList)) { + if (Array.isArray(stopsList) && stopsList.length > 0) { let isAfterStart = false; + const hasStartInStops = stopsList.some(s => { + const sName = s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : ''); + return normalizeName(sName) === normalizeName(stationName || ''); + }); + if (!hasStartInStops) { + isAfterStart = true; + } + for (const s of stopsList) { - const sName = s.gare_nom || s.nom || s.name || s.station_name || ''; + const sName = s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : ''); if (!sName) continue; const norm = normalizeName(sName); if (norm === normalizeName(stationName || '')) { @@ -1336,6 +1402,10 @@ export default function AFLDeparts(){
+ ) : showPlatform ? ( +
+
{rawPlatform}
+
) : null )}
diff --git a/src/pages/afficheurs/afl/AFLDepartsQuais.js b/src/pages/afficheurs/afl/AFLDepartsQuais.js index 4adb1be..d13b011 100644 --- a/src/pages/afficheurs/afl/AFLDepartsQuais.js +++ b/src/pages/afficheurs/afl/AFLDepartsQuais.js @@ -4,19 +4,22 @@ import { fetchAllRows } from '../../../lib/fetchAllRows'; import './AFLDepartsQuais.css'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import Logo from '../../../components/Logo'; +import { getSourceFromPath } from '../../../services/railwayService'; +import { getSncfTimetable } from '../../../services/sncfApi'; // Helper utilitaire : format HH:MM sans secondes (ex: 15:57) const formatTimeNoSeconds = (v) => { if (v == null) return ''; const tryDate = new Date(v); if (!isNaN(tryDate.getTime())) { - const hh = String(tryDate.getHours()).padStart(2, '0'); + const hh = String(tryDate.getHours() % 24).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0'); return `${hh}:${mm}`; } - const parts = String(v).split(':'); - if (parts.length >= 2) { - return `${parts[0].padStart(2, '0')}:${parts[1].padStart(2, '0')}`; + const m = String(v).match(/(\d{1,2})[:h](\d{2})/i); + if (m) { + const h = parseInt(m[1], 10) % 24; + return `${String(h).padStart(2, '0')}:${m[2]}`; } return v; }; @@ -185,9 +188,12 @@ const AFLDepartsQuais = () => { const parseTimeHM = useCallback((s) => { if (!s) return null; - if (typeof s === 'number') return s; - const m = String(s).match(/(\d{1,2})[h:](\d{2})/); - if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10); + if (typeof s === 'number') return s % 1440; + const m = String(s).match(/(\d{1,2})[h:](\d{2})/i); + if (m) { + const h = parseInt(m[1], 10) % 24; + return h * 60 + parseInt(m[2], 10); + } const d = new Date(s); if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes(); return null; @@ -421,6 +427,39 @@ const AFLDepartsQuais = () => { const fetchData = useCallback(async () => { try { + const source = getSourceFromPath(window.location.pathname); + if (source === 'sncf') { + const { data: sncfData } = await getSncfTimetable({ + gareName: gareDisplay || gareParam, + mode: 'departs', + quai: quaiParam + }); + if (sncfData && sncfData.length > 0) { + const train = sncfData[0]; + const enriched = { + ...train, + _rawTime: train.heure_depart, + _displayTime: formatTimeNoSeconds(train.heure_depart), + _status: { + label: train.etat === 'supprime' ? 'supprimĂ©' : (train.retard_minutes > 0 ? `retardĂ© +${train.retard_minutes}min.` : "Ă  l'heure"), + detail: '', + delayMinutes: train.retard_minutes || null + }, + _nextStationStatus: { + label: train.etat === 'supprime' ? 'supprimĂ©' : (train.retard_minutes > 0 ? `retardĂ© +${train.retard_minutes}min.` : "Ă  l'heure") + }, + _isToday: true + }; + setNextDeparture(enriched); + } else { + setNextDeparture(null); + } + setLoading(false); + return; + } + + if (!supabase) return; + const [{ data: horairesData }, { data: subData }, { data: perturbData }, { data: travauxData }] = await Promise.all([ fetchAllRows(supabase.from('horaires').select('*')), supabase.from('travaux_substitutions').select('*'), @@ -689,7 +728,7 @@ const AFLDepartsQuais = () => { console.error(err); setLoading(false); } - }, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]); + }, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, gareParam, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]); useEffect(() => { fetchData(); @@ -780,7 +819,7 @@ const AFLDepartsQuais = () => { if (gareDisplay && Array.isArray(stopsList)) { stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)); } - const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || trainData.quai_reel || trainData.voie || trainData.quai || quaiParam || '').toString().trim(); + const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || trainData.quai_reel || trainData.voie || trainData.quai || trainData._displayPlatform || quaiParam || '').toString().trim() || quaiParam || '1'; // Extraction des destinations (prise en compte des trains multi-branches / tranches) let destinations = []; @@ -839,7 +878,7 @@ const AFLDepartsQuais = () => { return sName && (sName === normCurrent || sName.includes(normCurrent) || normCurrent.includes(sName)); }); - const rawDownstream = startIdx !== -1 ? stopsList.slice(startIdx + 1) : stopsList.slice(1); + const rawDownstream = startIdx !== -1 ? stopsList.slice(startIdx + 1) : stopsList; const seen = new Set(); rawDownstream.forEach(s => { diff --git a/src/pages/afficheurs/classiques/ClassiquesArrivees.js b/src/pages/afficheurs/classiques/ClassiquesArrivees.js index 26cb5c9..85f557b 100644 --- a/src/pages/afficheurs/classiques/ClassiquesArrivees.js +++ b/src/pages/afficheurs/classiques/ClassiquesArrivees.js @@ -6,21 +6,34 @@ import MarqueeText from '../../../components/MarqueeText'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import { fetchAllRows } from '../../../lib/fetchAllRows'; +import { getSourceFromPath } from '../../../services/railwayService'; +import { getSncfTimetable } from '../../../services/sncfApi'; // Helper utilitaire : format HHhMM sans secondes (ex: 06h44) const formatTimeNoSeconds = (v) => { if (v == null) return ''; const tryDate = new Date(v); if (!isNaN(tryDate.getTime())) { - const hh = String(tryDate.getHours()).padStart(2, '0'); + const hh = String(tryDate.getHours() % 24).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0'); return `${hh}h${mm}`; } if (typeof v === 'string') { const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); - if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`; + if (m) { + const h = parseInt(m[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m[2]}`; + } const m2 = v.match(/T(\d{2}):(\d{2})/); - if (m2) return `${m2[1]}h${m2[2]}`; + if (m2) { + const h = parseInt(m2[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m2[2]}`; + } + const m3 = v.match(/(\d{1,2})h(\d{2})/i); + if (m3) { + const h = parseInt(m3[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m3[2]}`; + } return v.slice(0,5).replace(':','h'); } return String(v).slice(0,5).replace(':','h'); @@ -93,7 +106,7 @@ export default function ClassiquesArrivees(){ const [allHoraires, setAllHoraires] = useState([]); const [perturbations, setPerturbations] = useState([]); // type de la gare affichĂ©e (valeurs attendues: 'interurbaine', 'ville', ...) - const [stationType, setStationType] = useState(null); + const [, setStationType] = useState(null); const [, setTravaux] = useState([]); const [, setLoading] = useState(false); const [, setError] = useState(null); @@ -107,7 +120,15 @@ export default function ClassiquesArrivees(){ // RĂ©cupĂ©rer le type de gare une seule fois au montage ou quand les paramĂštres changent useEffect(() => { let mounted = true; + const source = getSourceFromPath(window.location.pathname); const fetchStationType = async () => { + if (source === 'sncf') { + if (mounted) { + setStationType('interurbaine'); + setResolvedGareId(null); + } + return; + } try { let fetchedType = null; let fetchedId = gareIdParam || null; @@ -137,9 +158,7 @@ export default function ClassiquesArrivees(){ } } catch (e) { /* ignore */ } }; - if (supabase) { - fetchStationType(); - } + fetchStationType(); return () => { mounted = false; }; }, [gareIdParam, gareDisplay]); @@ -263,29 +282,18 @@ export default function ClassiquesArrivees(){ // parse HH:mm or ISO into minutes since midnight (utilisable ailleurs dans le composant) const parseTimeHM = useCallback((s) => { if (!s) return null; - if (typeof s === 'number') return s; - const m = String(s).match(/(\d{1,2}):(\d{2})/); - if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10); + if (typeof s === 'number') return s % 1440; + const m = String(s).match(/(\d{1,2})[:h](\d{2})/i); + if (m) { + const h = parseInt(m[1], 10) % 24; + return h * 60 + parseInt(m[2], 10); + } const d = new Date(s); if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes(); return null; }, []); - // retourne le threshold (en minutes) pour afficher le quai selon le type de gare - const thresholdForStationType = useCallback((typeGare) => { - const tG = String(typeGare || '').trim().toLowerCase(); - - // VĂ©rification du type de gare - if (tG === 'interurbaine' || tG.includes('interurb')) { - return 360; // 6 heures - } - if (tG === 'ville' || tG.includes('ville')) { - return 30; // 30 minutes - } - - // par dĂ©faut, 30 minutes - return 30; - }, []); + const formatTimeFromHM = useCallback((totalMinutes) => { if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return ''; @@ -604,18 +612,57 @@ export default function ClassiquesArrivees(){ return () => clearTimeout(timeoutId); }, []); - // RĂ©cupĂ©ration Supabase — maintenant avec polling pour synchroniser sans recharger la page + // RĂ©cupĂ©ration des donnĂ©es (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu useEffect(() => { let mounted = true; let inFlight = false; // empĂȘche les fetchs concurrents + const source = getSourceFromPath(window.location.pathname); const fetchData = async () => { if (!mounted) return; - if (inFlight) return; // Ă©viter les appels simultanĂ©s si le fetch prĂ©cĂ©dent n'est pas terminĂ© + if (inFlight) return; inFlight = true; + + if (source === 'sncf') { + setLoading(true); + setError(null); + try { + const { data: sncfData, error: sncfErr } = await getSncfTimetable({ + gareName: gareDisplay || gareParam, + mode: 'arrivees' + }); + if (!mounted) { inFlight = false; return; } + if (sncfErr) { + setError(sncfErr); + } else { + const formatted = (sncfData || []).map(t => { + const timeStr = t.heure_arrivee || t.heure_depart || t.time || ''; + return { + ...t, + _displayTime: formatTimeNoSeconds(timeStr), + _displayPlatform: t.quai || t.voie || '', + _arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr), + _status: t._status || { + label: t.etat === 'supprime' ? 'supprimĂ©' : (t.retard_minutes > 0 ? `retardĂ© +${t.retard_minutes}min.` : "Ă  l'heure"), + detail: '', + delayMinutes: t.retard_minutes || null, + causeDetails: t.cause_retard || null + } + }; + }); + setHoraires(formatted.slice(0, defilementMode ? 20 : 10)); + setAllHoraires(formatted); + } + } catch (e) { + if (mounted) setError(e.message || String(e)); + } finally { + if (mounted) setLoading(false); + inFlight = false; + } + return; + } + if (!supabase) { - // Supabase non initialisĂ© — avertir et dĂ©finir un message d'erreur pour l'UI - // eslint-disable-next-line no-console console.warn('Supabase non initialisĂ© — vĂ©rifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY'); setError('Supabase non initialisĂ© — vĂ©rifiez les variables d\'environnement'); setLoading(false); @@ -1013,7 +1060,7 @@ export default function ClassiquesArrivees(){ const intervalId = setInterval(fetchData, intervalMs); return () => { mounted = false; clearInterval(intervalId); }; - }, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]); + }, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]); // Helpers pour perturbations (memoized plus haut) @@ -1089,21 +1136,6 @@ export default function ClassiquesArrivees(){ const isDelayed = status && status.label && String(status.label).startsWith('retardĂ©'); let showPlatform = !isSuppressed && Boolean(platform); - if (showPlatform) { - const nowHM = currentTime.getHours()*60 + currentTime.getMinutes(); - const sched = (typeof horaire._arrHM === 'number') ? horaire._arrHM : parseTimeHM(horaire.heure_arrivee || horaire.time || horaire.heure || horaire._rawTime || ''); - const threshold = thresholdForStationType(stationType); - if (typeof sched === 'number') { - let diff = sched - nowHM; - // GĂ©rer le passage Ă  minuit - if (diff < -720) { // e.g., now 23:00, sched 01:00, diff is ~ -22 hours - diff += 1440; // treat as next day - } - showPlatform = (diff >= 0 && diff <= threshold); - } else { - showPlatform = false; - } - } const isSub = isSubstitution(horaire); const infoText = isSub diff --git a/src/pages/afficheurs/classiques/ClassiquesDeparts.js b/src/pages/afficheurs/classiques/ClassiquesDeparts.js index 216b40f..29be4c0 100644 --- a/src/pages/afficheurs/classiques/ClassiquesDeparts.js +++ b/src/pages/afficheurs/classiques/ClassiquesDeparts.js @@ -6,21 +6,34 @@ import MarqueeText from '../../../components/MarqueeText'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import { fetchAllRows } from '../../../lib/fetchAllRows'; +import { getSourceFromPath } from '../../../services/railwayService'; +import { getSncfTimetable } from '../../../services/sncfApi'; // Helper utilitaire : format HHhMM sans secondes (ex: 06h44) const formatTimeNoSeconds = (v) => { if (v == null) return ''; const tryDate = new Date(v); if (!isNaN(tryDate.getTime())) { - const hh = String(tryDate.getHours()).padStart(2, '0'); + const hh = String(tryDate.getHours() % 24).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0'); return `${hh}h${mm}`; } if (typeof v === 'string') { const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); - if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`; + if (m) { + const h = parseInt(m[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m[2]}`; + } const m2 = v.match(/T(\d{2}):(\d{2})/); - if (m2) return `${m2[1]}h${m2[2]}`; + if (m2) { + const h = parseInt(m2[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m2[2]}`; + } + const m3 = v.match(/(\d{1,2})h(\d{2})/i); + if (m3) { + const h = parseInt(m3[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m3[2]}`; + } return v.slice(0,5).replace(':','h'); } return String(v).slice(0,5).replace(':','h'); @@ -93,7 +106,7 @@ export default function ClassiquesDeparts(){ const [allHoraires, setAllHoraires] = useState([]); const [perturbations, setPerturbations] = useState([]); // type de la gare affichĂ©e (valeurs attendues: 'interurbaine', 'ville', ...) - const [stationType, setStationType] = useState(null); + const [, setStationType] = useState(null); const [, setTravaux] = useState([]); const [, setLoading] = useState(false); const [, setError] = useState(null); @@ -107,7 +120,15 @@ export default function ClassiquesDeparts(){ // RĂ©cupĂ©rer le type de gare une seule fois au montage ou quand les paramĂštres changent useEffect(() => { let mounted = true; + const source = getSourceFromPath(window.location.pathname); const fetchStationType = async () => { + if (source === 'sncf') { + if (mounted) { + setStationType('interurbaine'); + setResolvedGareId(null); + } + return; + } try { let fetchedType = null; let fetchedId = gareIdParam || null; @@ -137,9 +158,7 @@ export default function ClassiquesDeparts(){ } } catch (e) { /* ignore */ } }; - if (supabase) { - fetchStationType(); - } + fetchStationType(); return () => { mounted = false; }; }, [gareIdParam, gareDisplay]); @@ -263,29 +282,18 @@ export default function ClassiquesDeparts(){ // parse HH:mm or ISO into minutes since midnight (utilisable ailleurs dans le composant) const parseTimeHM = useCallback((s) => { if (!s) return null; - if (typeof s === 'number') return s; - const m = String(s).match(/(\d{1,2}):(\d{2})/); - if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10); + if (typeof s === 'number') return s % 1440; + const m = String(s).match(/(\d{1,2})[:h](\d{2})/i); + if (m) { + const h = parseInt(m[1], 10) % 24; + return h * 60 + parseInt(m[2], 10); + } const d = new Date(s); - if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes(); + if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes(); return null; }, []); - // retourne le threshold (en minutes) pour afficher le quai selon le type de gare - const thresholdForStationType = useCallback((typeGare) => { - const tG = String(typeGare || '').trim().toLowerCase(); - - // VĂ©rification du type de gare - if (tG === 'interurbaine' || tG.includes('interurb')) { - return 360; // 6 heures - } - if (tG === 'ville' || tG.includes('ville')) { - return 30; // 30 minutes - } - - // par dĂ©faut, 30 minutes - return 30; - }, []); + const formatTimeFromHM = useCallback((totalMinutes) => { if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return ''; @@ -604,18 +612,54 @@ export default function ClassiquesDeparts(){ return () => clearTimeout(timeoutId); }, []); - // RĂ©cupĂ©ration Supabase — maintenant avec polling pour synchroniser sans recharger la page + // RĂ©cupĂ©ration des donnĂ©es (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu useEffect(() => { let mounted = true; let inFlight = false; // empĂȘche les fetchs concurrents + const source = getSourceFromPath(window.location.pathname); const fetchData = async () => { if (!mounted) return; - if (inFlight) return; // Ă©viter les appels simultanĂ©s si le fetch prĂ©cĂ©dent n'est pas terminĂ© + if (inFlight) return; inFlight = true; + + if (source === 'sncf') { + setLoading(true); + setError(null); + try { + const { data: sncfData, error: sncfErr } = await getSncfTimetable({ + gareName: gareDisplay || gareParam, + mode: 'departs' + }); + if (!mounted) { inFlight = false; return; } + if (sncfErr) { + setError(sncfErr); + } else { + const formatted = (sncfData || []).map(t => ({ + ...t, + _displayTime: formatTimeNoSeconds(t.heure_depart), + _displayPlatform: t.quai || t.voie || '', + _depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart), + _status: t._status || { + label: t.etat === 'supprime' ? 'supprimĂ©' : (t.retard_minutes > 0 ? `retardĂ© +${t.retard_minutes}min.` : "Ă  l'heure"), + detail: '', + delayMinutes: t.retard_minutes || null, + causeDetails: t.cause_retard || null + } + })); + setHoraires(formatted.slice(0, defilementMode ? 20 : 10)); + setAllHoraires(formatted); + } + } catch (e) { + if (mounted) setError(e.message || String(e)); + } finally { + if (mounted) setLoading(false); + inFlight = false; + } + return; + } + if (!supabase) { - // Supabase non initialisĂ© — avertir et dĂ©finir un message d'erreur pour l'UI - // eslint-disable-next-line no-console console.warn('Supabase non initialisĂ© — vĂ©rifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY'); setError('Supabase non initialisĂ© — vĂ©rifiez les variables d\'environnement'); setLoading(false); @@ -1013,7 +1057,7 @@ export default function ClassiquesDeparts(){ const intervalId = setInterval(fetchData, intervalMs); return () => { mounted = false; clearInterval(intervalId); }; - }, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]); + }, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]); // Helpers pour perturbations (memoized plus haut) @@ -1089,21 +1133,6 @@ export default function ClassiquesDeparts(){ const isDelayed = status && status.label && String(status.label).startsWith('retardĂ©'); let showPlatform = !isSuppressed && Boolean(platform); - if (showPlatform) { - const nowHM = currentTime.getHours()*60 + currentTime.getMinutes(); - const sched = (typeof horaire._depHM === 'number') ? horaire._depHM : parseTimeHM(horaire.heure_depart || horaire.time || horaire.heure || horaire._rawTime || ''); - const threshold = thresholdForStationType(stationType); - if (typeof sched === 'number') { - let diff = sched - nowHM; - // GĂ©rer le passage Ă  minuit - if (diff < -720) { // e.g., now 23:00, sched 01:00, diff is ~ -22 hours - diff += 1440; // treat as next day - } - showPlatform = (diff >= 0 && diff <= threshold); - } else { - showPlatform = false; - } - } const isSub = isSubstitution(horaire); const infoText = isSub diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js index 9e55103..81cc25d 100644 --- a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js +++ b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js @@ -4,6 +4,8 @@ import supabase from '../../../../lib/supabase'; import { fetchAllRows } from '../../../../lib/fetchAllRows'; import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime'; import useAutoAnnonce from '../../../../hooks/useAutoAnnonce'; +import { getSourceFromPath } from '../../../../services/railwayService'; +import { getSncfTimetable } from '../../../../services/sncfApi'; function Clock() { const [now, setNow] = useState(new Date()); useEffect(() => { @@ -46,7 +48,7 @@ export default function ClassiquesArriveesQuais() { const [loadingHoraire, setLoadingHoraire] = useState(false); const [, setHoraireError] = useState(null); const [, setCandidatesCount] = useState(0); - const [stationType, setStationType] = useState(null); + const [, setStationType] = useState(null); const [currentTime, setCurrentTime] = useState(new Date()); const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null); @@ -111,11 +113,14 @@ export default function ClassiquesArriveesQuais() { const parseTimeHM = useCallback((s) => { if (!s) return null; - if (typeof s === 'number') return s; - const m = String(s).match(/(\d{1,2}):(\d{2})/); - if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10); + if (typeof s === 'number') return s % 1440; + const m = String(s).match(/(\d{1,2})[:h](\d{2})/i); + if (m) { + const h = parseInt(m[1], 10) % 24; + return h * 60 + parseInt(m[2], 10); + } const d = new Date(s); - if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes(); + if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes(); return null; }, []); @@ -123,15 +128,26 @@ export default function ClassiquesArriveesQuais() { if (v == null) return ''; const tryDate = new Date(v); if (!isNaN(tryDate.getTime())) { - const hh = String(tryDate.getHours()).padStart(2,'0'); + const hh = String(tryDate.getHours() % 24).padStart(2,'0'); const mm = String(tryDate.getMinutes()).padStart(2,'0'); return `${hh}h${mm}`; } if (typeof v === 'string') { const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); - if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`; + if (m) { + const h = parseInt(m[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m[2]}`; + } const m2 = v.match(/T(\d{2}):(\d{2})/); - if (m2) return `${m2[1]}h${m2[2]}`; + if (m2) { + const h = parseInt(m2[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m2[2]}`; + } + const m3 = v.match(/(\d{1,2})h(\d{2})/i); + if (m3) { + const h = parseInt(m3[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m3[2]}`; + } return v.slice(0,5).replace(':','h'); } return String(v).slice(0,5).replace(':','h'); @@ -145,13 +161,7 @@ export default function ClassiquesArriveesQuais() { return `${hh}h${mm}`; }, []); - const thresholdForStationType = useCallback((type) => { - if (!type) return 0; - const t = String(type).trim().toLowerCase(); - if (t === 'interurbaine' || t.includes('interurb')) return 360; - if (t === 'ville' || t.includes('ville')) return 30; - return 0; - }, []); + @@ -457,6 +467,58 @@ export default function ClassiquesArriveesQuais() { setLoadingHoraire(true); setHoraireError(null); setSelectedHoraire(null); + + const source = getSourceFromPath(window.location.pathname); + if (source === 'sncf') { + try { + const { data: sncfData, error: sncfErr } = await getSncfTimetable({ + gareName: gareDisplayParam || gareParam, + mode: 'arrivees', + quai: quaiParam + }); + if (!mounted) return; + if (sncfErr) { + setHoraireError(sncfErr); + } else if (sncfData && sncfData.length > 0) { + const train = sncfData[0]; + const now = new Date(); + const nowHM = now.getHours() * 60 + now.getMinutes(); + const arrHM = parseTimeHM(train.heure_arrivee || train.heure_depart); + + const chosen = { + horaire: { + ...train, + _status: train._status || { + label: train.etat === 'supprime' ? 'supprimĂ©' : (train.retard_minutes > 0 ? `retardĂ© +${train.retard_minutes}min.` : "Ă  l'heure"), + detail: '', + delayMinutes: train.retard_minutes || null, + causeDetails: train.cause_retard || null + } + }, + stop: { + gare_nom: train.gare_arrivee_nom || gareDisplayParam || gareParam, + heure_arrivee: train.heure_arrivee || train.heure_depart, + quai: train.quai || quaiParam, + quai_reel: train.quai || quaiParam + }, + arrHM: arrHM != null ? arrHM : nowHM, + rawTime: train.heure_arrivee || train.heure_depart, + circulatesToday: true, + circulatesTomorrow: false, + isTomorrow: false + }; + setSelectedHoraire(chosen); + setCandidatesCount(sncfData.length); + setStationType('interurbaine'); + } + } catch (e) { + if (mounted) setHoraireError(e.message || String(e)); + } finally { + if (mounted) setLoadingHoraire(false); + } + return; + } + if (!supabase) { setHoraireError('Supabase non initialisĂ©'); setLoadingHoraire(false); @@ -650,16 +712,10 @@ export default function ClassiquesArriveesQuais() { loadNextForQuai(); return () => { mounted = false; }; - }, [gareDisplayParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]); + }, [gareDisplayParam, gareParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]); // Rail is handled purely in CSS for better performance and alignment - const showPlatform = (() => { - const sched = selectedHoraire ? selectedHoraire.arrHM : null; - if (sched === null || !stationType) return true; - const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes(); - const threshold = thresholdForStationType(stationType); - return nowHM >= (sched - threshold); - })(); + const showPlatform = true; if (!loadingHoraire && !selectedHoraire) { return ( @@ -789,7 +845,7 @@ export default function ClassiquesArriveesQuais() { try { const p = JSON.parse(horaire.gares_desservies); if (Array.isArray(p)) stops = p; } catch (e) { /* ignore */ } } } - if (!Array.isArray(stops) || stops.length === 0) stops = mock.stops || []; + if (!Array.isArray(stops) || stops.length === 0) stops = []; const labeled = stops.map((stop, idx) => { let label = ''; @@ -870,7 +926,7 @@ export default function ClassiquesArriveesQuais() { {/* BOTTOM FOOTER: Full Width */}
- ain. + {quaiParam ? `Voie ${quaiParam}` : (selectedHoraire?.horaire?.quai ? `Voie ${selectedHoraire.horaire.quai}` : (selectedHoraire?.stop?.quai ? `Voie ${selectedHoraire.stop.quai}` : ''))}
diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js index 92eb22c..e5bd5e2 100644 --- a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js +++ b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js @@ -4,6 +4,8 @@ import supabase from '../../../../lib/supabase'; import { fetchAllRows } from '../../../../lib/fetchAllRows'; import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime'; import useAutoAnnonce from '../../../../hooks/useAutoAnnonce'; +import { getSourceFromPath } from '../../../../services/railwayService'; +import { getSncfTimetable } from '../../../../services/sncfApi'; function Clock() { const [now, setNow] = useState(new Date()); useEffect(() => { @@ -46,7 +48,7 @@ export default function ClassiquesDepartsQuais() { const [loadingHoraire, setLoadingHoraire] = useState(false); const [, setHoraireError] = useState(null); const [, setCandidatesCount] = useState(0); - const [stationType, setStationType] = useState(null); + const [, setStationType] = useState(null); const [currentTime, setCurrentTime] = useState(new Date()); const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null); @@ -111,11 +113,14 @@ export default function ClassiquesDepartsQuais() { const parseTimeHM = useCallback((s) => { if (!s) return null; - if (typeof s === 'number') return s; - const m = String(s).match(/(\d{1,2}):(\d{2})/); - if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10); + if (typeof s === 'number') return s % 1440; + const m = String(s).match(/(\d{1,2})[:h](\d{2})/i); + if (m) { + const h = parseInt(m[1], 10) % 24; + return h * 60 + parseInt(m[2], 10); + } const d = new Date(s); - if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes(); + if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes(); return null; }, []); @@ -123,15 +128,26 @@ export default function ClassiquesDepartsQuais() { if (v == null) return ''; const tryDate = new Date(v); if (!isNaN(tryDate.getTime())) { - const hh = String(tryDate.getHours()).padStart(2,'0'); + const hh = String(tryDate.getHours() % 24).padStart(2,'0'); const mm = String(tryDate.getMinutes()).padStart(2,'0'); return `${hh}h${mm}`; } if (typeof v === 'string') { const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); - if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`; + if (m) { + const h = parseInt(m[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m[2]}`; + } const m2 = v.match(/T(\d{2}):(\d{2})/); - if (m2) return `${m2[1]}h${m2[2]}`; + if (m2) { + const h = parseInt(m2[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m2[2]}`; + } + const m3 = v.match(/(\d{1,2})h(\d{2})/i); + if (m3) { + const h = parseInt(m3[1], 10) % 24; + return `${String(h).padStart(2,'0')}h${m3[2]}`; + } return v.slice(0,5).replace(':','h'); } return String(v).slice(0,5).replace(':','h'); @@ -145,13 +161,7 @@ export default function ClassiquesDepartsQuais() { return `${hh}h${mm}`; }, []); - const thresholdForStationType = useCallback((type) => { - if (!type) return 0; - const t = String(type).trim().toLowerCase(); - if (t === 'interurbaine' || t.includes('interurb')) return 360; - if (t === 'ville' || t.includes('ville')) return 30; - return 0; - }, []); + @@ -457,6 +467,58 @@ export default function ClassiquesDepartsQuais() { setLoadingHoraire(true); setHoraireError(null); setSelectedHoraire(null); + + const source = getSourceFromPath(window.location.pathname); + if (source === 'sncf') { + try { + const { data: sncfData, error: sncfErr } = await getSncfTimetable({ + gareName: gareDisplayParam || gareParam, + mode: 'departs', + quai: quaiParam + }); + if (!mounted) return; + if (sncfErr) { + setHoraireError(sncfErr); + } else if (sncfData && sncfData.length > 0) { + const train = sncfData[0]; + const now = new Date(); + const nowHM = now.getHours() * 60 + now.getMinutes(); + const depHM = parseTimeHM(train.heure_depart); + + const chosen = { + horaire: { + ...train, + _status: train._status || { + label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"), + detail: '', + delayMinutes: train.retard_minutes || null, + causeDetails: train.cause_retard || null + } + }, + stop: { + gare_nom: train.gare_depart_nom || gareDisplayParam || gareParam, + heure_depart: train.heure_depart, + quai: train.quai || quaiParam, + quai_reel: train.quai || quaiParam + }, + depHM: depHM != null ? depHM : nowHM, + rawTime: train.heure_depart, + circulatesToday: true, + circulatesTomorrow: false, + isTomorrow: false + }; + setSelectedHoraire(chosen); + setCandidatesCount(sncfData.length); + setStationType('interurbaine'); + } + } catch (e) { + if (mounted) setHoraireError(e.message || String(e)); + } finally { + if (mounted) setLoadingHoraire(false); + } + return; + } + if (!supabase) { setHoraireError('Supabase non initialisé'); setLoadingHoraire(false); @@ -648,16 +710,10 @@ export default function ClassiquesDepartsQuais() { loadNextForQuai(); return () => { mounted = false; }; - }, [gareDisplayParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]); + }, [gareDisplayParam, gareParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]); // Rail is handled purely in CSS for better performance and alignment - const showPlatform = (() => { - const sched = selectedHoraire ? selectedHoraire.depHM : null; - if (sched === null || !stationType) return true; - const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes(); - const threshold = thresholdForStationType(stationType); - return nowHM >= (sched - threshold); - })(); + const showPlatform = true; if (!loadingHoraire && !selectedHoraire) { return ( @@ -786,7 +842,7 @@ export default function ClassiquesDepartsQuais() { try { const p = JSON.parse(horaire.gares_desservies); if (Array.isArray(p)) stops = p; } catch (e) { /* ignore */ } } } - if (!Array.isArray(stops) || stops.length === 0) stops = mock.stops || []; + if (!Array.isArray(stops) || stops.length === 0) stops = []; const labeled = stops.map((stop, idx) => { let label = ''; @@ -868,7 +924,7 @@ export default function ClassiquesDepartsQuais() { {/* BOTTOM FOOTER: Full Width */}
- ain. + {quaiParam ? `Voie ${quaiParam}` : (selectedHoraire?.horaire?.quai ? `Voie ${selectedHoraire.horaire.quai}` : (selectedHoraire?.stop?.quai ? `Voie ${selectedHoraire.stop.quai}` : ''))}
diff --git a/src/pages/afficheurs/eva/EVAArrivees.js b/src/pages/afficheurs/eva/EVAArrivees.js index 2c11159..de662bb 100644 --- a/src/pages/afficheurs/eva/EVAArrivees.js +++ b/src/pages/afficheurs/eva/EVAArrivees.js @@ -5,21 +5,34 @@ import { fetchAllRows } from '../../../lib/fetchAllRows'; import MarqueeText from '../../../components/MarqueeText'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; +import { getSourceFromPath } from '../../../services/railwayService'; +import { getSncfTimetable } from '../../../services/sncfApi'; // Helper utilitaire : format HH:MM avec deux points (ex: 22:39) const formatTimeWithColon = (v) => { if (v == null) return ''; const tryDate = new Date(v); if (!isNaN(tryDate.getTime())) { - const hh = String(tryDate.getHours()).padStart(2, '0'); + const hh = String(tryDate.getHours() % 24).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0'); return `${hh}:${mm}`; } if (typeof v === 'string') { const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); - if (m) return `${String(m[1]).padStart(2, '0')}:${m[2]}`; + if (m) { + const h = parseInt(m[1], 10) % 24; + return `${String(h).padStart(2, '0')}:${m[2]}`; + } const m2 = v.match(/T(\d{2}):(\d{2})/); - if (m2) return `${m2[1]}:${m2[2]}`; + if (m2) { + const h = parseInt(m2[1], 10) % 24; + return `${String(h).padStart(2, '0')}:${m2[2]}`; + } + const m3 = v.match(/(\d{1,2})h(\d{2})/i); + if (m3) { + const h = parseInt(m3[1], 10) % 24; + return `${String(h).padStart(2, '0')}:${m3[2]}`; + } return v.slice(0, 5).replace('h', ':'); } return String(v).slice(0, 5).replace('h', ':'); @@ -445,7 +458,15 @@ export default function EVAArrivees() { // Récupération type de gare useEffect(() => { let mounted = true; + const source = getSourceFromPath(window.location.pathname); const fetchStationType = async () => { + if (source === 'sncf') { + if (mounted) { + setStationType('ville'); + setResolvedGareId(null); + } + return; + } try { let fetchedType = null; let fetchedId = gareIdParam || null; @@ -475,9 +496,7 @@ export default function EVAArrivees() { } } catch (e) { /* ignore */ } }; - if (supabase) { - fetchStationType(); - } + fetchStationType(); return () => { mounted = false; }; }, [gareIdParam, gareDisplay]); @@ -831,14 +850,55 @@ export default function EVAArrivees() { }); }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]); - // Récupération Supabase avec polling + // Récupération des données (SNCF GTFS ou Supabase) avec polling useEffect(() => { let mounted = true; let inFlight = false; + const source = getSourceFromPath(window.location.pathname); const fetchData = async () => { if (!mounted || inFlight) return; inFlight = true; + + if (source === 'sncf') { + setLoading(true); + setError(null); + try { + const { data: sncfData, error: sncfErr } = await getSncfTimetable({ + gareName: gareDisplay || gareParam, + mode: 'arrivees' + }); + if (!mounted) { inFlight = false; return; } + if (sncfErr) { + setError(sncfErr); + } else { + const formatted = (sncfData || []).map(t => { + const timeStr = t.heure_arrivee || t.heure_depart || t.time || ''; + return { + ...t, + _displayTime: formatTimeWithColon(timeStr), + _displayPlatform: t.quai || t.voie || '', + _arrHM: t._arrHM || 0, + _status: t._status || { + label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"), + detail: '', + delayMinutes: t.retard_minutes || null, + causeDetails: t.cause_retard || null + } + }; + }); + setHoraires(formatted.slice(0, 10)); + setAllHoraires(formatted); + } + } catch (e) { + if (mounted) setError(e.message || String(e)); + } finally { + if (mounted) setLoading(false); + inFlight = false; + } + return; + } + if (!supabase) { setLoading(false); inFlight = false; @@ -936,9 +996,12 @@ export default function EVAArrivees() { const parseTimeHM = (s) => { if (!s) return null; - if (typeof s === 'number') return s; - const m = String(s).match(/(\d{1,2}):(\d{2})/); - if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10); + if (typeof s === 'number') return s % 1440; + const m = String(s).match(/(\d{1,2})[:h](\d{2})/i); + if (m) { + const h = parseInt(m[1], 10) % 24; + return h * 60 + parseInt(m[2], 10); + } const d = new Date(s); if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes(); return null; @@ -1129,7 +1192,7 @@ export default function EVAArrivees() { fetchData(); const intervalId = setInterval(fetchData, 15000); return () => { mounted = false; clearInterval(intervalId); }; - }, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]); + }, [gareDisplay, gareParam, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]); // Alternance retard / texte toutes les 2s const [alternateDelay, setAlternateDelay] = useState(false); diff --git a/src/pages/afficheurs/eva/EVADeparts.js b/src/pages/afficheurs/eva/EVADeparts.js index 0055be4..eb891d8 100644 --- a/src/pages/afficheurs/eva/EVADeparts.js +++ b/src/pages/afficheurs/eva/EVADeparts.js @@ -5,21 +5,34 @@ import { fetchAllRows } from '../../../lib/fetchAllRows'; import MarqueeText from '../../../components/MarqueeText'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; +import { getSourceFromPath } from '../../../services/railwayService'; +import { getSncfTimetable } from '../../../services/sncfApi'; // Helper utilitaire : format HH:MM avec deux points (ex: 22:39) const formatTimeWithColon = (v) => { if (v == null) return ''; const tryDate = new Date(v); if (!isNaN(tryDate.getTime())) { - const hh = String(tryDate.getHours()).padStart(2, '0'); + const hh = String(tryDate.getHours() % 24).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0'); return `${hh}:${mm}`; } if (typeof v === 'string') { const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); - if (m) return `${String(m[1]).padStart(2, '0')}:${m[2]}`; + if (m) { + const h = parseInt(m[1], 10) % 24; + return `${String(h).padStart(2, '0')}:${m[2]}`; + } const m2 = v.match(/T(\d{2}):(\d{2})/); - if (m2) return `${m2[1]}:${m2[2]}`; + if (m2) { + const h = parseInt(m2[1], 10) % 24; + return `${String(h).padStart(2, '0')}:${m2[2]}`; + } + const m3 = v.match(/(\d{1,2})h(\d{2})/i); + if (m3) { + const h = parseInt(m3[1], 10) % 24; + return `${String(h).padStart(2, '0')}:${m3[2]}`; + } return v.slice(0, 5).replace('h', ':'); } return String(v).slice(0, 5).replace('h', ':'); @@ -441,7 +454,15 @@ export default function EVADeparts() { // Récupération type de gare useEffect(() => { let mounted = true; + const source = getSourceFromPath(window.location.pathname); const fetchStationType = async () => { + if (source === 'sncf') { + if (mounted) { + setStationType('ville'); + setResolvedGareId(null); + } + return; + } try { let fetchedType = null; let fetchedId = gareIdParam || null; @@ -471,9 +492,7 @@ export default function EVADeparts() { } } catch (e) { /* ignore */ } }; - if (supabase) { - fetchStationType(); - } + fetchStationType(); return () => { mounted = false; }; }, [gareIdParam, gareDisplay]); @@ -827,14 +846,52 @@ export default function EVADeparts() { }); }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]); - // Récupération Supabase avec polling + // Récupération des données (SNCF GTFS ou Supabase) avec polling useEffect(() => { let mounted = true; let inFlight = false; + const source = getSourceFromPath(window.location.pathname); const fetchData = async () => { if (!mounted || inFlight) return; inFlight = true; + + if (source === 'sncf') { + setLoading(true); + setError(null); + try { + const { data: sncfData, error: sncfErr } = await getSncfTimetable({ + gareName: gareDisplay || gareParam, + mode: 'departs' + }); + if (!mounted) { inFlight = false; return; } + if (sncfErr) { + setError(sncfErr); + } else { + const formatted = (sncfData || []).map(t => ({ + ...t, + _displayTime: formatTimeWithColon(t.heure_depart), + _displayPlatform: t.quai || t.voie || '', + _depHM: t._depHM || 0, + _status: t._status || { + label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"), + detail: '', + delayMinutes: t.retard_minutes || null, + causeDetails: t.cause_retard || null + } + })); + setHoraires(formatted.slice(0, 10)); + setAllHoraires(formatted); + } + } catch (e) { + if (mounted) setError(e.message || String(e)); + } finally { + if (mounted) setLoading(false); + inFlight = false; + } + return; + } + if (!supabase) { setLoading(false); inFlight = false; @@ -952,9 +1009,12 @@ export default function EVADeparts() { const parseTimeHM = (s) => { if (!s) return null; - if (typeof s === 'number') return s; - const m = String(s).match(/(\d{1,2}):(\d{2})/); - if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10); + if (typeof s === 'number') return s % 1440; + const m = String(s).match(/(\d{1,2})[:h](\d{2})/i); + if (m) { + const h = parseInt(m[1], 10) % 24; + return h * 60 + parseInt(m[2], 10); + } const d = new Date(s); if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes(); return null; @@ -1145,7 +1205,7 @@ export default function EVADeparts() { fetchData(); const intervalId = setInterval(fetchData, 15000); return () => { mounted = false; clearInterval(intervalId); }; - }, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]); + }, [gareDisplay, gareParam, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]); // Alternance retard / texte toutes les 2s const [alternateDelay, setAlternateDelay] = useState(false);