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.
This commit is contained in:
@@ -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é
|
||||
|
||||
Reference in New Issue
Block a user