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`.
199 lines
6.1 KiB
Dart
199 lines
6.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:em2rp/models/event_model.dart';
|
|
import 'package:em2rp/services/data_service.dart';
|
|
import 'package:em2rp/services/api_service.dart';
|
|
import 'package:em2rp/utils/performance_monitor.dart';
|
|
|
|
class EventProvider with ChangeNotifier {
|
|
final DataService _dataService = DataService(FirebaseFunctionsApiService());
|
|
List<EventModel> _events = [];
|
|
bool _isLoading = false;
|
|
|
|
List<EventModel> get events => _events;
|
|
bool get isLoading => _isLoading;
|
|
|
|
// Cache des utilisateurs chargés depuis getEvents
|
|
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
|
|
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;
|
|
notifyListeners();
|
|
|
|
try {
|
|
print('Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)');
|
|
|
|
PerformanceMonitor.start('EventProvider.getEvents_API');
|
|
// Charger via l'API - les permissions sont vérifiées côté serveur
|
|
final result = await _dataService.getEvents(userId: userId);
|
|
PerformanceMonitor.end('EventProvider.getEvents_API');
|
|
|
|
final eventsData = result['events'] as List<Map<String, dynamic>>;
|
|
final usersData = result['users'] as Map<String, dynamic>;
|
|
|
|
// Stocker les utilisateurs dans le cache
|
|
_usersCache = usersData.map((key, value) =>
|
|
MapEntry(key, value as Map<String, dynamic>)
|
|
);
|
|
|
|
print('Found ${eventsData.length} events from API');
|
|
|
|
PerformanceMonitor.start('EventProvider.parseEvents');
|
|
List<EventModel> allEvents = [];
|
|
int failedCount = 0;
|
|
|
|
// Parser chaque événement
|
|
for (var eventData in eventsData) {
|
|
try {
|
|
final event = EventModel.fromMap(eventData, eventData['id'] as String);
|
|
allEvents.add(event);
|
|
} catch (e) {
|
|
print('Failed to parse event ${eventData['id']}: $e');
|
|
failedCount++;
|
|
}
|
|
}
|
|
PerformanceMonitor.end('EventProvider.parseEvents');
|
|
|
|
_events = allEvents;
|
|
_lastLoadTime = DateTime.now();
|
|
_lastUserId = userId;
|
|
_lastCanViewAll = canViewAllEvents;
|
|
|
|
print('Successfully loaded ${_events.length} events (${failedCount} failed)');
|
|
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
PerformanceMonitor.end('EventProvider.loadUserEvents');
|
|
} catch (e) {
|
|
print('Error loading events: $e');
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
PerformanceMonitor.end('EventProvider.loadUserEvents');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Recharger les événements (utilise le dernier userId)
|
|
Future<void> refreshEvents(String userId, {bool canViewAllEvents = false}) async {
|
|
await loadUserEvents(userId, canViewAllEvents: canViewAllEvents, forceReload: true);
|
|
}
|
|
|
|
/// Récupérer un événement spécifique par ID
|
|
EventModel? getEventById(String eventId) {
|
|
try {
|
|
return _events.firstWhere((event) => event.id == eventId);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Ajouter un nouvel événement
|
|
Future<void> addEvent(EventModel event) async {
|
|
try {
|
|
// L'événement est créé via l'API dans le service
|
|
await refreshEvents(_lastUserId ?? '', canViewAllEvents: _lastCanViewAll);
|
|
} catch (e) {
|
|
print('Error adding event: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Mettre à jour un événement
|
|
Future<void> updateEvent(EventModel event) async {
|
|
try {
|
|
// Mise à jour locale immédiate
|
|
final index = _events.indexWhere((e) => e.id == event.id);
|
|
if (index != -1) {
|
|
_events[index] = event;
|
|
notifyListeners();
|
|
}
|
|
} catch (e) {
|
|
print('Error updating event: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Supprimer un événement
|
|
Future<void> deleteEvent(String eventId) async {
|
|
try {
|
|
await _dataService.deleteEvent(eventId);
|
|
_events.removeWhere((event) => event.id == eventId);
|
|
notifyListeners();
|
|
} catch (e) {
|
|
print('Error deleting event: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Récupérer les données d'un utilisateur depuis le cache
|
|
Map<String, dynamic>? getUserFromCache(String uid) {
|
|
return _usersCache[uid];
|
|
}
|
|
|
|
/// Récupérer les utilisateurs de la workforce d'un événement
|
|
List<Map<String, dynamic>> getWorkforceUsers(EventModel event) {
|
|
final users = <Map<String, dynamic>>[];
|
|
|
|
for (final dynamic userRef in event.workforce) {
|
|
try {
|
|
String? uid;
|
|
|
|
// Tenter d'extraire l'UID
|
|
if (userRef is String) {
|
|
uid = userRef;
|
|
} else {
|
|
// Essayer d'extraire l'ID si c'est une DocumentReference
|
|
final ref = userRef as DocumentReference?;
|
|
uid = ref?.id;
|
|
}
|
|
|
|
if (uid != null) {
|
|
final userData = getUserFromCache(uid);
|
|
if (userData != null) {
|
|
users.add(userData);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Ignorer les références invalides
|
|
print('Skipping invalid workforce reference: $userRef');
|
|
}
|
|
}
|
|
|
|
return users;
|
|
}
|
|
|
|
/// Vider la liste des événements
|
|
void clearEvents() {
|
|
_events = [];
|
|
_lastLoadTime = null;
|
|
_lastUserId = null;
|
|
_lastCanViewAll = false;
|
|
notifyListeners();
|
|
}
|
|
}
|