Files
EM2_ERP/em2rp/lib/providers/event_provider.dart
ElPoyo 7cbb48e679 perf: Implémentation du lazy loading pour le calendrier
Cette mise à jour refactorise en profondeur le chargement des événements sur la page Calendrier pour améliorer drastiquement les performances et la réactivité de l'application, en particulier pour les utilisateurs avec un grand nombre d'événements. Le système de chargement initial de tous les événements est remplacé par un mécanisme de lazy loading qui ne récupère que les données du mois affiché.

**Changements majeurs :**

-   **Lazy Loading Côté Client (`EventProvider`) :**
    -   Une nouvelle méthode `loadMonthEvents` a été introduite pour charger uniquement les événements d'un mois spécifique (`year`, `month`).
    -   Un cache par mois (`_eventsByMonth`) a été mis en place pour éviter les rechargements inutiles lors de la navigation entre des mois déjà consultés.
    -   Ajout d'une fonction `preloadAdjacentMonths` qui charge en arrière-plan et silencieusement les mois précédent et suivant, assurant une navigation fluide dans le calendrier.

-   **Nouveau Endpoint Backend (`getEventsByMonth`) :**
    -   Création d'un nouvel endpoint Cloud Function `getEventsByMonth` optimisé pour ne requêter que les événements dans une plage de dates (début et fin du mois).
    -   La fonction récupère les utilisateurs associés de manière optimisée en parallélisant les requêtes Firestore (Promise.all).
    -   La limite du nombre d'IDs par requête 'in' a été augmentée de 10 à 30 pour réduire le nombre d'appels à la base de données.

-   **Intégration au Calendrier (`CalendarPage`) :**
    -   La page charge désormais les événements pour le mois courant au démarrage via `_loadCurrentMonthEvents`.
    -   Lorsqu'un utilisateur change de mois (`onPageChanged`), la page déclenche le chargement des données pour le nouveau mois, avec un préchargement des mois adjacents pour anticiper la navigation.
    -   Le chargement initial de tous les événements (`_loadEventsAsync`) a été déprécié.

-   **Correction de la Séquence de Démarrage (`main.dart`) :**
    -   L'appel à `_autoLogin` est maintenant enveloppé dans `WidgetsBinding.instance.addPostFrameCallback`. Cela garantit que la navigation ne se produit qu'après le premier rendu de l'interface, évitant ainsi des erreurs potentielles de build/navigation concurrentes et fiabilisant le chargement initial des données utilisateur.
2026-02-09 11:20:08 +01:00

321 lines
10 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;
// Cache des utilisateurs chargés depuis getEvents
Map<String, Map<String, dynamic>> _usersCache = {};
// Cache pour éviter les rechargements inutiles (ancien système)
DateTime? _lastLoadTime;
String? _lastUserId;
bool _lastCanViewAll = false;
// Nouveau: Cache par mois pour le lazy loading
Map<String, List<EventModel>> _eventsByMonth = {}; // "2026-02" => [events]
String? _currentMonth; // Mois actuellement affiché
List<EventModel> get events => _events;
bool get isLoading => _isLoading;
/// 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;
}
}
/// Charger les événements d'un mois spécifique (lazy loading optimisé)
Future<void> loadMonthEvents(String userId, int year, int month,
{bool canViewAllEvents = false, bool forceReload = false, bool silent = false}) async {
final monthKey = '$year-${month.toString().padLeft(2, '0')}';
// Vérifier le cache
if (!forceReload && _eventsByMonth.containsKey(monthKey)) {
print('[EventProvider] Using cached events for $monthKey');
if (!silent) {
_currentMonth = monthKey;
_events = _eventsByMonth[monthKey]!;
notifyListeners();
}
return;
}
if (!silent) {
_isLoading = true;
notifyListeners();
}
try {
print('[EventProvider] Loading events for month: $monthKey');
PerformanceMonitor.start('EventProvider.loadMonthEvents_API');
final result = await _dataService.getEventsByMonth(
userId: userId,
year: year,
month: month
);
PerformanceMonitor.end('EventProvider.loadMonthEvents_API');
final eventsData = result['events'] as List<Map<String, dynamic>>;
final usersData = result['users'] as Map<String, dynamic>;
// Mettre à jour le cache utilisateurs (addAll pour cumuler)
_usersCache.addAll(
usersData.map((key, value) => MapEntry(key, value as Map<String, dynamic>))
);
print('[EventProvider] Found ${eventsData.length} events for $monthKey');
PerformanceMonitor.start('EventProvider.parseMonthEvents');
List<EventModel> monthEvents = [];
int failedCount = 0;
// Parser les événements
for (var eventData in eventsData) {
try {
final event = EventModel.fromMap(eventData, eventData['id'] as String);
monthEvents.add(event);
} catch (e) {
print('[EventProvider] Failed to parse event ${eventData['id']}: $e');
failedCount++;
}
}
PerformanceMonitor.end('EventProvider.parseMonthEvents');
// Stocker dans le cache par mois
_eventsByMonth[monthKey] = monthEvents;
// Mettre à jour _events et _currentMonth seulement si ce n'est pas un préchargement silencieux
if (!silent) {
_currentMonth = monthKey;
_events = monthEvents;
}
// Mettre à jour les infos de cache global
_lastLoadTime = DateTime.now();
_lastUserId = userId;
_lastCanViewAll = canViewAllEvents;
print('[EventProvider] Successfully loaded ${monthEvents.length} events for $monthKey (${failedCount} failed)');
if (!silent) {
_isLoading = false;
notifyListeners();
}
} catch (e) {
print('[EventProvider] Error loading month events: $e');
if (!silent) {
_isLoading = false;
notifyListeners();
}
rethrow;
}
}
/// Précharger les mois adjacents en arrière-plan
void preloadAdjacentMonths(String userId, int year, int month,
{bool canViewAllEvents = false}) {
// Mois précédent
final prevMonth = month == 1 ? 12 : month - 1;
final prevYear = month == 1 ? year - 1 : year;
// Mois suivant
final nextMonth = month == 12 ? 1 : month + 1;
final nextYear = month == 12 ? year + 1 : year;
print('[EventProvider] Preloading adjacent months...');
// Charger en arrière-plan (sans bloquer l'UI ni notifier)
Future.microtask(() async {
try {
await loadMonthEvents(userId, prevYear, prevMonth,
canViewAllEvents: canViewAllEvents, silent: true);
await loadMonthEvents(userId, nextYear, nextMonth,
canViewAllEvents: canViewAllEvents, silent: true);
print('[EventProvider] Adjacent months preloaded successfully');
} catch (e) {
print('[EventProvider] Error preloading adjacent months: $e');
}
});
}
/// 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();
}
}