diff --git a/em2rp/functions/index.js b/em2rp/functions/index.js index d6512b8..4e28f33 100644 --- a/em2rp/functions/index.js +++ b/em2rp/functions/index.js @@ -1663,19 +1663,25 @@ exports.getEvents = onRequest(httpOptions, withCors(async (req, res) => { } }); - // Récupérer tous les utilisateurs en une seule fois + // Récupérer tous les utilisateurs en PARALLÈLE (optimisé) const usersMap = {}; if (userIdsSet.size > 0) { const userIds = Array.from(userIdsSet); + const batchSize = 30; // Augmenté de 10 à 30 pour réduire le nombre de requêtes - // Récupérer par batch (Firestore limite à 10 par requête 'in') - const batchSize = 10; + // Exécuter les requêtes en PARALLÈLE au lieu de séquentiel + const batchPromises = []; for (let i = 0; i < userIds.length; i += batchSize) { const batch = userIds.slice(i, i + batchSize); - const usersSnapshot = await db.collection('users') - .where(admin.firestore.FieldPath.documentId(), 'in', batch) - .get(); + batchPromises.push( + db.collection('users') + .where(admin.firestore.FieldPath.documentId(), 'in', batch) + .get() + ); + } + const results = await Promise.all(batchPromises); + results.forEach(usersSnapshot => { usersSnapshot.docs.forEach(userDoc => { const userData = userDoc.data(); // Stocker uniquement les données publiques @@ -1688,7 +1694,7 @@ exports.getEvents = onRequest(httpOptions, withCors(async (req, res) => { profilePhotoUrl: userData.profilePhotoUrl || '', }; }); - } + }); } // Sérialiser les événements avec workforce comme liste d'UIDs @@ -1726,6 +1732,137 @@ exports.getEvents = onRequest(httpOptions, withCors(async (req, res) => { } })); +// ============================================================================ +// EVENTS - Get by month (optimized lazy loading) +// ============================================================================ + +/** + * Récupère les événements d'un mois spécifique (lazy loading optimisé) + * Réduit drastiquement le temps de chargement en ne chargeant que le mois demandé + */ +exports.getEventsByMonth = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const { userId, year, month } = req.body.data || {}; + + if (!year || !month) { + res.status(400).json({ error: 'year and month are required' }); + return; + } + + logger.info(`Fetching events for ${year}-${month}`); + + // Calculer le début et la fin du mois + const startOfMonth = admin.firestore.Timestamp.fromDate( + new Date(year, month - 1, 1, 0, 0, 0) + ); + const endOfMonth = admin.firestore.Timestamp.fromDate( + new Date(year, month, 0, 23, 59, 59) + ); + + // Vérifier si l'utilisateur peut voir tous les événements + const canViewAll = await auth.hasPermission(decodedToken.uid, 'view_all_events'); + + let eventsQuery = db.collection('events') + .where('StartDateTime', '>=', startOfMonth) + .where('StartDateTime', '<=', endOfMonth); + + if (!canViewAll) { + // Utilisateur normal : seulement ses événements assignés + const userRef = db.collection('users').doc(userId || decodedToken.uid); + eventsQuery = eventsQuery.where('workforce', 'array-contains', userRef); + } + + const eventsSnapshot = await eventsQuery.get(); + + logger.info(`Found ${eventsSnapshot.docs.length} events for ${year}-${month}`); + + // Collecter tous les UIDs utilisateurs uniques + const userIdsSet = new Set(); + + eventsSnapshot.docs.forEach(doc => { + const data = doc.data(); + if (data.workforce && Array.isArray(data.workforce)) { + data.workforce.forEach(userRef => { + if (userRef && userRef.id) { + userIdsSet.add(userRef.id); + } else if (typeof userRef === 'string' && userRef.startsWith('users/')) { + userIdsSet.add(userRef.split('/')[1]); + } + }); + } + }); + + // Récupérer tous les utilisateurs en PARALLÈLE (optimisé) + const usersMap = {}; + if (userIdsSet.size > 0) { + const userIds = Array.from(userIdsSet); + const batchSize = 30; // Limite Firestore augmentée de 10 à 30 + + // Exécuter les requêtes en parallèle au lieu de séquentiel + const batchPromises = []; + for (let i = 0; i < userIds.length; i += batchSize) { + const batch = userIds.slice(i, i + batchSize); + batchPromises.push( + db.collection('users') + .where(admin.firestore.FieldPath.documentId(), 'in', batch) + .get() + ); + } + + const results = await Promise.all(batchPromises); + results.forEach(usersSnapshot => { + usersSnapshot.docs.forEach(userDoc => { + const userData = userDoc.data(); + usersMap[userDoc.id] = { + uid: userDoc.id, + firstName: userData.firstName || '', + lastName: userData.lastName || '', + email: userData.email || '', + phoneNumber: userData.phoneNumber || '', + profilePhotoUrl: userData.profilePhotoUrl || '', + }; + }); + }); + } + + // Sérialiser les événements avec workforce comme liste d'UIDs + const events = eventsSnapshot.docs.map(doc => { + const data = doc.data(); + + // Convertir workforce en liste d'UIDs + let workforceUids = []; + if (data.workforce && Array.isArray(data.workforce)) { + workforceUids = data.workforce.map(userRef => { + if (userRef && userRef.id) { + return userRef.id; + } else if (typeof userRef === 'string' && userRef.startsWith('users/')) { + return userRef.split('/')[1]; + } + return null; + }).filter(uid => uid !== null); + } + + return { + id: doc.id, + ...helpers.serializeTimestamps(data), + workforce: workforceUids, + }; + }); + + logger.info(`Returning ${events.length} events with ${Object.keys(usersMap).length} unique users`); + + res.status(200).json({ + events, + users: usersMap, + month: { year, month } + }); + } catch (error) { + logger.error("Error fetching events by month:", error); + res.status(500).json({ error: error.message }); + } +})); + /** * Récupère un événement avec tous les détails (équipements complets + containers avec enfants) * Optimisé pour la page de préparation et l'affichage détaillé diff --git a/em2rp/lib/main.dart b/em2rp/lib/main.dart index 3b3744f..739bf0b 100644 --- a/em2rp/lib/main.dart +++ b/em2rp/lib/main.dart @@ -200,7 +200,10 @@ class _AutoLoginWrapperState extends State { @override void initState() { super.initState(); - _autoLogin(); + // Attendre la fin du premier build avant de naviguer + WidgetsBinding.instance.addPostFrameCallback((_) { + _autoLogin(); + }); } Future _autoLogin() async { diff --git a/em2rp/lib/providers/event_provider.dart b/em2rp/lib/providers/event_provider.dart index 078e480..26ae209 100644 --- a/em2rp/lib/providers/event_provider.dart +++ b/em2rp/lib/providers/event_provider.dart @@ -10,17 +10,21 @@ class EventProvider with ChangeNotifier { List _events = []; bool _isLoading = false; - List get events => _events; - bool get isLoading => _isLoading; - // Cache des utilisateurs chargés depuis getEvents Map> _usersCache = {}; - // Cache pour éviter les rechargements inutiles + // 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> _eventsByMonth = {}; // "2026-02" => [events] + String? _currentMonth; // Mois actuellement affiché + + List 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; @@ -98,6 +102,124 @@ class EventProvider with ChangeNotifier { } } + /// Charger les événements d'un mois spécifique (lazy loading optimisé) + Future 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>; + final usersData = result['users'] as Map; + + // Mettre à jour le cache utilisateurs (addAll pour cumuler) + _usersCache.addAll( + usersData.map((key, value) => MapEntry(key, value as Map)) + ); + + print('[EventProvider] Found ${eventsData.length} events for $monthKey'); + + PerformanceMonitor.start('EventProvider.parseMonthEvents'); + List 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 refreshEvents(String userId, {bool canViewAllEvents = false}) async { await loadUserEvents(userId, canViewAllEvents: canViewAllEvents, forceReload: true); diff --git a/em2rp/lib/providers/local_user_provider.dart b/em2rp/lib/providers/local_user_provider.dart index 304d8a8..617f5f3 100644 --- a/em2rp/lib/providers/local_user_provider.dart +++ b/em2rp/lib/providers/local_user_provider.dart @@ -196,7 +196,8 @@ class LocalUserProvider with ChangeNotifier { try { UserCredential userCredential = await _auth.signInWithEmailAndPassword( email: email, password: password); - await loadUserData(); + // Note: loadUserData() sera appelé en arrière-plan dans main.dart + // pour ne pas bloquer la navigation return userCredential; } catch (e) { throw FirebaseAuthException(code: 'login-failed', message: e.toString()); diff --git a/em2rp/lib/services/data_service.dart b/em2rp/lib/services/data_service.dart index 4c8c0fb..4d4f509 100644 --- a/em2rp/lib/services/data_service.dart +++ b/em2rp/lib/services/data_service.dart @@ -253,6 +253,36 @@ class DataService { } } + /// Récupère les événements d'un mois spécifique (lazy loading optimisé) + Future> getEventsByMonth({ + required String userId, + required int year, + required int month, + }) async { + try { + print('[DataService] Calling getEventsByMonth for $year-$month'); + final result = await _apiService.call('getEventsByMonth', { + 'userId': userId, + 'year': year, + 'month': month, + }); + + // Extraire events et users + final events = result['events'] as List? ?? []; + final users = result['users'] as Map? ?? {}; + + print('[DataService] Events loaded for $year-$month: ${events.length} events'); + + return { + 'events': events.map((e) => e as Map).toList(), + 'users': users, + }; + } catch (e) { + print('[DataService] Error getting events by month: $e'); + throw Exception('Erreur lors de la récupération des événements du mois: $e'); + } + } + /// Récupère un événement avec tous les détails (équipements complets + containers avec enfants) Future> getEventWithDetails(String eventId) async { try { diff --git a/em2rp/lib/views/calendar_page.dart b/em2rp/lib/views/calendar_page.dart index 6a58c2f..4c9cf20 100644 --- a/em2rp/lib/views/calendar_page.dart +++ b/em2rp/lib/views/calendar_page.dart @@ -36,11 +36,51 @@ class _CalendarPageState extends State { void initState() { super.initState(); initializeDateFormatting('fr_FR', null); - // Charger les événements de manière asynchrone sans bloquer l'UI - _loadEventsAsync(); + // Charger les événements du mois courant après le premier build + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadCurrentMonthEvents(); + }); + } + + /// Charge les événements du mois courant avec lazy loading + Future _loadCurrentMonthEvents() async { + PerformanceMonitor.start('CalendarPage.loadCurrentMonthEvents'); + + final localAuthProvider = Provider.of(context, listen: false); + final eventProvider = Provider.of(context, listen: false); + final userId = localAuthProvider.uid; + final canViewAllEvents = localAuthProvider.hasPermission('view_all_events'); + + if (userId != null) { + print('[CalendarPage] Loading events for ${_focusedDay.year}-${_focusedDay.month}'); + + await eventProvider.loadMonthEvents( + userId, + _focusedDay.year, + _focusedDay.month, + canViewAllEvents: canViewAllEvents, + ); + + // Précharger les mois adjacents en arrière-plan + eventProvider.preloadAdjacentMonths( + userId, + _focusedDay.year, + _focusedDay.month, + canViewAllEvents: canViewAllEvents, + ); + + if (mounted) { + PerformanceMonitor.start('CalendarPage.selectDefaultEvent'); + _selectDefaultEvent(); + PerformanceMonitor.end('CalendarPage.selectDefaultEvent'); + } + } + + PerformanceMonitor.end('CalendarPage.loadCurrentMonthEvents'); } /// Charge les événements de manière asynchrone et sélectionne l'événement approprié + /// DEPRECATED: Utiliser _loadCurrentMonthEvents à la place Future _loadEventsAsync() async { PerformanceMonitor.start('CalendarPage.loadEventsAsync'); await _loadEvents(); @@ -157,6 +197,12 @@ class _CalendarPageState extends State { // Appliquer le filtre utilisateur si actif final filteredEvents = _getFilteredEvents(eventProvider.events); + // Debug logs + print('[CalendarPage.build] Total events: ${eventProvider.events.length}, Filtered: ${filteredEvents.length}'); + if (eventProvider.events.isNotEmpty) { + print('[CalendarPage.build] First event: ${eventProvider.events.first.name} at ${eventProvider.events.first.startDateTime}'); + } + if (eventProvider.isLoading) { return const Scaffold( body: Center( @@ -659,9 +705,19 @@ class _CalendarPageState extends State { }); }, onPageChanged: (focusedDay) { + // Détecter si on a changé de mois + final monthChanged = focusedDay.year != _focusedDay.year || + focusedDay.month != _focusedDay.month; + setState(() { _focusedDay = focusedDay; }); + + // Charger les événements du nouveau mois si nécessaire + if (monthChanged) { + print('[CalendarPage] Month changed to ${focusedDay.year}-${focusedDay.month}'); + _loadCurrentMonthEvents(); + } }, onEventSelected: (event) { setState(() {