refactor: Passage à la pagination côté serveur pour les équipements et containers
Cette mise à jour refactorise en profondeur le chargement des données pour les équipements et les containers, en remplaçant la récupération complète de la collection par un système de pagination côté serveur. Ce changement améliore considérablement les performances, réduit la consommation de mémoire et accélère le temps de chargement initial, en particulier pour les larges inventaires.
**Changements Backend (Cloud Functions) :**
- **Nouveaux Endpoints Paginés :**
- `getEquipmentsPaginated` et `getContainersPaginated` ont été créés pour remplacer les anciens `getEquipments` et `getContainers`.
- Ces nouvelles fonctions supportent le filtrage (catégorie, statut, type), la recherche textuelle et le tri directement côté serveur, limitant la quantité de données transférées.
- La pagination est gérée via les paramètres `limit` et `startAfter`, assurant un chargement par lots efficace.
- **Optimisation de `getContainersPaginated` :**
- Peuple désormais les containers avec leurs équipements enfants via une requête `in` optimisée, réduisant le nombre de lectures Firestore.
- **Suppression des Anciens Endpoints :** Les fonctions `getEquipments` et `getContainers`, qui chargeaient l'intégralité des collections, ont été supprimées.
- **Nouveau Script de Migration :** Ajout d'un script (`migrate_equipment_ids.js`) pour s'assurer que chaque équipement dans Firestore possède un champ `id` correspondant à son ID de document, ce qui est crucial pour le tri et la pagination.
**Changements Frontend (Flutter) :**
- **`EquipmentProvider` et `ContainerProvider` :**
- La logique de chargement a été entièrement réécrite pour utiliser les nouveaux endpoints paginés.
- Introduction d'un mode `usePagination` pour basculer entre le chargement paginé (pour les pages de gestion) et le chargement complet (pour les dialogues de sélection).
- Implémentation de `loadFirstPage` et `loadNextPage` pour gérer le scroll infini.
- Ajout d'un "debouncing" sur la recherche pour éviter les appels API excessifs lors de la saisie.
- **Pages de Gestion (`EquipmentManagementPage`, `ContainerManagementPage`) :**
- Utilisent désormais un `ScrollController` pour déclencher `loadNextPage` et implémenter un scroll infini.
- Le chargement initial et les rechargements (après filtre) sont beaucoup plus rapides.
- Refonte de l'UI avec un nouveau widget `SearchActionsBar` pour uniformiser la barre de recherche et les actions.
- **Dialogue de Sélection d'Équipement (`EquipmentSelectionDialog`) :**
- Passe également à un système de lazy loading basé sur des `ChoiceChip` pour afficher soit les équipements, soit les containers.
- Charge les pages de manière asynchrone au fur et à mesure du scroll, améliorant drastiquement la réactivité du dialogue.
- La logique de chargement des données a été fiabilisée pour attendre la disponibilité des données avant l'affichage.
- **Optimisations diverses :**
- Les sections qui listent les événements associés à un équipement (`EquipmentCurrentEventsSection`, etc.) chargent désormais uniquement les containers pertinents via `getContainersByIds` au lieu de toute la collection.
- Le calcul du statut d'un équipement (`EquipmentStatusBadge`) est maintenant synchrone, simplifiant le code et évitant des `FutureBuilder`.
**Correction mineure :**
- **Nom de l'application :** Le nom de l'application a été mis à jour de "EM2 ERP" à "EM2 Hub" dans `main.dart` et dans les exports ICS.
This commit is contained in:
@@ -1725,76 +1725,6 @@ exports.getEvents = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
}
|
||||
}));
|
||||
|
||||
// ============================================================================
|
||||
// EQUIPMENTS - Read with permissions
|
||||
// ============================================================================
|
||||
exports.getEquipments = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
|
||||
// Vérifier les permissions
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_equipment');
|
||||
const canView = await auth.hasPermission(decodedToken.uid, 'view_equipment');
|
||||
|
||||
if (!canManage && !canView) {
|
||||
res.status(403).json({ error: 'Forbidden: Requires equipment permissions' });
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = await db.collection('equipments').get();
|
||||
const equipments = snapshot.docs.map(doc => {
|
||||
const data = doc.data();
|
||||
|
||||
// Masquer les prix si l'utilisateur n'a pas manage_equipment
|
||||
if (!canManage) {
|
||||
delete data.purchasePrice;
|
||||
delete data.rentalPrice;
|
||||
}
|
||||
|
||||
return {
|
||||
id: doc.id,
|
||||
...helpers.serializeTimestamps(data, ['purchaseDate', 'nextMaintenanceDate', 'lastMaintenanceDate', 'createdAt', 'updatedAt'])
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).json({ equipments });
|
||||
} catch (error) {
|
||||
logger.error("Error fetching equipments:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
// ============================================================================
|
||||
// CONTAINERS - Read with permissions
|
||||
// ============================================================================
|
||||
exports.getContainers = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
|
||||
// Vérifier les permissions
|
||||
const canView = await auth.hasPermission(decodedToken.uid, 'view_equipment');
|
||||
|
||||
if (!canView) {
|
||||
res.status(403).json({ error: 'Forbidden: Requires equipment permissions' });
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = await db.collection('containers').get();
|
||||
const containers = snapshot.docs.map(doc => {
|
||||
const data = doc.data();
|
||||
return {
|
||||
id: doc.id,
|
||||
...helpers.serializeTimestamps(data, ['createdAt', 'updatedAt'])
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).json({ containers });
|
||||
} catch (error) {
|
||||
logger.error("Error fetching containers:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
// ============================================================================
|
||||
// MAINTENANCES - Read with permissions
|
||||
// ============================================================================
|
||||
@@ -3555,4 +3485,408 @@ exports.onAlertCreated = onDocumentCreated('alerts/{alertId}', async (event) =>
|
||||
// const {onAlertCreated} = require('./onAlertCreated');
|
||||
// exports.onAlertCreated = onAlertCreated;
|
||||
|
||||
// ============================================================================
|
||||
// EQUIPMENTS - Pagination et filtrage avancé
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Récupère les équipements avec pagination et filtrage côté serveur
|
||||
*
|
||||
* Paramètres de requête supportés:
|
||||
* - limit: nombre d'éléments par page (défaut: 20, max: 100)
|
||||
* - startAfter: ID du dernier élément de la page précédente (pour pagination)
|
||||
* - category: filtre par catégorie
|
||||
* - status: filtre par statut
|
||||
* - searchQuery: recherche textuelle (nom, ID, modèle, marque)
|
||||
* - sortBy: champ de tri (défaut: 'id')
|
||||
* - sortOrder: 'asc' ou 'desc' (défaut: 'asc')
|
||||
*/
|
||||
exports.getEquipmentsPaginated = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
|
||||
// Vérifier les permissions
|
||||
const canManage = await auth.hasPermission(decodedToken.uid, 'manage_equipment');
|
||||
const canView = await auth.hasPermission(decodedToken.uid, 'view_equipment');
|
||||
|
||||
if (!canManage && !canView) {
|
||||
res.status(403).json({ error: 'Forbidden: Requires equipment permissions' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer les paramètres de la requête
|
||||
const params = req.method === 'GET' ? req.query : (req.body?.data || {});
|
||||
const limit = Math.min(parseInt(params.limit) || 20, 100);
|
||||
const startAfterId = params.startAfter || null;
|
||||
// Convertir en majuscules pour correspondre au format Firestore
|
||||
const category = params.category ? params.category.toUpperCase() : null;
|
||||
const status = params.status ? params.status.toUpperCase() : null;
|
||||
const searchQuery = params.searchQuery?.toLowerCase() || null;
|
||||
const sortBy = params.sortBy || 'id';
|
||||
const sortOrder = params.sortOrder === 'desc' ? 'desc' : 'asc';
|
||||
|
||||
logger.info(`[getEquipmentsPaginated] Params: limit=${limit}, startAfter=${startAfterId}, category=${category}, status=${status}, search=${searchQuery}`);
|
||||
|
||||
// Construire la requête Firestore
|
||||
let query = db.collection('equipments');
|
||||
|
||||
// Si recherche textuelle, on augmente la limite pour filtrer ensuite
|
||||
const queryLimit = searchQuery ? Math.min(limit * 10, 200) : limit;
|
||||
|
||||
// Appliquer les filtres
|
||||
if (category) {
|
||||
query = query.where('category', '==', category);
|
||||
}
|
||||
if (status) {
|
||||
query = query.where('status', '==', status);
|
||||
}
|
||||
|
||||
// Tri : Utiliser FieldPath.documentId() pour trier par l'UID du document
|
||||
// Cela garantit que TOUS les documents sont inclus, même sans champ 'id'
|
||||
if (sortBy === 'id') {
|
||||
query = query.orderBy(admin.firestore.FieldPath.documentId(), sortOrder);
|
||||
} else {
|
||||
query = query.orderBy(sortBy, sortOrder);
|
||||
}
|
||||
|
||||
// Pagination
|
||||
if (startAfterId) {
|
||||
const startAfterDoc = await db.collection('equipments').doc(startAfterId).get();
|
||||
if (startAfterDoc.exists) {
|
||||
query = query.startAfter(startAfterDoc);
|
||||
}
|
||||
}
|
||||
|
||||
// Limiter les résultats
|
||||
query = query.limit(queryLimit + 1);
|
||||
|
||||
const snapshot = await query.get();
|
||||
|
||||
// Déterminer hasMore basé sur le nombre de documents Firestore
|
||||
const rawDocCount = snapshot.docs.length;
|
||||
const hasMoreDocs = rawDocCount > queryLimit;
|
||||
const docsToProcess = hasMoreDocs ? snapshot.docs.slice(0, queryLimit) : snapshot.docs;
|
||||
|
||||
logger.info(`[getEquipmentsPaginated] Firestore returned ${rawDocCount} docs, hasMore=${hasMoreDocs}`);
|
||||
|
||||
let equipments = docsToProcess.map(doc => {
|
||||
const data = doc.data();
|
||||
|
||||
// Masquer les prix si l'utilisateur n'a pas manage_equipment
|
||||
if (!canManage) {
|
||||
delete data.purchasePrice;
|
||||
delete data.rentalPrice;
|
||||
}
|
||||
|
||||
return {
|
||||
id: doc.id,
|
||||
...helpers.serializeTimestamps(data, ['purchaseDate', 'nextMaintenanceDate', 'lastMaintenanceDate', 'createdAt', 'updatedAt'])
|
||||
};
|
||||
});
|
||||
|
||||
// Filtrage textuel côté serveur
|
||||
if (searchQuery) {
|
||||
equipments = equipments.filter(eq => {
|
||||
const searchableText = [
|
||||
eq.name || '',
|
||||
eq.id || '',
|
||||
eq.model || '',
|
||||
eq.brand || '',
|
||||
eq.subCategory || ''
|
||||
].join(' ').toLowerCase();
|
||||
return searchableText.includes(searchQuery);
|
||||
});
|
||||
}
|
||||
|
||||
// Pour la limite finale après filtrage textuel
|
||||
const limitedEquipments = equipments.slice(0, limit);
|
||||
const lastVisible = limitedEquipments.length > 0 ? limitedEquipments[limitedEquipments.length - 1].id : null;
|
||||
|
||||
// hasMore reste basé sur le nombre de docs Firestore, pas sur le filtrage textuel
|
||||
logger.info(`[getEquipmentsPaginated] Returning ${limitedEquipments.length} equipments (filtered from ${equipments.length}), hasMore=${hasMoreDocs}`);
|
||||
|
||||
res.status(200).json({
|
||||
equipments: limitedEquipments,
|
||||
hasMore: hasMoreDocs,
|
||||
lastVisible,
|
||||
total: limitedEquipments.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error("Error fetching paginated equipments:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
// ============================================================================
|
||||
// CONTAINERS - Pagination et filtrage avancé
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Récupère les containers avec pagination et filtrage côté serveur
|
||||
*
|
||||
* Paramètres similaires à getEquipmentsPaginated
|
||||
*/
|
||||
exports.getContainersPaginated = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
|
||||
// Vérifier les permissions
|
||||
const canView = await auth.hasPermission(decodedToken.uid, 'view_equipment');
|
||||
|
||||
if (!canView) {
|
||||
res.status(403).json({ error: 'Forbidden: Requires equipment permissions' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer les paramètres de la requête
|
||||
const params = req.method === 'GET' ? req.query : (req.body?.data || {});
|
||||
const limit = Math.min(parseInt(params.limit) || 20, 100);
|
||||
const startAfterId = params.startAfter || null;
|
||||
// Convertir en majuscules pour correspondre au format Firestore
|
||||
const type = params.type ? params.type.toUpperCase() : null;
|
||||
const status = params.status ? params.status.toUpperCase() : null;
|
||||
const searchQuery = params.searchQuery?.toLowerCase() || null;
|
||||
const category = params.category ? params.category.toUpperCase() : null; // Filtre par catégorie d'équipements
|
||||
const sortBy = params.sortBy || 'id';
|
||||
const sortOrder = params.sortOrder === 'desc' ? 'desc' : 'asc';
|
||||
|
||||
logger.info(`[getContainersPaginated] Params: limit=${limit}, startAfter=${startAfterId}, type=${type}, status=${status}, category=${category}, search=${searchQuery}`);
|
||||
|
||||
// Construire la requête Firestore
|
||||
let query = db.collection('containers');
|
||||
|
||||
// Si recherche textuelle ou filtre par catégorie, on augmente la limite pour filtrer ensuite
|
||||
const queryLimit = (searchQuery || category) ? Math.min(limit * 10, 200) : limit;
|
||||
|
||||
// Appliquer les filtres sur les containers
|
||||
if (type) {
|
||||
query = query.where('type', '==', type);
|
||||
}
|
||||
if (status) {
|
||||
query = query.where('status', '==', status);
|
||||
}
|
||||
|
||||
// Tri : Utiliser FieldPath.documentId() pour trier par l'UID du document
|
||||
if (sortBy === 'id') {
|
||||
query = query.orderBy(admin.firestore.FieldPath.documentId(), sortOrder);
|
||||
} else {
|
||||
query = query.orderBy(sortBy, sortOrder);
|
||||
}
|
||||
|
||||
// Pagination
|
||||
if (startAfterId) {
|
||||
const startAfterDoc = await db.collection('containers').doc(startAfterId).get();
|
||||
if (startAfterDoc.exists) {
|
||||
query = query.startAfter(startAfterDoc);
|
||||
}
|
||||
}
|
||||
|
||||
// Limiter les résultats
|
||||
query = query.limit(queryLimit + 1);
|
||||
|
||||
const snapshot = await query.get();
|
||||
|
||||
// Déterminer hasMore basé sur le nombre de documents Firestore
|
||||
const rawDocCount = snapshot.docs.length;
|
||||
const hasMoreDocs = rawDocCount > queryLimit;
|
||||
const docsToProcess = hasMoreDocs ? snapshot.docs.slice(0, queryLimit) : snapshot.docs;
|
||||
|
||||
let containers = docsToProcess.map(doc => {
|
||||
const data = doc.data();
|
||||
return {
|
||||
id: doc.id,
|
||||
...helpers.serializeTimestamps(data, ['createdAt', 'updatedAt'])
|
||||
};
|
||||
});
|
||||
|
||||
// Récupérer tous les équipements liés aux containers (pour population ET filtrage)
|
||||
const allEquipmentIds = new Set();
|
||||
containers.forEach(c => {
|
||||
if (c.equipmentIds && Array.isArray(c.equipmentIds)) {
|
||||
c.equipmentIds.forEach(id => allEquipmentIds.add(id));
|
||||
}
|
||||
});
|
||||
|
||||
// Charger les équipements en batch (max 30 par requête Firestore)
|
||||
const equipmentMap = new Map();
|
||||
if (allEquipmentIds.size > 0) {
|
||||
const equipmentIdArray = Array.from(allEquipmentIds);
|
||||
const batchSize = 30; // Limite Firestore pour les requêtes 'in'
|
||||
|
||||
for (let i = 0; i < equipmentIdArray.length; i += batchSize) {
|
||||
const batch = equipmentIdArray.slice(i, i + batchSize);
|
||||
const equipmentSnapshot = await db.collection('equipments')
|
||||
.where(admin.firestore.FieldPath.documentId(), 'in', batch)
|
||||
.get();
|
||||
|
||||
equipmentSnapshot.docs.forEach(doc => {
|
||||
const equipmentData = doc.data();
|
||||
equipmentMap.set(doc.id, {
|
||||
id: doc.id,
|
||||
...helpers.serializeTimestamps(equipmentData)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Peupler les containers avec leurs équipements
|
||||
containers = containers.map(container => ({
|
||||
...container,
|
||||
equipment: (container.equipmentIds || [])
|
||||
.map(eqId => equipmentMap.get(eqId))
|
||||
.filter(eq => eq !== undefined) // Retirer les équipements non trouvés
|
||||
}));
|
||||
|
||||
// Filtrage par catégorie d'équipements
|
||||
if (category) {
|
||||
containers = containers.filter(c => {
|
||||
// Garder le container s'il contient au moins un équipement de la catégorie demandée
|
||||
return c.equipment.some(eq => eq.category === category);
|
||||
});
|
||||
}
|
||||
|
||||
// Filtrage textuel côté serveur
|
||||
if (searchQuery) {
|
||||
containers = containers.filter(c => {
|
||||
const searchableText = [
|
||||
c.name || '',
|
||||
c.id || '',
|
||||
...(c.equipment || []).map(eq => eq.name || '')
|
||||
].join(' ').toLowerCase();
|
||||
return searchableText.includes(searchQuery);
|
||||
});
|
||||
}
|
||||
|
||||
// Pour la limite finale après filtrage
|
||||
const limitedContainers = containers.slice(0, limit);
|
||||
const lastVisible = limitedContainers.length > 0 ? limitedContainers[limitedContainers.length - 1].id : null;
|
||||
|
||||
// Log pour debugging
|
||||
const totalEquipmentCount = limitedContainers.reduce((sum, c) => sum + (c.equipment?.length || 0), 0);
|
||||
logger.info(`[getContainersPaginated] Returning ${limitedContainers.length} containers with ${totalEquipmentCount} total equipment(s)`);
|
||||
|
||||
// Log détaillé pour chaque container
|
||||
limitedContainers.forEach(c => {
|
||||
logger.info(` - Container ${c.id}: ${c.equipment?.length || 0} equipment(s)`);
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
containers: limitedContainers,
|
||||
hasMore: containers.length > limit || hasMoreDocs,
|
||||
lastVisible,
|
||||
total: limitedContainers.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error("Error fetching paginated containers:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
// ============================================================================
|
||||
// SEARCH - Recherche unifiée avec autocomplétion
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Recherche rapide d'équipements et containers pour l'autocomplétion
|
||||
* Retourne un nombre limité de résultats pour des performances optimales
|
||||
*/
|
||||
exports.quickSearch = onRequest(httpOptions, withCors(async (req, res) => {
|
||||
try {
|
||||
const decodedToken = await auth.authenticateUser(req);
|
||||
|
||||
// Vérifier les permissions
|
||||
const canView = await auth.hasPermission(decodedToken.uid, 'view_equipment');
|
||||
|
||||
if (!canView) {
|
||||
res.status(403).json({ error: 'Forbidden: Requires view_equipment permission' });
|
||||
return;
|
||||
}
|
||||
|
||||
const params = req.method === 'GET' ? req.query : (req.body?.data || {});
|
||||
const searchQuery = params.query?.toLowerCase() || '';
|
||||
const limit = Math.min(parseInt(params.limit) || 10, 50);
|
||||
const includeEquipments = params.includeEquipments !== 'false';
|
||||
const includeContainers = params.includeContainers !== 'false';
|
||||
|
||||
if (!searchQuery || searchQuery.length < 2) {
|
||||
res.status(200).json({ results: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
// Rechercher dans les équipements
|
||||
if (includeEquipments) {
|
||||
const equipmentSnapshot = await db.collection('equipments')
|
||||
.orderBy('id')
|
||||
.limit(limit * 2) // Récupérer plus pour filtrer ensuite
|
||||
.get();
|
||||
|
||||
equipmentSnapshot.docs.forEach(doc => {
|
||||
const data = doc.data();
|
||||
const searchableText = [
|
||||
data.name || '',
|
||||
doc.id || '',
|
||||
data.model || '',
|
||||
data.brand || ''
|
||||
].join(' ').toLowerCase();
|
||||
|
||||
if (searchableText.includes(searchQuery)) {
|
||||
results.push({
|
||||
type: 'equipment',
|
||||
id: doc.id,
|
||||
name: data.name,
|
||||
category: data.category,
|
||||
model: data.model,
|
||||
brand: data.brand
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Rechercher dans les containers
|
||||
if (includeContainers) {
|
||||
const containerSnapshot = await db.collection('containers')
|
||||
.orderBy('id')
|
||||
.limit(limit * 2)
|
||||
.get();
|
||||
|
||||
containerSnapshot.docs.forEach(doc => {
|
||||
const data = doc.data();
|
||||
const searchableText = [
|
||||
data.name || '',
|
||||
doc.id || ''
|
||||
].join(' ').toLowerCase();
|
||||
|
||||
if (searchableText.includes(searchQuery)) {
|
||||
results.push({
|
||||
type: 'container',
|
||||
id: doc.id,
|
||||
name: data.name,
|
||||
containerType: data.type
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Limiter et trier les résultats
|
||||
const limitedResults = results
|
||||
.sort((a, b) => {
|
||||
// Prioriser les correspondances exactes au début
|
||||
const aStarts = a.id.toLowerCase().startsWith(searchQuery);
|
||||
const bStarts = b.id.toLowerCase().startsWith(searchQuery);
|
||||
if (aStarts && !bStarts) return -1;
|
||||
if (!aStarts && bStarts) return 1;
|
||||
return 0;
|
||||
})
|
||||
.slice(0, limit);
|
||||
|
||||
res.status(200).json({ results: limitedResults });
|
||||
|
||||
} catch (error) {
|
||||
logger.error("Error in quick search:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}));
|
||||
|
||||
93
em2rp/functions/migrate_equipment_ids.js
Normal file
93
em2rp/functions/migrate_equipment_ids.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Script de migration pour ajouter le champ 'id' aux équipements qui n'en ont pas
|
||||
*
|
||||
* Ce script parcourt tous les documents de la collection 'equipments' et ajoute
|
||||
* le champ 'id' avec la valeur du document ID si ce champ est manquant.
|
||||
*/
|
||||
|
||||
const admin = require('firebase-admin');
|
||||
const serviceAccount = require('./serviceAccountKey.json');
|
||||
|
||||
// Initialiser Firebase Admin
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert(serviceAccount)
|
||||
});
|
||||
|
||||
const db = admin.firestore();
|
||||
|
||||
async function migrateEquipmentIds() {
|
||||
console.log('🔧 Migration: Ajout du champ id aux équipements');
|
||||
console.log('================================================\n');
|
||||
|
||||
try {
|
||||
// Récupérer tous les équipements
|
||||
const equipmentsSnapshot = await db.collection('equipments').get();
|
||||
console.log(`📦 Total d'équipements: ${equipmentsSnapshot.size}`);
|
||||
|
||||
let missingIdCount = 0;
|
||||
let updatedCount = 0;
|
||||
let errorCount = 0;
|
||||
const batch = db.batch();
|
||||
let batchCount = 0;
|
||||
|
||||
for (const doc of equipmentsSnapshot.docs) {
|
||||
const data = doc.data();
|
||||
|
||||
// Vérifier si le champ 'id' est manquant ou vide
|
||||
if (!data.id || data.id === '') {
|
||||
missingIdCount++;
|
||||
console.log(`❌ Équipement ${doc.id} (${data.name || 'Sans nom'}) : champ 'id' manquant`);
|
||||
|
||||
// Ajouter au batch
|
||||
batch.update(doc.ref, { id: doc.id });
|
||||
batchCount++;
|
||||
updatedCount++;
|
||||
|
||||
// Exécuter le batch tous les 500 documents (limite Firestore)
|
||||
if (batchCount === 500) {
|
||||
await batch.commit();
|
||||
console.log(`✅ Batch de ${batchCount} documents mis à jour`);
|
||||
batchCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exécuter le dernier batch s'il reste des documents
|
||||
if (batchCount > 0) {
|
||||
await batch.commit();
|
||||
console.log(`✅ Batch final de ${batchCount} documents mis à jour`);
|
||||
}
|
||||
|
||||
console.log('\n================================================');
|
||||
console.log('📊 RÉSUMÉ DE LA MIGRATION');
|
||||
console.log('================================================');
|
||||
console.log(`Total d'équipements: ${equipmentsSnapshot.size}`);
|
||||
console.log(`Équipements avec 'id' manquant: ${missingIdCount}`);
|
||||
console.log(`Équipements mis à jour: ${updatedCount}`);
|
||||
console.log(`Erreurs: ${errorCount}`);
|
||||
console.log('================================================\n');
|
||||
|
||||
if (missingIdCount === 0) {
|
||||
console.log('✅ Tous les équipements ont déjà un champ id !');
|
||||
} else if (updatedCount === missingIdCount) {
|
||||
console.log('✅ Migration terminée avec succès !');
|
||||
} else {
|
||||
console.log('⚠️ Migration terminée avec des erreurs');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur lors de la migration:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Exécuter la migration
|
||||
migrateEquipmentIds()
|
||||
.then(() => {
|
||||
console.log('\n✅ Script terminé');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('\n❌ Script échoué:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user