refactor: Amélioration des performances et migration des Cloud Functions
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`.
This commit is contained in:
@@ -32,16 +32,16 @@ assets/assets/images/tshirt-incrust.webp,1737393735487,af7cb34adfca19c0b41c8eb63
|
|||||||
assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc
|
assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc
|
||||||
assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde
|
assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde
|
||||||
assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d
|
assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d
|
||||||
version.json,1768738172901,f258e76dbf34b4a64999cb6d1d983255ad592c590e53f7c4fe380b2bfef82762
|
version.json,1770478530807,2cbfdf7f34574c2f9d4f1af02acb86d8d230af93790c97a3c7e1674c4db42ef4
|
||||||
index.html,1768738180374,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
index.html,1770478536326,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
||||||
flutter_service_worker.js,1768738281912,ad5fcbc95e3f4e31b6c3ae92df0a872c24434ba7ac7448fdd9359f2e3bf7d76c
|
flutter_service_worker.js,1770478628965,cb72807cfcb05b0a2e7b3f4f0cf618a0284a3d2476c93672bd86ea99670b0f5d
|
||||||
assets/FontManifest.json,1768738277185,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
assets/FontManifest.json,1770478624084,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
||||||
flutter_bootstrap.js,1768738180360,f1963883a54097e939404b503b6a9963408fe0187a18d73adb648f6ef0f81578
|
assets/AssetManifest.json,1770478624084,1e1501af5844823ef215cf650f4cef4002c0389d88770225ac07576d57dc1067
|
||||||
assets/AssetManifest.bin.json,1768738277185,f446eb3de964f3a6f9e76fcc98d79a81b0429e076c9c7bf30cf8edd0263a0b0a
|
flutter_bootstrap.js,1770478536318,bf4a3b4bf79eaed1ce24892f20cfb270bcc22fb392bc9f6a1d17aeed42ed4ed8
|
||||||
assets/AssetManifest.bin,1768738277184,72bbccb69d9a02d3885df0c5e58ebfed29e25a4919e10bf195b59542f4709ca3
|
assets/AssetManifest.bin.json,1770478624084,f446eb3de964f3a6f9e76fcc98d79a81b0429e076c9c7bf30cf8edd0263a0b0a
|
||||||
assets/AssetManifest.json,1768738277185,1e1501af5844823ef215cf650f4cef4002c0389d88770225ac07576d57dc1067
|
assets/AssetManifest.bin,1770478624084,72bbccb69d9a02d3885df0c5e58ebfed29e25a4919e10bf195b59542f4709ca3
|
||||||
assets/shaders/ink_sparkle.frag,1768738277454,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1770478628013,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
||||||
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1768738280959,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
assets/shaders/ink_sparkle.frag,1770478624492,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
||||||
assets/fonts/MaterialIcons-Regular.otf,1768738280969,9e7c35e587de73a0aee5675d5aef4c6830478af0aa31ad0da76b84a503906b03
|
assets/fonts/MaterialIcons-Regular.otf,1770478628013,50e06fd231edee237d875cddbae1e22b682d32bb1284e3c32ca409fa489f9c21
|
||||||
assets/NOTICES,1768738277188,fc20c3c3c998057eb7e58ad2e009c7268bf748bfde685e95130431f4c54bd51c
|
assets/NOTICES,1770478624086,d02d64a466e62fdaeee2534a3f65541362ccf29beb495e2af0fdce41f4ae28d9
|
||||||
main.dart.js,1768738275891,4ef7f90056f38602de6430a68a479a005268f9d83395ad9b444337c214a3710c
|
main.dart.js,1770478620736,03d43aeaa96cfdbe5b7491f9610223ec95c29d47095570dd61cd6cddac863496
|
||||||
|
|||||||
@@ -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
|
|
||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
|
||||||
<meta http-equiv="Pragma" content="no-cache">
|
|
||||||
<meta http-equiv="Expires" content="0">
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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é.
|
|
||||||
|
|
||||||
@@ -1,23 +1,97 @@
|
|||||||
{
|
{
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
"collectionGroup": "events",
|
"collectionGroup": "alerts",
|
||||||
"queryScope": "COLLECTION",
|
"queryScope": "COLLECTION",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "EndDateTime",
|
"fieldPath": "assignedTo",
|
||||||
|
"arrayConfig": "CONTAINS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isRead",
|
||||||
"order": "ASCENDING"
|
"order": "ASCENDING"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "StartDateTime",
|
"fieldPath": "createdAt",
|
||||||
"order": "ASCENDING"
|
"order": "DESCENDING"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collectionGroup": "alerts",
|
||||||
|
"queryScope": "COLLECTION",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "assignedTo",
|
||||||
|
"arrayConfig": "CONTAINS"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "status",
|
"fieldPath": "status",
|
||||||
"order": "ASCENDING"
|
"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"
|
"order": "ASCENDING"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -27,7 +101,7 @@
|
|||||||
"queryScope": "COLLECTION",
|
"queryScope": "COLLECTION",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "status",
|
"fieldPath": "EndDateTime",
|
||||||
"order": "ASCENDING"
|
"order": "ASCENDING"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -35,7 +109,7 @@
|
|||||||
"order": "ASCENDING"
|
"order": "ASCENDING"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "EndDateTime",
|
"fieldPath": "status",
|
||||||
"order": "ASCENDING"
|
"order": "ASCENDING"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -43,4 +117,3 @@
|
|||||||
],
|
],
|
||||||
"fieldOverrides": []
|
"fieldOverrides": []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
9
em2rp/functions/.env
Normal file
9
em2rp/functions/.env
Normal file
@@ -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"
|
||||||
|
|
||||||
@@ -46,7 +46,11 @@ const withCors = (handler) => {
|
|||||||
* Crée une alerte et envoie les notifications
|
* Crée une alerte et envoie les notifications
|
||||||
* Gère tout le processus côté backend de A à Z
|
* 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 {
|
try {
|
||||||
// Vérifier l'authentification
|
// Vérifier l'authentification
|
||||||
const decodedToken = await auth.authenticateUser(req);
|
const decodedToken = await auth.authenticateUser(req);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const db = admin.firestore();
|
|||||||
const httpOptions = {
|
const httpOptions = {
|
||||||
cors: false,
|
cors: false,
|
||||||
invoker: 'public', // Permet les invocations non authentifiées (l'auth est gérée par notre token Firebase)
|
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
|
// 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
|
* Récupère un utilisateur spécifique par son ID
|
||||||
* Tout utilisateur authentifié peut accéder aux données publiques
|
* Tout utilisateur authentifié peut accéder aux données publiques
|
||||||
*/
|
*/
|
||||||
exports.getUser = onCall(async (request) => {
|
exports.getUser = onRequest(httpOptions, withCors(async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await authenticateUser(request);
|
const decodedToken = await auth.authenticateUser(req);
|
||||||
const db = getFirestore();
|
|
||||||
|
|
||||||
const { userId } = request.data;
|
const { userId } = req.body.data || req.body || {};
|
||||||
if (!userId) {
|
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) {
|
if (!userDoc.exists) {
|
||||||
throw new Error("User not found");
|
res.status(404).json({ error: 'User not found' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = userDoc.data();
|
const user = userDoc.data();
|
||||||
@@ -2070,11 +2072,11 @@ exports.getUser = onCall(async (request) => {
|
|||||||
const userData = {
|
const userData = {
|
||||||
id: userDoc.id,
|
id: userDoc.id,
|
||||||
uid: user.uid || userDoc.id,
|
uid: user.uid || userDoc.id,
|
||||||
email: user.email || "",
|
email: user.email || '',
|
||||||
firstName: user.firstName || "",
|
firstName: user.firstName || '',
|
||||||
lastName: user.lastName || "",
|
lastName: user.lastName || '',
|
||||||
phoneNumber: user.phoneNumber || "",
|
phoneNumber: user.phoneNumber || '',
|
||||||
profilePhotoUrl: user.profilePhotoUrl || "",
|
profilePhotoUrl: user.profilePhotoUrl || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Inclure le rôle si disponible
|
// 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) {
|
} catch (error) {
|
||||||
logger.error("Error fetching user:", error);
|
logger.error('Error fetching user:', error);
|
||||||
throw new Error(error.message || "Failed to fetch user");
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -3488,6 +3490,7 @@ const {sendDailyDigest} = require('./sendDailyDigest');
|
|||||||
exports.sendDailyDigest = onSchedule({
|
exports.sendDailyDigest = onSchedule({
|
||||||
schedule: '0 8 * * *',
|
schedule: '0 8 * * *',
|
||||||
timeZone: 'Europe/Paris',
|
timeZone: 'Europe/Paris',
|
||||||
|
region: 'europe-west9',
|
||||||
retryCount: 2,
|
retryCount: 2,
|
||||||
memory: '512MiB'
|
memory: '512MiB'
|
||||||
}, async (context) => {
|
}, async (context) => {
|
||||||
@@ -3507,7 +3510,10 @@ exports.sendDailyDigest = onSchedule({
|
|||||||
* Trigger : Nouvel événement créé
|
* Trigger : Nouvel événement créé
|
||||||
* Envoie une notification à tous les membres de la workforce
|
* 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}`);
|
logger.info(`[onEventCreated] Événement créé: ${event.params.eventId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -3547,7 +3553,10 @@ exports.onEventCreated = onDocumentCreated('events/{eventId}', async (event) =>
|
|||||||
* Trigger : Événement modifié (workforce changée)
|
* Trigger : Événement modifié (workforce changée)
|
||||||
* Envoie une notification aux nouveaux membres ajoutés à la workforce
|
* 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 before = event.data.before.data();
|
||||||
const after = event.data.after.data();
|
const after = event.data.after.data();
|
||||||
const eventId = event.params.eventId;
|
const eventId = event.params.eventId;
|
||||||
@@ -3598,7 +3607,10 @@ exports.onEventUpdated = onDocumentUpdated('events/{eventId}', async (event) =>
|
|||||||
* Trigger : Nouvelle alerte créée
|
* Trigger : Nouvelle alerte créée
|
||||||
* Envoie un email immédiat si l'alerte est critique
|
* 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 alertId = event.params.alertId;
|
||||||
const alertData = event.data.data();
|
const alertData = event.data.data();
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ const {getSmtpConfig, EMAIL_CONFIG} = require('./utils/emailConfig');
|
|||||||
* Appelée par le client lors du chargement/déchargement
|
* Appelée par le client lors du chargement/déchargement
|
||||||
* Crée automatiquement les alertes nécessaires
|
* 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 {
|
try {
|
||||||
// L'authentification est automatique avec onCall
|
// L'authentification est automatique avec onCall
|
||||||
const {auth, data} = request;
|
const {auth, data} = request;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const functions = require('firebase-functions');
|
const {onCall} = require('firebase-functions/v2/https');
|
||||||
const admin = require('firebase-admin');
|
const admin = require('firebase-admin');
|
||||||
const nodemailer = require('nodemailer');
|
const nodemailer = require('nodemailer');
|
||||||
const handlebars = require('handlebars');
|
const handlebars = require('handlebars');
|
||||||
@@ -10,22 +10,19 @@ const {getSmtpConfig, EMAIL_CONFIG} = require('./utils/emailConfig');
|
|||||||
* Envoie un email d'alerte à un utilisateur
|
* Envoie un email d'alerte à un utilisateur
|
||||||
* Appelé par le client Dart via callable function
|
* 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
|
// Vérifier l'authentification
|
||||||
if (!context.auth) {
|
if (!request.auth) {
|
||||||
throw new functions.https.HttpsError(
|
throw new Error('L\'utilisateur doit être authentifié');
|
||||||
'unauthenticated',
|
|
||||||
'L\'utilisateur doit être authentifié',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {alertId, userId, templateType} = data;
|
const {alertId, userId, templateType} = request.data;
|
||||||
|
|
||||||
if (!alertId || !userId) {
|
if (!alertId || !userId) {
|
||||||
throw new functions.https.HttpsError(
|
throw new Error('alertId et userId sont requis');
|
||||||
'invalid-argument',
|
|
||||||
'alertId et userId sont requis',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -36,10 +33,7 @@ exports.sendAlertEmail = functions.https.onCall(async (data, context) => {
|
|||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (!alertDoc.exists) {
|
if (!alertDoc.exists) {
|
||||||
throw new functions.https.HttpsError(
|
throw new Error('Alerte introuvable');
|
||||||
'not-found',
|
|
||||||
'Alerte introuvable',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const alert = alertDoc.data();
|
const alert = alertDoc.data();
|
||||||
@@ -51,10 +45,7 @@ exports.sendAlertEmail = functions.https.onCall(async (data, context) => {
|
|||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (!userDoc.exists) {
|
if (!userDoc.exists) {
|
||||||
throw new functions.https.HttpsError(
|
throw new Error('Utilisateur introuvable');
|
||||||
'not-found',
|
|
||||||
'Utilisateur introuvable',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = userDoc.data();
|
const user = userDoc.data();
|
||||||
@@ -112,10 +103,7 @@ exports.sendAlertEmail = functions.https.onCall(async (data, context) => {
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur envoi email:', error);
|
console.error('Erreur envoi email:', error);
|
||||||
throw new functions.https.HttpsError(
|
throw new Error(`Erreur lors de l'envoi de l'email: ${error.message}`);
|
||||||
'internal',
|
|
||||||
`Erreur lors de l'envoi de l'email: ${error.message}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ class ApiConfig {
|
|||||||
static const bool isDevelopment = false; // false = utilise Cloud Functions prod
|
static const bool isDevelopment = false; // false = utilise Cloud Functions prod
|
||||||
|
|
||||||
// URL de base pour les Cloud Functions
|
// URL de base pour les Cloud Functions
|
||||||
static const String productionUrl = 'https://us-central1-em2rp-951dc.cloudfunctions.net';
|
static const String productionUrl = 'https://europe-west9-em2rp-951dc.cloudfunctions.net';
|
||||||
static const String developmentUrl = 'http://localhost:5001/em2rp-951dc/us-central1';
|
static const String developmentUrl = 'http://localhost:5001/em2rp-951dc/europe-west9';
|
||||||
|
|
||||||
/// Retourne l'URL de base selon l'environnement
|
/// Retourne l'URL de base selon l'environnement
|
||||||
static String get baseUrl => isDevelopment ? developmentUrl : productionUrl;
|
static String get baseUrl => isDevelopment ? developmentUrl : productionUrl;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/// Configuration de la version de l'application
|
/// Configuration de la version de l'application
|
||||||
class AppVersion {
|
class AppVersion {
|
||||||
static const String version = '1.0.6';
|
static const String version = '1.0.9';
|
||||||
|
|
||||||
/// Retourne la version complète de l'application
|
/// Retourne la version complète de l'application
|
||||||
static String get fullVersion => 'v$version';
|
static String get fullVersion => 'v$version';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:em2rp/providers/container_provider.dart';
|
|||||||
import 'package:em2rp/providers/maintenance_provider.dart';
|
import 'package:em2rp/providers/maintenance_provider.dart';
|
||||||
import 'package:em2rp/providers/alert_provider.dart';
|
import 'package:em2rp/providers/alert_provider.dart';
|
||||||
import 'package:em2rp/utils/auth_guard_widget.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/alerts_page.dart';
|
||||||
import 'package:em2rp/views/calendar_page.dart';
|
import 'package:em2rp/views/calendar_page.dart';
|
||||||
import 'package:em2rp/views/login_page.dart';
|
import 'package:em2rp/views/login_page.dart';
|
||||||
@@ -203,22 +204,22 @@ class _AutoLoginWrapperState extends State<AutoLoginWrapper> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _autoLogin() async {
|
Future<void> _autoLogin() async {
|
||||||
|
PerformanceMonitor.start('App.autoLogin');
|
||||||
try {
|
try {
|
||||||
final localAuthProvider =
|
final localAuthProvider =
|
||||||
Provider.of<LocalUserProvider>(context, listen: false);
|
Provider.of<LocalUserProvider>(context, listen: false);
|
||||||
|
|
||||||
// Vérifier si l'utilisateur est déjà connecté
|
// Vérifier si l'utilisateur est déjà connecté
|
||||||
if (FirebaseAuth.instance.currentUser == null && Env.isDevelopment) {
|
if (FirebaseAuth.instance.currentUser == null && Env.isDevelopment) {
|
||||||
|
PerformanceMonitor.start('App.signIn');
|
||||||
// Connexion automatique en mode développement
|
// Connexion automatique en mode développement
|
||||||
await localAuthProvider.signInWithEmailAndPassword(
|
await localAuthProvider.signInWithEmailAndPassword(
|
||||||
Env.devAdminEmail,
|
Env.devAdminEmail,
|
||||||
Env.devAdminPassword,
|
Env.devAdminPassword,
|
||||||
);
|
);
|
||||||
|
PerformanceMonitor.end('App.signIn');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Charger les données utilisateur
|
|
||||||
await localAuthProvider.loadUserData();
|
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
// MODIFIÉ : Vérifier si une route spécifique est demandée dans l'URL
|
// MODIFIÉ : Vérifier si une route spécifique est demandée dans l'URL
|
||||||
// En Flutter Web, on peut vérifier window.location.hash
|
// En Flutter Web, on peut vérifier window.location.hash
|
||||||
@@ -227,7 +228,7 @@ class _AutoLoginWrapperState extends State<AutoLoginWrapper> {
|
|||||||
|
|
||||||
print('[AutoLoginWrapper] Fragment URL: $fragment');
|
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') {
|
if (fragment.isNotEmpty && fragment != '/' && fragment != '/calendar') {
|
||||||
print('[AutoLoginWrapper] Redirection vers: $fragment');
|
print('[AutoLoginWrapper] Redirection vers: $fragment');
|
||||||
Navigator.of(context).pushReplacementNamed(fragment);
|
Navigator.of(context).pushReplacementNamed(fragment);
|
||||||
@@ -236,9 +237,18 @@ class _AutoLoginWrapperState extends State<AutoLoginWrapper> {
|
|||||||
print('[AutoLoginWrapper] Redirection vers: /calendar (défaut)');
|
print('[AutoLoginWrapper] Redirection vers: /calendar (défaut)');
|
||||||
Navigator.of(context).pushReplacementNamed('/calendar');
|
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) {
|
} catch (e) {
|
||||||
print('Auto login failed: $e');
|
print('Auto login failed: $e');
|
||||||
|
PerformanceMonitor.end('App.autoLogin');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).pushReplacementNamed('/login');
|
Navigator.of(context).pushReplacementNamed('/login');
|
||||||
}
|
}
|
||||||
@@ -247,9 +257,41 @@ class _AutoLoginWrapperState extends State<AutoLoginWrapper> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
body: Center(
|
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<Color>(AppColors.rouge),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text(
|
||||||
|
'Chargement...',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:cloud_firestore/cloud_firestore.dart';
|
|||||||
import 'package:em2rp/models/event_model.dart';
|
import 'package:em2rp/models/event_model.dart';
|
||||||
import 'package:em2rp/services/data_service.dart';
|
import 'package:em2rp/services/data_service.dart';
|
||||||
import 'package:em2rp/services/api_service.dart';
|
import 'package:em2rp/services/api_service.dart';
|
||||||
|
import 'package:em2rp/utils/performance_monitor.dart';
|
||||||
|
|
||||||
class EventProvider with ChangeNotifier {
|
class EventProvider with ChangeNotifier {
|
||||||
final DataService _dataService = DataService(FirebaseFunctionsApiService());
|
final DataService _dataService = DataService(FirebaseFunctionsApiService());
|
||||||
@@ -15,19 +16,43 @@ class EventProvider with ChangeNotifier {
|
|||||||
// Cache des utilisateurs chargés depuis getEvents
|
// Cache des utilisateurs chargés depuis getEvents
|
||||||
Map<String, Map<String, dynamic>> _usersCache = {};
|
Map<String, Map<String, dynamic>> _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
|
/// Charger les événements d'un utilisateur via l'API
|
||||||
Future<void> loadUserEvents(String userId, {bool canViewAllEvents = false}) async {
|
Future<void> 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;
|
_isLoading = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
// Sauvegarder les paramètres
|
|
||||||
_saveLastLoadParams(userId, canViewAllEvents);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
print('Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)');
|
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
|
// Charger via l'API - les permissions sont vérifiées côté serveur
|
||||||
final result = await _dataService.getEvents(userId: userId);
|
final result = await _dataService.getEvents(userId: userId);
|
||||||
|
PerformanceMonitor.end('EventProvider.getEvents_API');
|
||||||
|
|
||||||
final eventsData = result['events'] as List<Map<String, dynamic>>;
|
final eventsData = result['events'] as List<Map<String, dynamic>>;
|
||||||
final usersData = result['users'] as Map<String, dynamic>;
|
final usersData = result['users'] as Map<String, dynamic>;
|
||||||
|
|
||||||
@@ -38,6 +63,7 @@ class EventProvider with ChangeNotifier {
|
|||||||
|
|
||||||
print('Found ${eventsData.length} events from API');
|
print('Found ${eventsData.length} events from API');
|
||||||
|
|
||||||
|
PerformanceMonitor.start('EventProvider.parseEvents');
|
||||||
List<EventModel> allEvents = [];
|
List<EventModel> allEvents = [];
|
||||||
int failedCount = 0;
|
int failedCount = 0;
|
||||||
|
|
||||||
@@ -51,23 +77,30 @@ class EventProvider with ChangeNotifier {
|
|||||||
failedCount++;
|
failedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PerformanceMonitor.end('EventProvider.parseEvents');
|
||||||
|
|
||||||
_events = allEvents;
|
_events = allEvents;
|
||||||
|
_lastLoadTime = DateTime.now();
|
||||||
|
_lastUserId = userId;
|
||||||
|
_lastCanViewAll = canViewAllEvents;
|
||||||
|
|
||||||
print('Successfully loaded ${_events.length} events (${failedCount} failed)');
|
print('Successfully loaded ${_events.length} events (${failedCount} failed)');
|
||||||
|
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
PerformanceMonitor.end('EventProvider.loadUserEvents');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error loading events: $e');
|
print('Error loading events: $e');
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
PerformanceMonitor.end('EventProvider.loadUserEvents');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recharger les événements (utilise le dernier userId)
|
/// Recharger les événements (utilise le dernier userId)
|
||||||
Future<void> refreshEvents(String userId, {bool canViewAllEvents = false}) async {
|
Future<void> 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
|
/// Récupérer un événement spécifique par ID
|
||||||
@@ -157,16 +190,9 @@ class EventProvider with ChangeNotifier {
|
|||||||
/// Vider la liste des événements
|
/// Vider la liste des événements
|
||||||
void clearEvents() {
|
void clearEvents() {
|
||||||
_events = [];
|
_events = [];
|
||||||
|
_lastLoadTime = null;
|
||||||
|
_lastUserId = null;
|
||||||
|
_lastCanViewAll = false;
|
||||||
notifyListeners();
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../models/notification_preferences_model.dart';
|
|||||||
import '../utils/firebase_storage_manager.dart';
|
import '../utils/firebase_storage_manager.dart';
|
||||||
import '../services/api_service.dart';
|
import '../services/api_service.dart';
|
||||||
import '../services/data_service.dart';
|
import '../services/data_service.dart';
|
||||||
|
import '../utils/performance_monitor.dart';
|
||||||
|
|
||||||
class LocalUserProvider with ChangeNotifier {
|
class LocalUserProvider with ChangeNotifier {
|
||||||
UserModel? _currentUser;
|
UserModel? _currentUser;
|
||||||
@@ -15,6 +16,9 @@ class LocalUserProvider with ChangeNotifier {
|
|||||||
final FirebaseStorageManager _storageManager = FirebaseStorageManager();
|
final FirebaseStorageManager _storageManager = FirebaseStorageManager();
|
||||||
final DataService _dataService = DataService(apiService);
|
final DataService _dataService = DataService(apiService);
|
||||||
|
|
||||||
|
bool _isLoadingUserData = false;
|
||||||
|
DateTime? _lastUserDataLoad;
|
||||||
|
|
||||||
UserModel? get currentUser => _currentUser;
|
UserModel? get currentUser => _currentUser;
|
||||||
String? get uid => _currentUser?.uid;
|
String? get uid => _currentUser?.uid;
|
||||||
String? get firstName => _currentUser?.firstName;
|
String? get firstName => _currentUser?.firstName;
|
||||||
@@ -25,18 +29,46 @@ class LocalUserProvider with ChangeNotifier {
|
|||||||
String? get phoneNumber => _currentUser?.phoneNumber;
|
String? get phoneNumber => _currentUser?.phoneNumber;
|
||||||
RoleModel? get currentRole => _currentRole;
|
RoleModel? get currentRole => _currentRole;
|
||||||
List<String> get permissions => _currentRole?.permissions ?? [];
|
List<String> 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
|
/// Charge les données de l'utilisateur actuel via Cloud Function
|
||||||
Future<void> loadUserData() async {
|
Future<void> loadUserData({bool forceReload = false}) async {
|
||||||
if (_auth.currentUser == null) {
|
if (_auth.currentUser == null) {
|
||||||
print('No current user in Auth');
|
print('No current user in Auth');
|
||||||
return;
|
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}');
|
print('Loading user data for: ${_auth.currentUser!.uid}');
|
||||||
try {
|
try {
|
||||||
// Utiliser la Cloud Function getCurrentUser
|
// Utiliser la Cloud Function getCurrentUser
|
||||||
|
PerformanceMonitor.start('LocalUserProvider.getCurrentUser_API');
|
||||||
final result = await apiService.call('getCurrentUser', {});
|
final result = await apiService.call('getCurrentUser', {});
|
||||||
|
PerformanceMonitor.end('LocalUserProvider.getCurrentUser_API');
|
||||||
|
|
||||||
final userData = result['user'] as Map<String, dynamic>;
|
final userData = result['user'] as Map<String, dynamic>;
|
||||||
|
|
||||||
print('User data loaded from API: ${userData['uid']}');
|
print('User data loaded from API: ${userData['uid']}');
|
||||||
@@ -59,9 +91,14 @@ class LocalUserProvider with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
|
|
||||||
print('User data loaded successfully');
|
print('User data loaded successfully');
|
||||||
|
_lastUserDataLoad = DateTime.now();
|
||||||
|
_isLoadingUserData = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
PerformanceMonitor.end('LocalUserProvider.loadUserData');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error loading user data: $e');
|
print('Error loading user data: $e');
|
||||||
|
_isLoadingUserData = false;
|
||||||
|
PerformanceMonitor.end('LocalUserProvider.loadUserData');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,6 +113,8 @@ class LocalUserProvider with ChangeNotifier {
|
|||||||
void clearUser() {
|
void clearUser() {
|
||||||
_currentUser = null;
|
_currentUser = null;
|
||||||
_currentRole = null;
|
_currentRole = null;
|
||||||
|
_lastUserDataLoad = null;
|
||||||
|
_isLoadingUserData = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -196,7 +196,11 @@ class DataService {
|
|||||||
/// Crée une option
|
/// Crée une option
|
||||||
Future<String> createOption(String code, Map<String, dynamic> data) async {
|
Future<String> createOption(String code, Map<String, dynamic> data) async {
|
||||||
try {
|
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);
|
final result = await _apiService.call('createOption', requestData);
|
||||||
return result['id'] as String? ?? code;
|
return result['id'] as String? ?? code;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:firebase_auth/firebase_auth.dart';
|
|||||||
|
|
||||||
/// Service d'envoi d'emails via Cloud Functions
|
/// Service d'envoi d'emails via Cloud Functions
|
||||||
class EmailService {
|
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
|
/// Envoie un email d'alerte à un utilisateur
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class EventFormService {
|
|||||||
required String sourcePath,
|
required String sourcePath,
|
||||||
required String destinationPath,
|
required String destinationPath,
|
||||||
}) async {
|
}) 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 user = FirebaseAuth.instance.currentUser;
|
||||||
final idToken = await user?.getIdToken();
|
final idToken = await user?.getIdToken();
|
||||||
|
|
||||||
|
|||||||
129
em2rp/lib/utils/performance_monitor.dart
Normal file
129
em2rp/lib/utils/performance_monitor.dart
Normal file
@@ -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<String, DateTime> _timings = {};
|
||||||
|
static final Map<String, Duration> _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<String, Duration> 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>(
|
||||||
|
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<T> measure<T>(String key, Future<T> 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<T>(String key, T Function() operation) {
|
||||||
|
start(key);
|
||||||
|
try {
|
||||||
|
final result = operation();
|
||||||
|
end(key);
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
end(key);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:em2rp/providers/local_user_provider.dart';
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
import 'package:em2rp/providers/event_provider.dart';
|
import 'package:em2rp/providers/event_provider.dart';
|
||||||
|
import 'package:em2rp/utils/performance_monitor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
||||||
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
||||||
@@ -35,23 +36,44 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
initializeDateFormatting('fr_FR', null);
|
initializeDateFormatting('fr_FR', null);
|
||||||
Future.microtask(() => _loadEvents());
|
// Charger les événements de manière asynchrone sans bloquer l'UI
|
||||||
// Sélection automatique de l'événement le plus proche de maintenant
|
_loadEventsAsync();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
}
|
||||||
|
|
||||||
|
/// Charge les événements de manière asynchrone et sélectionne l'événement approprié
|
||||||
|
Future<void> _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<EventProvider>(context, listen: false);
|
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||||
final events = eventProvider.events;
|
final events = eventProvider.events;
|
||||||
if (events.isNotEmpty) {
|
|
||||||
|
if (events.isEmpty) return;
|
||||||
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
// Pour mobile : sélectionner le premier événement du jour ou le prochain événement à venir
|
|
||||||
final todayEvents = events
|
// Trouver les événements d'aujourd'hui
|
||||||
.where((e) =>
|
final todayEvents = events.where((e) {
|
||||||
e.startDateTime.year == now.year &&
|
final start = e.startDateTime;
|
||||||
e.startDateTime.month == now.month &&
|
return start.year == now.year &&
|
||||||
e.startDateTime.day == now.day)
|
start.month == now.month &&
|
||||||
.toList()
|
start.day == now.day;
|
||||||
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
}).toList()..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||||
|
|
||||||
EventModel? selected;
|
EventModel? selected;
|
||||||
DateTime? selectedDay;
|
DateTime? selectedDay;
|
||||||
|
|
||||||
if (todayEvents.isNotEmpty) {
|
if (todayEvents.isNotEmpty) {
|
||||||
selected = todayEvents[0];
|
selected = todayEvents[0];
|
||||||
selectedDay = DateTime(now.year, now.month, now.day);
|
selectedDay = DateTime(now.year, now.month, now.day);
|
||||||
@@ -59,20 +81,23 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
// Chercher le prochain événement à venir
|
// Chercher le prochain événement à venir
|
||||||
final futureEvents = events
|
final futureEvents = events
|
||||||
.where((e) => e.startDateTime.isAfter(now))
|
.where((e) => e.startDateTime.isAfter(now))
|
||||||
.toList()
|
.toList()..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||||
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
|
||||||
if (futureEvents.isNotEmpty) {
|
if (futureEvents.isNotEmpty) {
|
||||||
selected = futureEvents[0];
|
selected = futureEvents[0];
|
||||||
selectedDay = DateTime(selected.startDateTime.year,
|
final start = selected.startDateTime;
|
||||||
selected.startDateTime.month, selected.startDateTime.day);
|
selectedDay = DateTime(start.year, start.month, start.day);
|
||||||
} else {
|
} else {
|
||||||
// Aucun événement à venir, prendre le plus proche dans le passé
|
// Aucun événement à venir, prendre le plus récent
|
||||||
events.sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
final sortedEvents = events.toList()
|
||||||
selected = events.last;
|
..sort((a, b) => b.startDateTime.compareTo(a.startDateTime));
|
||||||
selectedDay = DateTime(selected.startDateTime.year,
|
selected = sortedEvents.first;
|
||||||
selected.startDateTime.month, selected.startDateTime.day);
|
final start = selected.startDateTime;
|
||||||
|
selectedDay = DateTime(start.year, start.month, start.day);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedDay = selectedDay;
|
_selectedDay = selectedDay;
|
||||||
_focusedDay = selectedDay!;
|
_focusedDay = selectedDay!;
|
||||||
@@ -80,7 +105,6 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
_selectedEvent = selected;
|
_selectedEvent = selected;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadEvents() async {
|
Future<void> _loadEvents() async {
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
};
|
};
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
final result = await FirebaseFunctions.instanceFor(region: 'us-central1')
|
final result = await FirebaseFunctions.instanceFor(region: 'europe-west9')
|
||||||
.httpsCallable('processEquipmentValidation')
|
.httpsCallable('processEquipmentValidation')
|
||||||
.call({
|
.call({
|
||||||
'eventId': _currentEvent.id,
|
'eventId': _currentEvent.id,
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ class LoginPage extends StatelessWidget {
|
|||||||
Widget _buildLoginForm(BuildContext context) {
|
Widget _buildLoginForm(BuildContext context) {
|
||||||
return Consumer<LoginViewModel>(
|
return Consumer<LoginViewModel>(
|
||||||
builder: (context, loginViewModel, child) {
|
builder: (context, loginViewModel, child) {
|
||||||
return Column(
|
return AutofillGroup(
|
||||||
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@@ -69,6 +70,7 @@ class LoginPage extends StatelessWidget {
|
|||||||
EmailTextFieldWidget(
|
EmailTextFieldWidget(
|
||||||
emailController: loginViewModel.emailController,
|
emailController: loginViewModel.emailController,
|
||||||
highlightEmailField: loginViewModel.highlightEmailField,
|
highlightEmailField: loginViewModel.highlightEmailField,
|
||||||
|
onSubmitted: () => loginViewModel.signIn(context),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
PasswordTextFieldWidget(
|
PasswordTextFieldWidget(
|
||||||
@@ -77,6 +79,7 @@ class LoginPage extends StatelessWidget {
|
|||||||
highlightPasswordField: loginViewModel.highlightPasswordField,
|
highlightPasswordField: loginViewModel.highlightPasswordField,
|
||||||
onTogglePasswordVisibility:
|
onTogglePasswordVisibility:
|
||||||
loginViewModel.togglePasswordVisibility,
|
loginViewModel.togglePasswordVisibility,
|
||||||
|
onSubmitted: () => loginViewModel.signIn(context),
|
||||||
),
|
),
|
||||||
ForgotPasswordButtonWidget(
|
ForgotPasswordButtonWidget(
|
||||||
onPressed: () => showDialog(
|
onPressed: () => showDialog(
|
||||||
@@ -93,6 +96,7 @@ class LoginPage extends StatelessWidget {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
ErrorMessageWidget(errorMessage: loginViewModel.errorMessage),
|
ErrorMessageWidget(errorMessage: loginViewModel.errorMessage),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import 'package:flutter/material.dart';
|
|||||||
class EmailTextFieldWidget extends StatelessWidget {
|
class EmailTextFieldWidget extends StatelessWidget {
|
||||||
final TextEditingController emailController;
|
final TextEditingController emailController;
|
||||||
final bool highlightEmailField;
|
final bool highlightEmailField;
|
||||||
|
final VoidCallback? onSubmitted;
|
||||||
|
|
||||||
const EmailTextFieldWidget({
|
const EmailTextFieldWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.emailController,
|
required this.emailController,
|
||||||
required this.highlightEmailField,
|
required this.highlightEmailField,
|
||||||
|
this.onSubmitted,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -16,6 +18,9 @@ class EmailTextFieldWidget extends StatelessWidget {
|
|||||||
return TextField(
|
return TextField(
|
||||||
controller: emailController,
|
controller: emailController,
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
autofillHints: const [AutofillHints.email, AutofillHints.username],
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
onSubmitted: (_) => onSubmitted?.call(),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Email',
|
labelText: 'Email',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class PasswordTextFieldWidget extends StatelessWidget {
|
|||||||
final bool obscurePassword;
|
final bool obscurePassword;
|
||||||
final bool highlightPasswordField;
|
final bool highlightPasswordField;
|
||||||
final VoidCallback onTogglePasswordVisibility;
|
final VoidCallback onTogglePasswordVisibility;
|
||||||
|
final VoidCallback? onSubmitted;
|
||||||
|
|
||||||
const PasswordTextFieldWidget({
|
const PasswordTextFieldWidget({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -14,6 +15,7 @@ class PasswordTextFieldWidget extends StatelessWidget {
|
|||||||
required this.obscurePassword,
|
required this.obscurePassword,
|
||||||
required this.highlightPasswordField,
|
required this.highlightPasswordField,
|
||||||
required this.onTogglePasswordVisibility,
|
required this.onTogglePasswordVisibility,
|
||||||
|
this.onSubmitted,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -21,6 +23,9 @@ class PasswordTextFieldWidget extends StatelessWidget {
|
|||||||
return TextField(
|
return TextField(
|
||||||
controller: passwordController,
|
controller: passwordController,
|
||||||
obscureText: obscurePassword,
|
obscureText: obscurePassword,
|
||||||
|
autofillHints: const [AutofillHints.password],
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
onSubmitted: (_) => onSubmitted?.call(),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Mot de passe',
|
labelText: 'Mot de passe',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: em2rp
|
name: em2rp
|
||||||
description: "A new Flutter project."
|
description: "L'app de gestion d'événements et matériel par EM2 Events"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.6",
|
"version": "1.0.9",
|
||||||
"updateUrl": "https://app.em2events.fr",
|
"updateUrl": "https://app.em2events.fr",
|
||||||
"forceUpdate": true,
|
"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.",
|
"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"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user