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:
ElPoyo
2026-02-09 10:14:52 +01:00
parent a7e5f91a21
commit 8cd4854924
24 changed files with 545 additions and 515 deletions

View File

@@ -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

View File

@@ -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é.

View File

@@ -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,12 +109,11 @@
"order": "ASCENDING" "order": "ASCENDING"
}, },
{ {
"fieldPath": "EndDateTime", "fieldPath": "status",
"order": "ASCENDING" "order": "ASCENDING"
} }
] ]
} }
], ],
"fieldOverrides": [] "fieldOverrides": []
} }

9
em2rp/functions/.env Normal file
View 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"

View File

@@ -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);

View File

@@ -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();

View File

@@ -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;

View File

@@ -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}`,
);
} }
}); });

View File

@@ -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;

View File

@@ -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';

View File

@@ -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,
),
),
],
),
), ),
); );
} }

View File

@@ -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;
}
} }

View File

@@ -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();
} }

View File

@@ -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) {

View File

@@ -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
/// ///

View File

@@ -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();

View 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;
}
}
}

View File

@@ -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,52 +36,75 @@ 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((_) { }
final eventProvider = Provider.of<EventProvider>(context, listen: false);
final events = eventProvider.events; /// Charge les événements de manière asynchrone et sélectionne l'événement approprié
if (events.isNotEmpty) { Future<void> _loadEventsAsync() async {
final now = DateTime.now(); PerformanceMonitor.start('CalendarPage.loadEventsAsync');
// Pour mobile : sélectionner le premier événement du jour ou le prochain événement à venir await _loadEvents();
final todayEvents = events
.where((e) => // Sélectionner l'événement approprié après le chargement
e.startDateTime.year == now.year && if (mounted) {
e.startDateTime.month == now.month && PerformanceMonitor.start('CalendarPage.selectDefaultEvent');
e.startDateTime.day == now.day) _selectDefaultEvent();
.toList() PerformanceMonitor.end('CalendarPage.selectDefaultEvent');
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); }
EventModel? selected; PerformanceMonitor.end('CalendarPage.loadEventsAsync');
DateTime? selectedDay; }
if (todayEvents.isNotEmpty) {
selected = todayEvents[0]; /// Sélectionne automatiquement l'événement le plus proche de maintenant
selectedDay = DateTime(now.year, now.month, now.day); void _selectDefaultEvent() {
} else { final eventProvider = Provider.of<EventProvider>(context, listen: false);
// Chercher le prochain événement à venir final events = eventProvider.events;
final futureEvents = events
.where((e) => e.startDateTime.isAfter(now)) if (events.isEmpty) return;
.toList()
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); final now = DateTime.now();
if (futureEvents.isNotEmpty) {
selected = futureEvents[0]; // Trouver les événements d'aujourd'hui
selectedDay = DateTime(selected.startDateTime.year, final todayEvents = events.where((e) {
selected.startDateTime.month, selected.startDateTime.day); final start = e.startDateTime;
} else { return start.year == now.year &&
// Aucun événement à venir, prendre le plus proche dans le passé start.month == now.month &&
events.sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); start.day == now.day;
selected = events.last; }).toList()..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
selectedDay = DateTime(selected.startDateTime.year,
selected.startDateTime.month, selected.startDateTime.day); EventModel? selected;
} DateTime? selectedDay;
}
setState(() { if (todayEvents.isNotEmpty) {
_selectedDay = selectedDay; selected = todayEvents[0];
_focusedDay = selectedDay!; selectedDay = DateTime(now.year, now.month, now.day);
_selectedEventIndex = 0; } else {
_selectedEvent = selected; // 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<void> _loadEvents() async { Future<void> _loadEvents() async {

View File

@@ -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,

View File

@@ -58,41 +58,45 @@ 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(
mainAxisAlignment: MainAxisAlignment.center, child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ crossAxisAlignment: CrossAxisAlignment.stretch,
const LogoWidget(), children: <Widget>[
const SizedBox(height: 30), const LogoWidget(),
const WelcomeTextWidget(), const SizedBox(height: 30),
const SizedBox(height: 40), const WelcomeTextWidget(),
EmailTextFieldWidget( const SizedBox(height: 40),
emailController: loginViewModel.emailController, EmailTextFieldWidget(
highlightEmailField: loginViewModel.highlightEmailField, emailController: loginViewModel.emailController,
), highlightEmailField: loginViewModel.highlightEmailField,
const SizedBox(height: 20), onSubmitted: () => loginViewModel.signIn(context),
PasswordTextFieldWidget(
passwordController: loginViewModel.passwordController,
obscurePassword: loginViewModel.obscurePassword,
highlightPasswordField: loginViewModel.highlightPasswordField,
onTogglePasswordVisibility:
loginViewModel.togglePasswordVisibility,
),
ForgotPasswordButtonWidget(
onPressed: () => showDialog(
context: context,
builder: (BuildContext context) =>
const ForgotPasswordDialogWidget(),
), ),
), const SizedBox(height: 20),
const SizedBox(height: 30), PasswordTextFieldWidget(
LoginButtonWidget( passwordController: loginViewModel.passwordController,
isLoading: loginViewModel.isLoading, obscurePassword: loginViewModel.obscurePassword,
onPressed: () => loginViewModel.signIn(context), highlightPasswordField: loginViewModel.highlightPasswordField,
), onTogglePasswordVisibility:
const SizedBox(height: 20), loginViewModel.togglePasswordVisibility,
ErrorMessageWidget(errorMessage: loginViewModel.errorMessage), 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),
],
),
); );
}, },
); );

View File

@@ -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(

View File

@@ -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(

View File

@@ -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

View File

@@ -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"
} }