From 8cd48549249ba89a77b191e218b3352a0faa52d3 Mon Sep 17 00:00:00 2001 From: ElPoyo Date: Mon, 9 Feb 2026 10:14:52 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20Am=C3=A9lioration=20des=20performan?= =?UTF-8?q?ces=20et=20migration=20des=20Cloud=20Functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cette mise à jour majeure vise à améliorer significativement les performances de l'application, en particulier au démarrage, et à standardiser l'infrastructure backend. Les principaux changements incluent la migration de toutes les Cloud Functions vers une région européenne (`europe-west9`), l'optimisation du chargement des données, et l'introduction d'un moniteur de performance pour le débogage. **Changements Backend (Cloud Functions) :** - **Migration de la Région :** - Toutes les Cloud Functions ont été déplacées de `us-central1` à `europe-west9` (Paris) pour réduire la latence pour les utilisateurs européens. Cela concerne les appels depuis le frontend (ex: `api_config.dart`, `email_service.dart`) et les définitions des fonctions elles-mêmes (`index.js`, etc.). - **Standardisation des Fonctions :** - La plupart des fonctions `onCall` (v1) ont été migrées vers le format `onRequest` (v2) avec une gestion d'authentification et de CORS unifiée, améliorant la robustesse et la cohérence. - Les triggers Firestore (`onDocumentCreated`, `onDocumentUpdated`) et les tâches planifiées (`onSchedule`) ont été mis à jour pour spécifier explicitement la région `europe-west9`. - **Mise à jour des Index Firestore :** - Les index `firestore.indexes.json` ont été mis à jour pour supporter les nouvelles requêtes de l'application et optimiser les performances de filtrage. **Améliorations des Performances Frontend :** - **Chargement Asynchrone et Mis en Cache :** - Le chargement des données utilisateur (`LocalUserProvider`) et des événements (`EventProvider`) a été optimisé pour utiliser un cache local à court terme (5 minutes pour l'utilisateur, 30 secondes pour les événements). - Les données ne sont rechargées que si le cache a expiré ou si un rechargement est forcé, évitant des appels réseau redondants et accélérant la navigation. - **Démarrage de l'Application Optimisé :** - Le processus de connexion automatique (`main.dart`) a été revu. L'application navigue désormais immédiatement vers la page demandée sans attendre la fin du chargement des données utilisateur, qui s'effectue en arrière-plan. - Un écran de chargement plus esthétique avec le logo de l'entreprise a été ajouté, remplaçant l'indicateur de chargement simple. - **Chargement de la Page Calendrier :** - Le chargement et la sélection de l'événement par défaut sur la page `CalendarPage` sont maintenant entièrement asynchrones, rendant l'affichage de la page quasi instantané. **Nouveaux Outils et Améliorations UX :** - **Moniteur de Performance :** - Ajout d'un nouvel outil `PerformanceMonitor` (`lib/utils/performance_monitor.dart`) pour mesurer précisément le temps d'exécution des opérations critiques (appels API, parsing, etc.) en mode débogage. Il aide à identifier les goulots d'étranglement. - **Amélioration du Formulaire de Connexion :** - Les champs "Email" et "Mot de passe" sur la page de connexion (`LoginPage`) supportent désormais l'autocomplétion du navigateur (`AutofillGroup`). - Appuyer sur "Entrée" dans l'un des champs déclenche désormais la connexion, améliorant l'ergonomie. **Mise à jour de la version :** - La version de l'application a été incrémentée à `1.0.9`. --- em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache | 26 +- em2rp/SYSTEME_MISE_A_JOUR.md | 337 ------------------ em2rp/firestore.indexes.json | 91 ++++- em2rp/functions/.env | 9 + em2rp/functions/createAlert.js | 6 +- em2rp/functions/index.js | 50 ++- em2rp/functions/processEquipmentValidation.js | 5 +- em2rp/functions/sendAlertEmail.js | 36 +- em2rp/lib/config/api_config.dart | 4 +- em2rp/lib/config/app_version.dart | 2 +- em2rp/lib/main.dart | 54 ++- em2rp/lib/providers/event_provider.dart | 56 ++- em2rp/lib/providers/local_user_provider.dart | 41 ++- em2rp/lib/services/data_service.dart | 6 +- em2rp/lib/services/email_service.dart | 2 +- em2rp/lib/services/event_form_service.dart | 2 +- em2rp/lib/utils/performance_monitor.dart | 129 +++++++ em2rp/lib/views/calendar_page.dart | 114 +++--- em2rp/lib/views/event_preparation_page.dart | 2 +- em2rp/lib/views/login_page.dart | 72 ++-- .../views/widgets/auth/mail_textfield.dart | 5 + .../widgets/auth/password_textfield.dart | 5 + em2rp/pubspec.yaml | 2 +- em2rp/web/version.json | 4 +- 24 files changed, 545 insertions(+), 515 deletions(-) delete mode 100644 em2rp/SYSTEME_MISE_A_JOUR.md create mode 100644 em2rp/functions/.env create mode 100644 em2rp/lib/utils/performance_monitor.dart diff --git a/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache b/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache index 8c7bf2f..2f8c4a6 100644 --- a/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache +++ b/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache @@ -32,16 +32,16 @@ assets/assets/images/tshirt-incrust.webp,1737393735487,af7cb34adfca19c0b41c8eb63 assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d -version.json,1768738172901,f258e76dbf34b4a64999cb6d1d983255ad592c590e53f7c4fe380b2bfef82762 -index.html,1768738180374,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10 -flutter_service_worker.js,1768738281912,ad5fcbc95e3f4e31b6c3ae92df0a872c24434ba7ac7448fdd9359f2e3bf7d76c -assets/FontManifest.json,1768738277185,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5 -flutter_bootstrap.js,1768738180360,f1963883a54097e939404b503b6a9963408fe0187a18d73adb648f6ef0f81578 -assets/AssetManifest.bin.json,1768738277185,f446eb3de964f3a6f9e76fcc98d79a81b0429e076c9c7bf30cf8edd0263a0b0a -assets/AssetManifest.bin,1768738277184,72bbccb69d9a02d3885df0c5e58ebfed29e25a4919e10bf195b59542f4709ca3 -assets/AssetManifest.json,1768738277185,1e1501af5844823ef215cf650f4cef4002c0389d88770225ac07576d57dc1067 -assets/shaders/ink_sparkle.frag,1768738277454,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406 -assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1768738280959,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb -assets/fonts/MaterialIcons-Regular.otf,1768738280969,9e7c35e587de73a0aee5675d5aef4c6830478af0aa31ad0da76b84a503906b03 -assets/NOTICES,1768738277188,fc20c3c3c998057eb7e58ad2e009c7268bf748bfde685e95130431f4c54bd51c -main.dart.js,1768738275891,4ef7f90056f38602de6430a68a479a005268f9d83395ad9b444337c214a3710c +version.json,1770478530807,2cbfdf7f34574c2f9d4f1af02acb86d8d230af93790c97a3c7e1674c4db42ef4 +index.html,1770478536326,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10 +flutter_service_worker.js,1770478628965,cb72807cfcb05b0a2e7b3f4f0cf618a0284a3d2476c93672bd86ea99670b0f5d +assets/FontManifest.json,1770478624084,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5 +assets/AssetManifest.json,1770478624084,1e1501af5844823ef215cf650f4cef4002c0389d88770225ac07576d57dc1067 +flutter_bootstrap.js,1770478536318,bf4a3b4bf79eaed1ce24892f20cfb270bcc22fb392bc9f6a1d17aeed42ed4ed8 +assets/AssetManifest.bin.json,1770478624084,f446eb3de964f3a6f9e76fcc98d79a81b0429e076c9c7bf30cf8edd0263a0b0a +assets/AssetManifest.bin,1770478624084,72bbccb69d9a02d3885df0c5e58ebfed29e25a4919e10bf195b59542f4709ca3 +assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1770478628013,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb +assets/shaders/ink_sparkle.frag,1770478624492,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406 +assets/fonts/MaterialIcons-Regular.otf,1770478628013,50e06fd231edee237d875cddbae1e22b682d32bb1284e3c32ca409fa489f9c21 +assets/NOTICES,1770478624086,d02d64a466e62fdaeee2534a3f65541362ccf29beb495e2af0fdce41f4ae28d9 +main.dart.js,1770478620736,03d43aeaa96cfdbe5b7491f9610223ec95c29d47095570dd61cd6cddac863496 diff --git a/em2rp/SYSTEME_MISE_A_JOUR.md b/em2rp/SYSTEME_MISE_A_JOUR.md deleted file mode 100644 index db72b84..0000000 --- a/em2rp/SYSTEME_MISE_A_JOUR.md +++ /dev/null @@ -1,337 +0,0 @@ -# Système de Gestion des Mises à Jour - EM2RP - -## 📋 Vue d'ensemble - -Ce système permet de gérer automatiquement les mises à jour de l'application web Flutter, en notifiant les utilisateurs et en forçant le rechargement du cache si nécessaire. - ---- - -## 🔧 Architecture - -### Fichiers impliqués - -#### Configuration -- **`lib/config/app_version.dart`** : Fichier source de vérité pour la version -- **`web/version.json`** : Fichier déployé avec l'app pour vérification côté serveur - -#### Services -- **`lib/services/update_service.dart`** : Service de vérification des mises à jour -- **`lib/views/widgets/common/update_dialog.dart`** : Widget d'affichage du dialog de mise à jour - -#### Scripts -- **`scripts/increment_version.js`** : Incrémente automatiquement la version -- **`scripts/update_version_json.js`** : Génère version.json depuis app_version.dart -- **`deploy.bat`** : Script de déploiement complet - -#### Documentation -- **`CHANGELOG.md`** : Notes de version (utilisées dans le dialog) - ---- - -## 🚀 Workflow de déploiement - -### 1. Développement normal -Travaillez normalement sur votre code en mode développement. - -### 2. Déploiement d'une nouvelle version -```bash -deploy.bat -``` - -Ce script exécute automatiquement : -1. ✅ Bascule en mode PRODUCTION -2. ✅ **Incrémente la version** (0.3.8 → 0.3.9) -3. ✅ **Incrémente le buildNumber** (1 → 2) -4. ✅ **Génère version.json** depuis app_version.dart -5. ✅ Build Flutter Web -6. ✅ Déploie sur Firebase Hosting -7. ✅ Retour en mode DÉVELOPPEMENT - -### 3. Mise à jour côté utilisateur -Au prochain chargement de l'app (ou après 2 secondes) : -- L'app vérifie `https://em2rp.web.app/version.json` -- Compare avec la version locale dans `app_version.dart` -- Si `buildNumber serveur > buildNumber local` → Affiche le dialog - ---- - -## 📝 Format de version - -### app_version.dart -```dart -class AppVersion { - static const String version = '0.3.8'; // Version sémantique - static const int buildNumber = 1; // Numéro de build (incrémenté automatiquement) - - static String get fullVersion => 'v$version'; - static String get fullVersionWithBuild => 'v$version+$buildNumber'; -} -``` - -### version.json (déployé) -```json -{ - "version": "0.3.8", - "buildNumber": 1, - "updateUrl": "https://em2rp.web.app", - "forceUpdate": false, - "releaseNotes": "• Scanner QR Code\n• Génération QR conteneurs\n• Performance améliorée" -} -``` - ---- - -## 🔄 Comparaison des versions - -Le système compare uniquement le **buildNumber** : -- `buildNumber serveur > buildNumber local` → Mise à jour disponible -- Ignore les versions identiques même si la version sémantique change - -**Exemple** : -- Local : `0.3.8+1` -- Serveur : `0.3.9+2` -- Résultat : Mise à jour proposée (2 > 1) ✅ - ---- - -## 🎨 Expérience utilisateur - -### Mise à jour normale (forceUpdate: false) -``` -┌────────────────────────────────────┐ -│ 🔄 Mise à jour disponible │ -├────────────────────────────────────┤ -│ Version actuelle : 0.3.8 (1) │ -│ Nouvelle version : 0.3.9 (2) │ -│ │ -│ Nouveautés : │ -│ • Scanner QR Code │ -│ • Performance améliorée │ -│ │ -│ [Plus tard] [Mettre à jour] 🔄 │ -└────────────────────────────────────┘ -``` - -### Mise à jour forcée (forceUpdate: true) -``` -┌────────────────────────────────────┐ -│ ⚠️ Mise à jour requise │ -├────────────────────────────────────┤ -│ Version actuelle : 0.3.8 (1) │ -│ Nouvelle version : 0.3.9 (2) │ -│ │ -│ ⚠️ Cette mise à jour est │ -│ obligatoire pour continuer │ -│ │ -│ [Mettre à jour] 🔄 │ -└────────────────────────────────────┘ -``` - ---- - -## 🛠️ Utilisation avancée - -### Forcer une mise à jour critique -Si vous déployez un correctif critique : - -1. Modifiez `web/version.json` **après le déploiement** : -```json -{ - "version": "0.3.9", - "buildNumber": 2, - "forceUpdate": true, // ← Changer à true - "releaseNotes": "🔴 Correctif de sécurité important" -} -``` - -2. Les utilisateurs ne pourront plus fermer le dialog jusqu'à la mise à jour - -### Personnaliser les notes de version -Éditez `CHANGELOG.md` avant le déploiement : - -```markdown -## [0.3.9] - 2026-01-16 - -### Ajouté -- Scanner QR Code pour équipements -- Génération QR pour conteneurs - -### Amélioré -- Performance du dialog de sélection -- Gestion du cache - -### Corrigé -- Bug de cache des équipements -``` - -Les 5 premières lignes de la section seront utilisées dans le dialog. - ---- - -## 🧪 Tests - -### Test 1 : Vérification de version locale -```dart -// Dans n'importe quel fichier -import 'package:em2rp/config/app_version.dart'; - -print('Version: ${AppVersion.version}'); -print('Build: ${AppVersion.buildNumber}'); -print('Full: ${AppVersion.fullVersionWithBuild}'); -``` - -### Test 2 : Forcer l'affichage du dialog -Modifiez temporairement `web/version.json` : -```json -{ - "buildNumber": 999 // Très grand nombre -} -``` - -Rechargez l'app → Le dialog s'affiche immédiatement - -### Test 3 : Tester le rechargement -1. Cliquez sur "Mettre à jour" -2. Vérifiez que la page se recharge -3. Vérifiez que le cache est vidé (nouvelles ressources chargées) - ---- - -## 📊 Logs de debug - -En mode debug, des logs sont affichés dans la console : - -``` -[UpdateService] Current version: 0.3.8+1 -[UpdateService] Server version: 0.3.9+2 -``` - -Si pas de mise à jour disponible, rien ne s'affiche. - ---- - -## 🔐 Sécurité - -### Headers HTTP pour forcer le non-cache -Le fichier `web/index.html` contient : -```html - - - -``` - -### Cache-busting sur version.json -Chaque requête ajoute un timestamp : -```dart -final timestamp = DateTime.now().millisecondsSinceEpoch; -Uri.parse('$versionUrl?t=$timestamp') -``` - -Garantit que la version la plus récente est toujours récupérée. - ---- - -## 🚨 Résolution de problèmes - -### Problème : Le dialog ne s'affiche pas -**Causes possibles :** -1. Le `buildNumber` serveur n'est pas supérieur au local -2. Erreur réseau (timeout 10s) -3. Le fichier `version.json` n'existe pas sur le serveur - -**Solution :** -```bash -# Vérifier la version déployée -curl https://em2rp.web.app/version.json - -# Forcer un nouveau déploiement -deploy.bat -``` - -### Problème : Le cache ne se vide pas -**Causes possibles :** -1. Service Worker actif (ancienne version) -2. Cache navigateur très persistant - -**Solution :** -```javascript -// Dans les DevTools du navigateur -navigator.serviceWorker.getRegistrations().then(registrations => { - registrations.forEach(r => r.unregister()); -}); - -// Puis CTRL+SHIFT+R (rechargement forcé) -``` - -### Problème : Le script increment_version.js échoue -**Solution :** -```bash -# Vérifier la syntaxe du fichier app_version.dart -# Doit contenir exactement : -static const String version = '0.3.8'; -static const int buildNumber = 1; -``` - ---- - -## 📈 Évolution future - -### Fonctionnalités possibles -- [ ] Afficher un changelog complet dans le dialog -- [ ] Permettre de sauter une version (skip this version) -- [ ] Notifications push pour les mises à jour critiques -- [ ] Analytics sur le taux d'adoption des mises à jour -- [ ] Support des mises à jour en arrière-plan - -### Améliorations techniques -- [ ] Utiliser un CDN pour version.json -- [ ] Implémenter un rollback automatique si erreur -- [ ] Ajouter une vérification de santé post-déploiement - ---- - -## 🎯 Commandes rapides - -```bash -# Déployer une nouvelle version -deploy.bat - -# Incrémenter manuellement la version -node scripts\increment_version.js - -# Générer version.json manuellement -node scripts\update_version_json.js - -# Vérifier la version actuelle -type lib\config\app_version.dart - -# Vérifier la version déployée -curl https://em2rp.web.app/version.json -``` - ---- - -## ✅ Checklist de déploiement - -Avant chaque déploiement : - -- [ ] Tester l'application en local -- [ ] Mettre à jour `CHANGELOG.md` avec les nouveautés -- [ ] Vérifier que tous les tests passent -- [ ] Exécuter `deploy.bat` -- [ ] Vérifier le déploiement sur https://em2rp.web.app -- [ ] Tester la mise à jour sur un navigateur propre -- [ ] Informer l'équipe de la nouvelle version - ---- - -## 📞 Support - -En cas de problème avec le système de mise à jour, vérifier : -1. Les logs dans la console du navigateur -2. Le fichier `version.json` déployé -3. Le fichier `app_version.dart` local -4. La connexion réseau de l'utilisateur - -**Le système est conçu pour échouer silencieusement** : Si une erreur se produit, l'utilisateur peut continuer à utiliser l'app normalement sans être bloqué. - diff --git a/em2rp/firestore.indexes.json b/em2rp/firestore.indexes.json index 6dc6673..181f03d 100644 --- a/em2rp/firestore.indexes.json +++ b/em2rp/firestore.indexes.json @@ -1,23 +1,97 @@ { "indexes": [ { - "collectionGroup": "events", + "collectionGroup": "alerts", "queryScope": "COLLECTION", "fields": [ { - "fieldPath": "EndDateTime", + "fieldPath": "assignedTo", + "arrayConfig": "CONTAINS" + }, + { + "fieldPath": "isRead", "order": "ASCENDING" }, { - "fieldPath": "StartDateTime", - "order": "ASCENDING" + "fieldPath": "createdAt", + "order": "DESCENDING" + } + ] + }, + { + "collectionGroup": "alerts", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "assignedTo", + "arrayConfig": "CONTAINS" }, { "fieldPath": "status", "order": "ASCENDING" }, { - "fieldPath": "__name__", + "fieldPath": "createdAt", + "order": "DESCENDING" + } + ] + }, + { + "collectionGroup": "containers", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "status", + "order": "ASCENDING" + }, + { + "fieldPath": "id", + "order": "ASCENDING" + } + ] + }, + { + "collectionGroup": "containers", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "status", + "order": "ASCENDING" + }, + { + "fieldPath": "type", + "order": "ASCENDING" + }, + { + "fieldPath": "id", + "order": "ASCENDING" + } + ] + }, + { + "collectionGroup": "containers", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "type", + "order": "ASCENDING" + }, + { + "fieldPath": "id", + "order": "ASCENDING" + } + ] + }, + { + "collectionGroup": "equipments", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "category", + "order": "ASCENDING" + }, + { + "fieldPath": "id", "order": "ASCENDING" } ] @@ -27,7 +101,7 @@ "queryScope": "COLLECTION", "fields": [ { - "fieldPath": "status", + "fieldPath": "EndDateTime", "order": "ASCENDING" }, { @@ -35,12 +109,11 @@ "order": "ASCENDING" }, { - "fieldPath": "EndDateTime", + "fieldPath": "status", "order": "ASCENDING" } ] } ], "fieldOverrides": [] -} - +} \ No newline at end of file diff --git a/em2rp/functions/.env b/em2rp/functions/.env new file mode 100644 index 0000000..5af40c5 --- /dev/null +++ b/em2rp/functions/.env @@ -0,0 +1,9 @@ +# Configuration SMTP pour l'envoi d'emails +SMTP_HOST="mail.em2events.fr" +SMTP_PORT=465 +SMTP_USER="notify@em2events.fr" +SMTP_PASS="aL8@Rx8xqFrNij$a" + +# URL de l'application +APP_URL="https://app.em2events.fr" + diff --git a/em2rp/functions/createAlert.js b/em2rp/functions/createAlert.js index ce00614..1f8ab87 100644 --- a/em2rp/functions/createAlert.js +++ b/em2rp/functions/createAlert.js @@ -46,7 +46,11 @@ const withCors = (handler) => { * Crée une alerte et envoie les notifications * Gère tout le processus côté backend de A à Z */ -exports.createAlert = onRequest({cors: false, invoker: 'public'}, withCors(async (req, res) => { +exports.createAlert = onRequest({ + cors: false, + invoker: 'public', + region: 'europe-west9' +}, withCors(async (req, res) => { try { // Vérifier l'authentification const decodedToken = await auth.authenticateUser(req); diff --git a/em2rp/functions/index.js b/em2rp/functions/index.js index 0abdb5d..d6512b8 100644 --- a/em2rp/functions/index.js +++ b/em2rp/functions/index.js @@ -28,6 +28,7 @@ const db = admin.firestore(); const httpOptions = { cors: false, invoker: 'public', // Permet les invocations non authentifiées (l'auth est gérée par notre token Firebase) + region: 'europe-west9', // Région européenne (Paris) // Version: 2.0 - Ajout de l'invoker public pour résoudre les problèmes CORS }; @@ -2049,19 +2050,20 @@ exports.getUsers = onRequest(httpOptions, withCors(async (req, res) => { * Récupère un utilisateur spécifique par son ID * Tout utilisateur authentifié peut accéder aux données publiques */ -exports.getUser = onCall(async (request) => { +exports.getUser = onRequest(httpOptions, withCors(async (req, res) => { try { - await authenticateUser(request); - const db = getFirestore(); + const decodedToken = await auth.authenticateUser(req); - const { userId } = request.data; + const { userId } = req.body.data || req.body || {}; if (!userId) { - throw new Error("userId is required"); + res.status(400).json({ error: 'userId is required' }); + return; } - const userDoc = await db.collection("users").doc(userId).get(); + const userDoc = await db.collection('users').doc(userId).get(); if (!userDoc.exists) { - throw new Error("User not found"); + res.status(404).json({ error: 'User not found' }); + return; } const user = userDoc.data(); @@ -2070,11 +2072,11 @@ exports.getUser = onCall(async (request) => { const userData = { id: userDoc.id, uid: user.uid || userDoc.id, - email: user.email || "", - firstName: user.firstName || "", - lastName: user.lastName || "", - phoneNumber: user.phoneNumber || "", - profilePhotoUrl: user.profilePhotoUrl || "", + email: user.email || '', + firstName: user.firstName || '', + lastName: user.lastName || '', + phoneNumber: user.phoneNumber || '', + profilePhotoUrl: user.profilePhotoUrl || '', }; // Inclure le rôle si disponible @@ -2088,12 +2090,12 @@ exports.getUser = onCall(async (request) => { } } - return { user: userData }; + res.status(200).json({ user: userData }); } catch (error) { - logger.error("Error fetching user:", error); - throw new Error(error.message || "Failed to fetch user"); + logger.error('Error fetching user:', error); + res.status(500).json({ error: error.message }); } -}); +})); // ============================================================================ @@ -3488,6 +3490,7 @@ const {sendDailyDigest} = require('./sendDailyDigest'); exports.sendDailyDigest = onSchedule({ schedule: '0 8 * * *', timeZone: 'Europe/Paris', + region: 'europe-west9', retryCount: 2, memory: '512MiB' }, async (context) => { @@ -3507,7 +3510,10 @@ exports.sendDailyDigest = onSchedule({ * Trigger : Nouvel événement créé * Envoie une notification à tous les membres de la workforce */ -exports.onEventCreated = onDocumentCreated('events/{eventId}', async (event) => { +exports.onEventCreated = onDocumentCreated({ + document: 'events/{eventId}', + region: 'europe-west9' +}, async (event) => { logger.info(`[onEventCreated] Événement créé: ${event.params.eventId}`); try { @@ -3547,7 +3553,10 @@ exports.onEventCreated = onDocumentCreated('events/{eventId}', async (event) => * Trigger : Événement modifié (workforce changée) * Envoie une notification aux nouveaux membres ajoutés à la workforce */ -exports.onEventUpdated = onDocumentUpdated('events/{eventId}', async (event) => { +exports.onEventUpdated = onDocumentUpdated({ + document: 'events/{eventId}', + region: 'europe-west9' +}, async (event) => { const before = event.data.before.data(); const after = event.data.after.data(); const eventId = event.params.eventId; @@ -3598,7 +3607,10 @@ exports.onEventUpdated = onDocumentUpdated('events/{eventId}', async (event) => * Trigger : Nouvelle alerte créée * Envoie un email immédiat si l'alerte est critique */ -exports.onAlertCreated = onDocumentCreated('alerts/{alertId}', async (event) => { +exports.onAlertCreated = onDocumentCreated({ + document: 'alerts/{alertId}', + region: 'europe-west9' +}, async (event) => { const alertId = event.params.alertId; const alertData = event.data.data(); diff --git a/em2rp/functions/processEquipmentValidation.js b/em2rp/functions/processEquipmentValidation.js index 8ab6c71..8aa424c 100644 --- a/em2rp/functions/processEquipmentValidation.js +++ b/em2rp/functions/processEquipmentValidation.js @@ -8,7 +8,10 @@ const {getSmtpConfig, EMAIL_CONFIG} = require('./utils/emailConfig'); * Appelée par le client lors du chargement/déchargement * Crée automatiquement les alertes nécessaires */ -exports.processEquipmentValidation = onCall({cors: true}, async (request) => { +exports.processEquipmentValidation = onCall({ + cors: true, + region: 'europe-west9' +}, async (request) => { try { // L'authentification est automatique avec onCall const {auth, data} = request; diff --git a/em2rp/functions/sendAlertEmail.js b/em2rp/functions/sendAlertEmail.js index 9ed0e3c..259b4f7 100644 --- a/em2rp/functions/sendAlertEmail.js +++ b/em2rp/functions/sendAlertEmail.js @@ -1,4 +1,4 @@ -const functions = require('firebase-functions'); +const {onCall} = require('firebase-functions/v2/https'); const admin = require('firebase-admin'); const nodemailer = require('nodemailer'); const handlebars = require('handlebars'); @@ -10,22 +10,19 @@ const {getSmtpConfig, EMAIL_CONFIG} = require('./utils/emailConfig'); * Envoie un email d'alerte à un utilisateur * Appelé par le client Dart via callable function */ -exports.sendAlertEmail = functions.https.onCall(async (data, context) => { +exports.sendAlertEmail = onCall({ + region: 'europe-west9', + cors: true +}, async (request) => { // Vérifier l'authentification - if (!context.auth) { - throw new functions.https.HttpsError( - 'unauthenticated', - 'L\'utilisateur doit être authentifié', - ); + if (!request.auth) { + throw new Error('L\'utilisateur doit être authentifié'); } - const {alertId, userId, templateType} = data; + const {alertId, userId, templateType} = request.data; if (!alertId || !userId) { - throw new functions.https.HttpsError( - 'invalid-argument', - 'alertId et userId sont requis', - ); + throw new Error('alertId et userId sont requis'); } try { @@ -36,10 +33,7 @@ exports.sendAlertEmail = functions.https.onCall(async (data, context) => { .get(); if (!alertDoc.exists) { - throw new functions.https.HttpsError( - 'not-found', - 'Alerte introuvable', - ); + throw new Error('Alerte introuvable'); } const alert = alertDoc.data(); @@ -51,10 +45,7 @@ exports.sendAlertEmail = functions.https.onCall(async (data, context) => { .get(); if (!userDoc.exists) { - throw new functions.https.HttpsError( - 'not-found', - 'Utilisateur introuvable', - ); + throw new Error('Utilisateur introuvable'); } const user = userDoc.data(); @@ -112,10 +103,7 @@ exports.sendAlertEmail = functions.https.onCall(async (data, context) => { }; } catch (error) { console.error('Erreur envoi email:', error); - throw new functions.https.HttpsError( - 'internal', - `Erreur lors de l'envoi de l'email: ${error.message}`, - ); + throw new Error(`Erreur lors de l'envoi de l'email: ${error.message}`); } }); diff --git a/em2rp/lib/config/api_config.dart b/em2rp/lib/config/api_config.dart index 0b65c06..9f78e33 100644 --- a/em2rp/lib/config/api_config.dart +++ b/em2rp/lib/config/api_config.dart @@ -4,8 +4,8 @@ class ApiConfig { static const bool isDevelopment = false; // false = utilise Cloud Functions prod // URL de base pour les Cloud Functions - static const String productionUrl = 'https://us-central1-em2rp-951dc.cloudfunctions.net'; - static const String developmentUrl = 'http://localhost:5001/em2rp-951dc/us-central1'; + static const String productionUrl = 'https://europe-west9-em2rp-951dc.cloudfunctions.net'; + static const String developmentUrl = 'http://localhost:5001/em2rp-951dc/europe-west9'; /// Retourne l'URL de base selon l'environnement static String get baseUrl => isDevelopment ? developmentUrl : productionUrl; diff --git a/em2rp/lib/config/app_version.dart b/em2rp/lib/config/app_version.dart index c8bd4dd..71565f1 100644 --- a/em2rp/lib/config/app_version.dart +++ b/em2rp/lib/config/app_version.dart @@ -1,6 +1,6 @@ /// Configuration de la version de l'application class AppVersion { - static const String version = '1.0.6'; + static const String version = '1.0.9'; /// Retourne la version complète de l'application static String get fullVersion => 'v$version'; diff --git a/em2rp/lib/main.dart b/em2rp/lib/main.dart index c296020..3b3744f 100644 --- a/em2rp/lib/main.dart +++ b/em2rp/lib/main.dart @@ -5,6 +5,7 @@ import 'package:em2rp/providers/container_provider.dart'; import 'package:em2rp/providers/maintenance_provider.dart'; import 'package:em2rp/providers/alert_provider.dart'; import 'package:em2rp/utils/auth_guard_widget.dart'; +import 'package:em2rp/utils/performance_monitor.dart'; import 'package:em2rp/views/alerts_page.dart'; import 'package:em2rp/views/calendar_page.dart'; import 'package:em2rp/views/login_page.dart'; @@ -203,22 +204,22 @@ class _AutoLoginWrapperState extends State { } Future _autoLogin() async { + PerformanceMonitor.start('App.autoLogin'); try { final localAuthProvider = Provider.of(context, listen: false); // Vérifier si l'utilisateur est déjà connecté if (FirebaseAuth.instance.currentUser == null && Env.isDevelopment) { + PerformanceMonitor.start('App.signIn'); // Connexion automatique en mode développement await localAuthProvider.signInWithEmailAndPassword( Env.devAdminEmail, Env.devAdminPassword, ); + PerformanceMonitor.end('App.signIn'); } - // Charger les données utilisateur - await localAuthProvider.loadUserData(); - if (mounted) { // MODIFIÉ : Vérifier si une route spécifique est demandée dans l'URL // En Flutter Web, on peut vérifier window.location.hash @@ -227,7 +228,7 @@ class _AutoLoginWrapperState extends State { print('[AutoLoginWrapper] Fragment URL: $fragment'); - // Si une route spécifique est demandée (autre que / ou vide) + // Navigation immédiate sans attendre le chargement des données if (fragment.isNotEmpty && fragment != '/' && fragment != '/calendar') { print('[AutoLoginWrapper] Redirection vers: $fragment'); Navigator.of(context).pushReplacementNamed(fragment); @@ -236,9 +237,18 @@ class _AutoLoginWrapperState extends State { print('[AutoLoginWrapper] Redirection vers: /calendar (défaut)'); Navigator.of(context).pushReplacementNamed('/calendar'); } + + PerformanceMonitor.end('App.autoLogin'); + PerformanceMonitor.printSummary(); + + // Charger les données utilisateur en arrière-plan + localAuthProvider.loadUserData().catchError((e) { + print('Error loading user data: $e'); + }); } } catch (e) { print('Auto login failed: $e'); + PerformanceMonitor.end('App.autoLogin'); if (mounted) { Navigator.of(context).pushReplacementNamed('/login'); } @@ -247,9 +257,41 @@ class _AutoLoginWrapperState extends State { @override Widget build(BuildContext context) { - return const Scaffold( + return Scaffold( + backgroundColor: Colors.white, body: Center( - child: CircularProgressIndicator(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo de l'application + Image.asset( + 'assets/logos/RectangleLogoBlack.png', + width: 200, + height: 200, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return const Icon( + Icons.event_available, + size: 80, + color: AppColors.rouge, + ); + }, + ), + const SizedBox(height: 40), + const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(AppColors.rouge), + ), + const SizedBox(height: 20), + const Text( + 'Chargement...', + style: TextStyle( + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w400, + ), + ), + ], + ), ), ); } diff --git a/em2rp/lib/providers/event_provider.dart b/em2rp/lib/providers/event_provider.dart index 24b9c8b..078e480 100644 --- a/em2rp/lib/providers/event_provider.dart +++ b/em2rp/lib/providers/event_provider.dart @@ -3,6 +3,7 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/services/data_service.dart'; import 'package:em2rp/services/api_service.dart'; +import 'package:em2rp/utils/performance_monitor.dart'; class EventProvider with ChangeNotifier { final DataService _dataService = DataService(FirebaseFunctionsApiService()); @@ -15,19 +16,43 @@ class EventProvider with ChangeNotifier { // Cache des utilisateurs chargés depuis getEvents Map> _usersCache = {}; + // Cache pour éviter les rechargements inutiles + DateTime? _lastLoadTime; + String? _lastUserId; + bool _lastCanViewAll = false; + + /// Vérifie si les données doivent être rechargées (cache de 30 secondes) + bool _shouldReload(String userId, bool canViewAllEvents) { + if (_lastLoadTime == null) return true; + if (_lastUserId != userId || _lastCanViewAll != canViewAllEvents) return true; + + final now = DateTime.now(); + final difference = now.difference(_lastLoadTime!); + return difference.inSeconds > 30; + } + /// Charger les événements d'un utilisateur via l'API - Future loadUserEvents(String userId, {bool canViewAllEvents = false}) async { + Future loadUserEvents(String userId, {bool canViewAllEvents = false, bool forceReload = false}) async { + PerformanceMonitor.start('EventProvider.loadUserEvents'); + + // Éviter les rechargements inutiles + if (!forceReload && !_shouldReload(userId, canViewAllEvents)) { + print('Using cached events (loaded ${DateTime.now().difference(_lastLoadTime!).inSeconds}s ago)'); + PerformanceMonitor.end('EventProvider.loadUserEvents'); + return; + } + _isLoading = true; notifyListeners(); - // Sauvegarder les paramètres - _saveLastLoadParams(userId, canViewAllEvents); - try { print('Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)'); + PerformanceMonitor.start('EventProvider.getEvents_API'); // Charger via l'API - les permissions sont vérifiées côté serveur final result = await _dataService.getEvents(userId: userId); + PerformanceMonitor.end('EventProvider.getEvents_API'); + final eventsData = result['events'] as List>; final usersData = result['users'] as Map; @@ -38,6 +63,7 @@ class EventProvider with ChangeNotifier { print('Found ${eventsData.length} events from API'); + PerformanceMonitor.start('EventProvider.parseEvents'); List allEvents = []; int failedCount = 0; @@ -51,23 +77,30 @@ class EventProvider with ChangeNotifier { failedCount++; } } + PerformanceMonitor.end('EventProvider.parseEvents'); _events = allEvents; + _lastLoadTime = DateTime.now(); + _lastUserId = userId; + _lastCanViewAll = canViewAllEvents; + print('Successfully loaded ${_events.length} events (${failedCount} failed)'); _isLoading = false; notifyListeners(); + PerformanceMonitor.end('EventProvider.loadUserEvents'); } catch (e) { print('Error loading events: $e'); _isLoading = false; notifyListeners(); + PerformanceMonitor.end('EventProvider.loadUserEvents'); rethrow; } } /// Recharger les événements (utilise le dernier userId) Future refreshEvents(String userId, {bool canViewAllEvents = false}) async { - await loadUserEvents(userId, canViewAllEvents: canViewAllEvents); + await loadUserEvents(userId, canViewAllEvents: canViewAllEvents, forceReload: true); } /// Récupérer un événement spécifique par ID @@ -157,16 +190,9 @@ class EventProvider with ChangeNotifier { /// Vider la liste des événements void clearEvents() { _events = []; + _lastLoadTime = null; + _lastUserId = null; + _lastCanViewAll = false; notifyListeners(); } - - // Variables pour stocker le dernier appel - String? _lastUserId; - bool _lastCanViewAll = false; - - /// Sauvegarder les paramètres du dernier chargement - void _saveLastLoadParams(String userId, bool canViewAllEvents) { - _lastUserId = userId; - _lastCanViewAll = canViewAllEvents; - } } diff --git a/em2rp/lib/providers/local_user_provider.dart b/em2rp/lib/providers/local_user_provider.dart index e544299..304d8a8 100644 --- a/em2rp/lib/providers/local_user_provider.dart +++ b/em2rp/lib/providers/local_user_provider.dart @@ -7,6 +7,7 @@ import '../models/notification_preferences_model.dart'; import '../utils/firebase_storage_manager.dart'; import '../services/api_service.dart'; import '../services/data_service.dart'; +import '../utils/performance_monitor.dart'; class LocalUserProvider with ChangeNotifier { UserModel? _currentUser; @@ -15,6 +16,9 @@ class LocalUserProvider with ChangeNotifier { final FirebaseStorageManager _storageManager = FirebaseStorageManager(); final DataService _dataService = DataService(apiService); + bool _isLoadingUserData = false; + DateTime? _lastUserDataLoad; + UserModel? get currentUser => _currentUser; String? get uid => _currentUser?.uid; String? get firstName => _currentUser?.firstName; @@ -25,18 +29,46 @@ class LocalUserProvider with ChangeNotifier { String? get phoneNumber => _currentUser?.phoneNumber; RoleModel? get currentRole => _currentRole; List get permissions => _currentRole?.permissions ?? []; + bool get isLoadingUserData => _isLoadingUserData; + + /// Vérifie si les données utilisateur doivent être rechargées + bool _shouldReloadUserData() { + if (_currentUser == null) return true; + if (_lastUserDataLoad == null) return true; + + final now = DateTime.now(); + final difference = now.difference(_lastUserDataLoad!); + return difference.inMinutes > 5; // Cache de 5 minutes pour les données utilisateur + } /// Charge les données de l'utilisateur actuel via Cloud Function - Future loadUserData() async { + Future loadUserData({bool forceReload = false}) async { if (_auth.currentUser == null) { print('No current user in Auth'); return; } + // Éviter les rechargements inutiles + if (!forceReload && !_shouldReloadUserData()) { + print('Using cached user data'); + return; + } + + // Éviter les appels simultanés + if (_isLoadingUserData) { + print('User data already loading, skipping'); + return; + } + + _isLoadingUserData = true; + PerformanceMonitor.start('LocalUserProvider.loadUserData'); print('Loading user data for: ${_auth.currentUser!.uid}'); try { // Utiliser la Cloud Function getCurrentUser + PerformanceMonitor.start('LocalUserProvider.getCurrentUser_API'); final result = await apiService.call('getCurrentUser', {}); + PerformanceMonitor.end('LocalUserProvider.getCurrentUser_API'); + final userData = result['user'] as Map; print('User data loaded from API: ${userData['uid']}'); @@ -59,9 +91,14 @@ class LocalUserProvider with ChangeNotifier { ); print('User data loaded successfully'); + _lastUserDataLoad = DateTime.now(); + _isLoadingUserData = false; notifyListeners(); + PerformanceMonitor.end('LocalUserProvider.loadUserData'); } catch (e) { print('Error loading user data: $e'); + _isLoadingUserData = false; + PerformanceMonitor.end('LocalUserProvider.loadUserData'); rethrow; } } @@ -76,6 +113,8 @@ class LocalUserProvider with ChangeNotifier { void clearUser() { _currentUser = null; _currentRole = null; + _lastUserDataLoad = null; + _isLoadingUserData = false; notifyListeners(); } diff --git a/em2rp/lib/services/data_service.dart b/em2rp/lib/services/data_service.dart index 4cf4318..4c8c0fb 100644 --- a/em2rp/lib/services/data_service.dart +++ b/em2rp/lib/services/data_service.dart @@ -196,7 +196,11 @@ class DataService { /// Crée une option Future createOption(String code, Map data) async { try { - final requestData = {'code': code, ...data}; + final requestData = { + 'id': code, // Ajouter l'ID en utilisant le code comme identifiant + 'code': code, + ...data + }; final result = await _apiService.call('createOption', requestData); return result['id'] as String? ?? code; } catch (e) { diff --git a/em2rp/lib/services/email_service.dart b/em2rp/lib/services/email_service.dart index 826f555..f7a125f 100644 --- a/em2rp/lib/services/email_service.dart +++ b/em2rp/lib/services/email_service.dart @@ -5,7 +5,7 @@ import 'package:firebase_auth/firebase_auth.dart'; /// Service d'envoi d'emails via Cloud Functions class EmailService { - final FirebaseFunctions _functions = FirebaseFunctions.instanceFor(region: 'us-central1'); + final FirebaseFunctions _functions = FirebaseFunctions.instanceFor(region: 'europe-west9'); /// Envoie un email d'alerte à un utilisateur /// diff --git a/em2rp/lib/services/event_form_service.dart b/em2rp/lib/services/event_form_service.dart index 2509115..6743102 100644 --- a/em2rp/lib/services/event_form_service.dart +++ b/em2rp/lib/services/event_form_service.dart @@ -71,7 +71,7 @@ class EventFormService { required String sourcePath, required String destinationPath, }) async { - final url = Uri.parse('https://us-central1-em2rp-951dc.cloudfunctions.net/moveEventFileV2'); + final url = Uri.parse('https://europe-west9-em2rp-951dc.cloudfunctions.net/moveEventFileV2'); final user = FirebaseAuth.instance.currentUser; final idToken = await user?.getIdToken(); diff --git a/em2rp/lib/utils/performance_monitor.dart b/em2rp/lib/utils/performance_monitor.dart new file mode 100644 index 0000000..2cfaab2 --- /dev/null +++ b/em2rp/lib/utils/performance_monitor.dart @@ -0,0 +1,129 @@ +import 'package:flutter/foundation.dart'; + +/// Service de monitoring des performances de l'application +/// Permet de mesurer les temps de chargement et d'identifier les goulots d'étranglement +class PerformanceMonitor { + static final Map _timings = {}; + static final Map _results = {}; + static bool _enabled = kDebugMode; // Actif uniquement en mode debug par défaut + + /// Active ou désactive le monitoring + static void setEnabled(bool enabled) { + _enabled = enabled; + } + + /// Démarre le chronomètre pour une opération + static void start(String key) { + if (!_enabled) return; + _timings[key] = DateTime.now(); + if (kDebugMode) { + print('[PerformanceMonitor] START: $key'); + } + } + + /// Arrête le chronomètre et affiche le résultat + static void end(String key) { + if (!_enabled) return; + + if (_timings.containsKey(key)) { + final duration = DateTime.now().difference(_timings[key]!); + _results[key] = duration; + _timings.remove(key); + + if (kDebugMode) { + final color = _getColorForDuration(duration); + print('[PerformanceMonitor] $color END: $key - ${duration.inMilliseconds}ms'); + } + } else { + if (kDebugMode) { + print('[PerformanceMonitor] ⚠️ No start time found for: $key'); + } + } + } + + /// Marque un point dans le temps (pour mesurer des étapes) + static void mark(String key) { + if (!_enabled) return; + + if (kDebugMode) { + print('[PerformanceMonitor] 📍 MARK: $key'); + } + } + + /// Récupère les résultats de toutes les mesures + static Map getResults() { + return Map.unmodifiable(_results); + } + + /// Affiche un résumé des performances + static void printSummary() { + if (!_enabled || _results.isEmpty) return; + + print('\n' + '=' * 60); + print('PERFORMANCE SUMMARY'); + print('=' * 60); + + // Trier par durée décroissante + final sortedResults = _results.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + + for (var entry in sortedResults) { + final color = _getColorForDuration(entry.value); + final ms = entry.value.inMilliseconds; + print('$color ${entry.key.padRight(40)} : ${ms.toString().padLeft(6)}ms'); + } + + final total = _results.values.fold( + Duration.zero, + (sum, duration) => sum + duration, + ); + print('${'=' * 60}'); + print('TOTAL: ${total.inMilliseconds}ms'); + print('=' * 60 + '\n'); + } + + /// Réinitialise toutes les mesures + static void reset() { + _timings.clear(); + _results.clear(); + if (kDebugMode) { + print('[PerformanceMonitor] 🔄 Reset'); + } + } + + /// Retourne une couleur basée sur la durée (pour les logs) + static String _getColorForDuration(Duration duration) { + final ms = duration.inMilliseconds; + if (ms < 100) return '🟢'; // Rapide + if (ms < 500) return '🟡'; // Moyen + if (ms < 1000) return '🟠'; // Lent + return '🔴'; // Très lent + } + + /// Mesure une opération asynchrone + static Future measure(String key, Future Function() operation) async { + start(key); + try { + final result = await operation(); + end(key); + return result; + } catch (e) { + end(key); + rethrow; + } + } + + /// Mesure une opération synchrone + static T measureSync(String key, T Function() operation) { + start(key); + try { + final result = operation(); + end(key); + return result; + } catch (e) { + end(key); + rethrow; + } + } +} + diff --git a/em2rp/lib/views/calendar_page.dart b/em2rp/lib/views/calendar_page.dart index c7f0f55..6a58c2f 100644 --- a/em2rp/lib/views/calendar_page.dart +++ b/em2rp/lib/views/calendar_page.dart @@ -1,5 +1,6 @@ import 'package:em2rp/providers/local_user_provider.dart'; import 'package:em2rp/providers/event_provider.dart'; +import 'package:em2rp/utils/performance_monitor.dart'; import 'package:flutter/material.dart'; import 'package:em2rp/views/widgets/nav/custom_app_bar.dart'; import 'package:em2rp/views/widgets/nav/main_drawer.dart'; @@ -35,52 +36,75 @@ class _CalendarPageState extends State { void initState() { super.initState(); initializeDateFormatting('fr_FR', null); - Future.microtask(() => _loadEvents()); - // Sélection automatique de l'événement le plus proche de maintenant - WidgetsBinding.instance.addPostFrameCallback((_) { - final eventProvider = Provider.of(context, listen: false); - final events = eventProvider.events; - if (events.isNotEmpty) { - final now = DateTime.now(); - // Pour mobile : sélectionner le premier événement du jour ou le prochain événement à venir - final todayEvents = events - .where((e) => - e.startDateTime.year == now.year && - e.startDateTime.month == now.month && - e.startDateTime.day == now.day) - .toList() - ..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); - EventModel? selected; - DateTime? selectedDay; - if (todayEvents.isNotEmpty) { - selected = todayEvents[0]; - selectedDay = DateTime(now.year, now.month, now.day); - } else { - // Chercher le prochain événement à venir - final futureEvents = events - .where((e) => e.startDateTime.isAfter(now)) - .toList() - ..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); - if (futureEvents.isNotEmpty) { - selected = futureEvents[0]; - selectedDay = DateTime(selected.startDateTime.year, - selected.startDateTime.month, selected.startDateTime.day); - } else { - // Aucun événement à venir, prendre le plus proche dans le passé - events.sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); - selected = events.last; - selectedDay = DateTime(selected.startDateTime.year, - selected.startDateTime.month, selected.startDateTime.day); - } - } - setState(() { - _selectedDay = selectedDay; - _focusedDay = selectedDay!; - _selectedEventIndex = 0; - _selectedEvent = selected; - }); + // Charger les événements de manière asynchrone sans bloquer l'UI + _loadEventsAsync(); + } + + /// Charge les événements de manière asynchrone et sélectionne l'événement approprié + Future _loadEventsAsync() async { + PerformanceMonitor.start('CalendarPage.loadEventsAsync'); + await _loadEvents(); + + // Sélectionner l'événement approprié après le chargement + if (mounted) { + PerformanceMonitor.start('CalendarPage.selectDefaultEvent'); + _selectDefaultEvent(); + PerformanceMonitor.end('CalendarPage.selectDefaultEvent'); + } + PerformanceMonitor.end('CalendarPage.loadEventsAsync'); + } + + /// Sélectionne automatiquement l'événement le plus proche de maintenant + void _selectDefaultEvent() { + final eventProvider = Provider.of(context, listen: false); + final events = eventProvider.events; + + if (events.isEmpty) return; + + final now = DateTime.now(); + + // Trouver les événements d'aujourd'hui + final todayEvents = events.where((e) { + final start = e.startDateTime; + return start.year == now.year && + start.month == now.month && + start.day == now.day; + }).toList()..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); + + EventModel? selected; + DateTime? selectedDay; + + if (todayEvents.isNotEmpty) { + selected = todayEvents[0]; + selectedDay = DateTime(now.year, now.month, now.day); + } else { + // Chercher le prochain événement à venir + final futureEvents = events + .where((e) => e.startDateTime.isAfter(now)) + .toList()..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); + + if (futureEvents.isNotEmpty) { + selected = futureEvents[0]; + final start = selected.startDateTime; + selectedDay = DateTime(start.year, start.month, start.day); + } else { + // Aucun événement à venir, prendre le plus récent + final sortedEvents = events.toList() + ..sort((a, b) => b.startDateTime.compareTo(a.startDateTime)); + selected = sortedEvents.first; + final start = selected.startDateTime; + selectedDay = DateTime(start.year, start.month, start.day); } - }); + } + + if (mounted) { + setState(() { + _selectedDay = selectedDay; + _focusedDay = selectedDay!; + _selectedEventIndex = 0; + _selectedEvent = selected; + }); + } } Future _loadEvents() async { diff --git a/em2rp/lib/views/event_preparation_page.dart b/em2rp/lib/views/event_preparation_page.dart index e094ff7..00c4cb8 100644 --- a/em2rp/lib/views/event_preparation_page.dart +++ b/em2rp/lib/views/event_preparation_page.dart @@ -430,7 +430,7 @@ class _EventPreparationPageState extends State with Single }; }).toList(); - final result = await FirebaseFunctions.instanceFor(region: 'us-central1') + final result = await FirebaseFunctions.instanceFor(region: 'europe-west9') .httpsCallable('processEquipmentValidation') .call({ 'eventId': _currentEvent.id, diff --git a/em2rp/lib/views/login_page.dart b/em2rp/lib/views/login_page.dart index fba89c2..13bf0d4 100644 --- a/em2rp/lib/views/login_page.dart +++ b/em2rp/lib/views/login_page.dart @@ -58,41 +58,45 @@ class LoginPage extends StatelessWidget { Widget _buildLoginForm(BuildContext context) { return Consumer( builder: (context, loginViewModel, child) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const LogoWidget(), - const SizedBox(height: 30), - const WelcomeTextWidget(), - const SizedBox(height: 40), - EmailTextFieldWidget( - emailController: loginViewModel.emailController, - highlightEmailField: loginViewModel.highlightEmailField, - ), - const SizedBox(height: 20), - PasswordTextFieldWidget( - passwordController: loginViewModel.passwordController, - obscurePassword: loginViewModel.obscurePassword, - highlightPasswordField: loginViewModel.highlightPasswordField, - onTogglePasswordVisibility: - loginViewModel.togglePasswordVisibility, - ), - ForgotPasswordButtonWidget( - onPressed: () => showDialog( - context: context, - builder: (BuildContext context) => - const ForgotPasswordDialogWidget(), + return AutofillGroup( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const LogoWidget(), + const SizedBox(height: 30), + const WelcomeTextWidget(), + const SizedBox(height: 40), + EmailTextFieldWidget( + emailController: loginViewModel.emailController, + highlightEmailField: loginViewModel.highlightEmailField, + onSubmitted: () => loginViewModel.signIn(context), ), - ), - const SizedBox(height: 30), - LoginButtonWidget( - isLoading: loginViewModel.isLoading, - onPressed: () => loginViewModel.signIn(context), - ), - const SizedBox(height: 20), - ErrorMessageWidget(errorMessage: loginViewModel.errorMessage), - ], + const SizedBox(height: 20), + PasswordTextFieldWidget( + passwordController: loginViewModel.passwordController, + obscurePassword: loginViewModel.obscurePassword, + highlightPasswordField: loginViewModel.highlightPasswordField, + onTogglePasswordVisibility: + loginViewModel.togglePasswordVisibility, + onSubmitted: () => loginViewModel.signIn(context), + ), + ForgotPasswordButtonWidget( + onPressed: () => showDialog( + context: context, + builder: (BuildContext context) => + const ForgotPasswordDialogWidget(), + ), + ), + const SizedBox(height: 30), + LoginButtonWidget( + isLoading: loginViewModel.isLoading, + onPressed: () => loginViewModel.signIn(context), + ), + const SizedBox(height: 20), + ErrorMessageWidget(errorMessage: loginViewModel.errorMessage), + ], + ), ); }, ); diff --git a/em2rp/lib/views/widgets/auth/mail_textfield.dart b/em2rp/lib/views/widgets/auth/mail_textfield.dart index 35386e5..a405d38 100644 --- a/em2rp/lib/views/widgets/auth/mail_textfield.dart +++ b/em2rp/lib/views/widgets/auth/mail_textfield.dart @@ -4,11 +4,13 @@ import 'package:flutter/material.dart'; class EmailTextFieldWidget extends StatelessWidget { final TextEditingController emailController; final bool highlightEmailField; + final VoidCallback? onSubmitted; const EmailTextFieldWidget({ super.key, required this.emailController, required this.highlightEmailField, + this.onSubmitted, }); @override @@ -16,6 +18,9 @@ class EmailTextFieldWidget extends StatelessWidget { return TextField( controller: emailController, keyboardType: TextInputType.emailAddress, + autofillHints: const [AutofillHints.email, AutofillHints.username], + textInputAction: TextInputAction.next, + onSubmitted: (_) => onSubmitted?.call(), decoration: InputDecoration( labelText: 'Email', border: OutlineInputBorder( diff --git a/em2rp/lib/views/widgets/auth/password_textfield.dart b/em2rp/lib/views/widgets/auth/password_textfield.dart index 9691b1c..0b0bf6a 100644 --- a/em2rp/lib/views/widgets/auth/password_textfield.dart +++ b/em2rp/lib/views/widgets/auth/password_textfield.dart @@ -7,6 +7,7 @@ class PasswordTextFieldWidget extends StatelessWidget { final bool obscurePassword; final bool highlightPasswordField; final VoidCallback onTogglePasswordVisibility; + final VoidCallback? onSubmitted; const PasswordTextFieldWidget({ super.key, @@ -14,6 +15,7 @@ class PasswordTextFieldWidget extends StatelessWidget { required this.obscurePassword, required this.highlightPasswordField, required this.onTogglePasswordVisibility, + this.onSubmitted, }); @override @@ -21,6 +23,9 @@ class PasswordTextFieldWidget extends StatelessWidget { return TextField( controller: passwordController, obscureText: obscurePassword, + autofillHints: const [AutofillHints.password], + textInputAction: TextInputAction.done, + onSubmitted: (_) => onSubmitted?.call(), decoration: InputDecoration( labelText: 'Mot de passe', border: OutlineInputBorder( diff --git a/em2rp/pubspec.yaml b/em2rp/pubspec.yaml index 47fb909..b6b2c3a 100644 --- a/em2rp/pubspec.yaml +++ b/em2rp/pubspec.yaml @@ -1,5 +1,5 @@ name: em2rp -description: "A new Flutter project." +description: "L'app de gestion d'événements et matériel par EM2 Events" publish_to: 'none' version: 1.0.0+1 diff --git a/em2rp/web/version.json b/em2rp/web/version.json index 27cefb2..23146b9 100644 --- a/em2rp/web/version.json +++ b/em2rp/web/version.json @@ -1,7 +1,7 @@ { - "version": "1.0.6", + "version": "1.0.9", "updateUrl": "https://app.em2events.fr", "forceUpdate": true, "releaseNotes": "Cette version apporte des outils majeurs pour faciliter la gestion de votre parc et de vos événements :\r\n\r\n* **Scanner QR Code :** Retrouvez instantanément la fiche d'un équipement ou d'un conteneur en scannant son code directement depuis l'application. La génération des codes a également été rendue plus fluide.\r\n* **Centre de Notifications & Alertes :** Ne ratez plus rien ! Un nouveau système d'alertes (dans l'app et par email) vous prévient des maintenances, équipements manquants ou conflits. Vous pouvez configurer vos préférences d'envoi.", - "timestamp": "2026-01-18T12:09:32.899Z" + "timestamp": "2026-02-07T15:35:30.790Z" } \ No newline at end of file