feat: refactor de la gestion des utilisateurs et migration de la logique métier vers les Cloud Functions
Cette mise à jour majeure refactorise entièrement la gestion des utilisateurs pour la faire passer par des Cloud Functions sécurisées et migre une part importante de la logique métier (gestion des événements, maintenances, containers) du client vers le backend.
**Gestion des Utilisateurs (Backend & Frontend):**
- **Nouvelle fonction `createUserWithInvite` :**
- Crée l'utilisateur dans Firebase Auth avec un mot de passe temporaire.
- Crée le document utilisateur correspondant dans Firestore.
- Envoie automatiquement un e-mail de réinitialisation de mot de passe (via l'API REST de Firebase et `axios`) pour que l'utilisateur définisse son propre mot de passe, améliorant la sécurité et l'expérience d'intégration.
- **Refactorisation de `updateUser` et `deleteUser` :**
- Les anciennes fonctions `onCall` sont remplacées par des fonctions `onRequest` (HTTP) standards, alignées avec le reste de l'API.
- La logique de suppression gère désormais la suppression dans Auth et Firestore.
- **Réinitialisation de Mot de Passe (UI) :**
- Ajout d'un bouton "Réinitialiser le mot de passe" sur la carte utilisateur, permettant aux administrateurs d'envoyer un e-mail de réinitialisation à n'importe quel utilisateur.
- **Amélioration de l'UI :**
- Boîte de dialogue de confirmation améliorée pour la suppression d'un utilisateur.
- Notifications (Snackbars) pour les opérations de création, suppression et réinitialisation de mot de passe.
**Migration de la Logique Métier vers les Cloud Functions:**
- **Gestion de la Préparation d'Événements :**
- Migration complète de la logique de validation des étapes (préparation, chargement, déchargement, retour) du client vers de nouvelles Cloud Functions (`validateEquipmentPreparation`, `validateAllLoading`, etc.).
- Le backend gère désormais la mise à jour des statuts de l'événement (`inProgress`, `completed`) et des équipements (`inUse`, `available`).
- Le code frontend (`EventPreparationService`) a été simplifié pour appeler ces nouvelles fonctions au lieu d'effectuer des écritures directes sur Firestore.
- **Création de Maintenance :**
- La fonction `createMaintenance` gère maintenant la mise à jour des équipements associés (`maintenanceIds`) et la création d'alertes (`maintenanceDue`) si une maintenance est prévue prochainement. La logique client a été supprimée.
- **Suppression de Container :**
- La fonction `deleteContainer` a été améliorée pour nettoyer automatiquement les références (`parentBoxIds`) dans tous les équipements contenus avant de supprimer le container.
**Refactorisation et Corrections (Backend & Frontend) :**
- **Fiabilisation des Appels API (Frontend) :**
- Le `ApiService` a été renforcé pour convertir de manière plus robuste les données (notamment les `Map` de type `_JsonMap`) en JSON standard avant de les envoyer aux Cloud Functions, évitant ainsi des erreurs de sérialisation.
- **Correction des Références (Backend) :**
- La fonction `updateUser` convertit correctement les `roleId` (string) en `DocumentReference` Firestore.
- Sécurisation de la vérification de l'assignation d'un utilisateur à un événement (`workforce`) pour éviter les erreurs sur des références nulles.
- **Dépendance (Backend) :**
- Ajout de la librairie `axios` pour effectuer des appels à l'API REST de Firebase.
This commit is contained in:
@@ -1,19 +1,14 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:em2rp/models/equipment_model.dart';
|
||||
import 'package:em2rp/models/container_model.dart';
|
||||
import 'package:em2rp/models/alert_model.dart';
|
||||
import 'package:em2rp/models/maintenance_model.dart';
|
||||
import 'package:em2rp/models/container_model.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
import 'package:em2rp/services/data_service.dart';
|
||||
import 'package:em2rp/services/maintenance_service.dart';
|
||||
|
||||
class EquipmentService {
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
final ApiService _apiService = apiService;
|
||||
final DataService _dataService = DataService(apiService);
|
||||
|
||||
// Collection references (utilisées seulement pour les lectures)
|
||||
CollectionReference get _equipmentCollection => _firestore.collection('equipments');
|
||||
|
||||
// ============================================================================
|
||||
// CRUD Operations - Utilise le backend sécurisé
|
||||
// ============================================================================
|
||||
@@ -58,61 +53,61 @@ class EquipmentService {
|
||||
/// Récupérer un équipement par ID
|
||||
Future<EquipmentModel?> getEquipmentById(String id) async {
|
||||
try {
|
||||
final doc = await _equipmentCollection.doc(id).get();
|
||||
if (doc.exists) {
|
||||
return EquipmentModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
|
||||
}
|
||||
return null;
|
||||
final equipmentsData = await _dataService.getEquipmentsByIds([id]);
|
||||
if (equipmentsData.isEmpty) return null;
|
||||
|
||||
return EquipmentModel.fromMap(equipmentsData.first, id);
|
||||
} catch (e) {
|
||||
print('Error getting equipment: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupérer les équipements avec filtres (stream temps réel)
|
||||
Stream<List<EquipmentModel>> getEquipment({
|
||||
/// Récupérer les équipements avec filtres
|
||||
Future<List<EquipmentModel>> getEquipment({
|
||||
EquipmentCategory? category,
|
||||
EquipmentStatus? status,
|
||||
String? model,
|
||||
String? searchQuery,
|
||||
}) {
|
||||
}) async {
|
||||
try {
|
||||
Query query = _equipmentCollection;
|
||||
final equipmentsData = await _dataService.getEquipments();
|
||||
|
||||
// Filtre par catégorie
|
||||
var equipmentList = equipmentsData
|
||||
.map((data) => EquipmentModel.fromMap(data, data['id'] as String))
|
||||
.toList();
|
||||
|
||||
// Filtres côté client
|
||||
if (category != null) {
|
||||
query = query.where('category', isEqualTo: equipmentCategoryToString(category));
|
||||
}
|
||||
|
||||
// Filtre par statut
|
||||
if (status != null) {
|
||||
query = query.where('status', isEqualTo: equipmentStatusToString(status));
|
||||
}
|
||||
|
||||
// Filtre par modèle
|
||||
if (model != null && model.isNotEmpty) {
|
||||
query = query.where('model', isEqualTo: model);
|
||||
}
|
||||
|
||||
return query.snapshots().map((snapshot) {
|
||||
List<EquipmentModel> equipmentList = snapshot.docs
|
||||
.map((doc) => EquipmentModel.fromMap(doc.data() as Map<String, dynamic>, doc.id))
|
||||
equipmentList = equipmentList
|
||||
.where((e) => e.category == category)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Filtre par recherche texte (côté client car Firestore ne supporte pas les recherches texte complexes)
|
||||
if (searchQuery != null && searchQuery.isNotEmpty) {
|
||||
final lowerSearch = searchQuery.toLowerCase();
|
||||
equipmentList = equipmentList.where((equipment) {
|
||||
return equipment.name.toLowerCase().contains(lowerSearch) ||
|
||||
(equipment.model?.toLowerCase().contains(lowerSearch) ?? false) ||
|
||||
equipment.id.toLowerCase().contains(lowerSearch);
|
||||
}).toList();
|
||||
}
|
||||
if (status != null) {
|
||||
equipmentList = equipmentList
|
||||
.where((e) => e.status == status)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return equipmentList;
|
||||
});
|
||||
if (model != null && model.isNotEmpty) {
|
||||
equipmentList = equipmentList
|
||||
.where((e) => e.model == model)
|
||||
.toList();
|
||||
}
|
||||
|
||||
if (searchQuery != null && searchQuery.isNotEmpty) {
|
||||
final lowerSearch = searchQuery.toLowerCase();
|
||||
equipmentList = equipmentList.where((equipment) {
|
||||
return equipment.name.toLowerCase().contains(lowerSearch) ||
|
||||
(equipment.model?.toLowerCase().contains(lowerSearch) ?? false) ||
|
||||
equipment.id.toLowerCase().contains(lowerSearch);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return equipmentList;
|
||||
} catch (e) {
|
||||
print('Error streaming equipment: $e');
|
||||
print('Error getting equipment: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -122,33 +117,21 @@ class EquipmentService {
|
||||
// ============================================================================
|
||||
|
||||
/// Vérifier la disponibilité d'un équipement pour une période donnée
|
||||
Future<List<String>> checkAvailability(
|
||||
Future<List<Map<String, dynamic>>> checkAvailability(
|
||||
String equipmentId,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
) async {
|
||||
try {
|
||||
final conflicts = <String>[];
|
||||
final response = await _apiService.call('checkEquipmentAvailability', {
|
||||
'equipmentId': equipmentId,
|
||||
'startDate': startDate.toIso8601String(),
|
||||
'endDate': endDate.toIso8601String(),
|
||||
});
|
||||
|
||||
// Récupérer tous les événements qui chevauchent la période
|
||||
final eventsQuery = await _firestore.collection('events')
|
||||
.where('StartDateTime', isLessThanOrEqualTo: Timestamp.fromDate(endDate))
|
||||
.where('EndDateTime', isGreaterThanOrEqualTo: Timestamp.fromDate(startDate))
|
||||
.get();
|
||||
|
||||
for (var eventDoc in eventsQuery.docs) {
|
||||
final eventData = eventDoc.data();
|
||||
final assignedEquipmentRaw = eventData['assignedEquipment'] ?? [];
|
||||
|
||||
if (assignedEquipmentRaw is List) {
|
||||
for (var eq in assignedEquipmentRaw) {
|
||||
if (eq is Map && eq['equipmentId'] == equipmentId) {
|
||||
conflicts.add(eventDoc.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
final conflicts = (response['conflicts'] as List?)
|
||||
?.map((c) => c as Map<String, dynamic>)
|
||||
.toList() ?? [];
|
||||
|
||||
return conflicts;
|
||||
} catch (e) {
|
||||
@@ -164,26 +147,15 @@ class EquipmentService {
|
||||
DateTime endDate,
|
||||
) async {
|
||||
try {
|
||||
// Récupérer tous les équipements du même modèle
|
||||
final equipmentQuery = await _firestore.collection('equipments')
|
||||
.where('model', isEqualTo: model)
|
||||
.get();
|
||||
final response = await _apiService.call('findAlternativeEquipment', {
|
||||
'model': model,
|
||||
'startDate': startDate.toIso8601String(),
|
||||
'endDate': endDate.toIso8601String(),
|
||||
});
|
||||
|
||||
final alternatives = <EquipmentModel>[];
|
||||
|
||||
for (var doc in equipmentQuery.docs) {
|
||||
final equipment = EquipmentModel.fromMap(
|
||||
doc.data(),
|
||||
doc.id,
|
||||
);
|
||||
|
||||
// Vérifier la disponibilité
|
||||
final conflicts = await checkAvailability(equipment.id, startDate, endDate);
|
||||
|
||||
if (conflicts.isEmpty && equipment.status == EquipmentStatus.available) {
|
||||
alternatives.add(equipment);
|
||||
}
|
||||
}
|
||||
final alternatives = (response['alternatives'] as List?)
|
||||
?.map((a) => EquipmentModel.fromMap(a as Map<String, dynamic>, a['id'] as String))
|
||||
.toList() ?? [];
|
||||
|
||||
return alternatives;
|
||||
} catch (e) {
|
||||
@@ -224,20 +196,15 @@ class EquipmentService {
|
||||
/// Vérifier les stocks critiques et créer des alertes
|
||||
Future<void> checkCriticalStock() async {
|
||||
try {
|
||||
final equipmentQuery = await _firestore.collection('equipments')
|
||||
.where('category', whereIn: [
|
||||
equipmentCategoryToString(EquipmentCategory.consumable),
|
||||
equipmentCategoryToString(EquipmentCategory.cable),
|
||||
])
|
||||
.get();
|
||||
final equipmentsData = await _dataService.getEquipments();
|
||||
|
||||
for (var doc in equipmentQuery.docs) {
|
||||
final equipment = EquipmentModel.fromMap(
|
||||
doc.data(),
|
||||
doc.id,
|
||||
);
|
||||
for (var data in equipmentsData) {
|
||||
final equipment = EquipmentModel.fromMap(data, data['id'] as String);
|
||||
|
||||
if (equipment.isCriticalStock) {
|
||||
// Filtrer uniquement les consommables et câbles
|
||||
if ((equipment.category == EquipmentCategory.consumable ||
|
||||
equipment.category == EquipmentCategory.cable) &&
|
||||
equipment.isCriticalStock) {
|
||||
await _createLowStockAlert(equipment);
|
||||
}
|
||||
}
|
||||
@@ -250,27 +217,19 @@ class EquipmentService {
|
||||
/// Créer une alerte de stock faible
|
||||
Future<void> _createLowStockAlert(EquipmentModel equipment) async {
|
||||
try {
|
||||
// Vérifier si une alerte existe déjà pour cet équipement
|
||||
final existingAlerts = await _firestore.collection('alerts')
|
||||
.where('equipmentId', isEqualTo: equipment.id)
|
||||
.where('type', isEqualTo: alertTypeToString(AlertType.lowStock))
|
||||
.where('isRead', isEqualTo: false)
|
||||
.get();
|
||||
|
||||
if (existingAlerts.docs.isEmpty) {
|
||||
final alert = AlertModel(
|
||||
id: _firestore.collection('alerts').doc().id,
|
||||
type: AlertType.lowStock,
|
||||
message: 'Stock critique pour ${equipment.name} (${equipment.model ?? ""}): ${equipment.availableQuantity}/${equipment.criticalThreshold}',
|
||||
equipmentId: equipment.id,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
await _firestore.collection('alerts').doc(alert.id).set(alert.toMap());
|
||||
}
|
||||
// Note: Cette fonction pourrait utiliser une Cloud Function dédiée dans le futur
|
||||
// Pour l'instant, on utilise l'API directement pour éviter de créer trop de fonctions
|
||||
// Cette méthode est appelée rarement et en arrière-plan
|
||||
await _apiService.call('createAlert', {
|
||||
'type': 'LOW_STOCK',
|
||||
'title': 'Stock critique',
|
||||
'message': 'Stock critique pour ${equipment.name} (${equipment.model ?? ""}): ${equipment.availableQuantity}/${equipment.criticalThreshold}',
|
||||
'severity': 'HIGH',
|
||||
'equipmentId': equipment.id,
|
||||
});
|
||||
} catch (e) {
|
||||
print('Error creating low stock alert: $e');
|
||||
rethrow;
|
||||
// Ne pas rethrow pour ne pas bloquer le processus
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,11 +243,10 @@ class EquipmentService {
|
||||
/// Récupérer tous les modèles uniques (pour l'indexation/autocomplete)
|
||||
Future<List<String>> getAllModels() async {
|
||||
try {
|
||||
final equipmentQuery = await _firestore.collection('equipments').get();
|
||||
final equipmentsData = await _dataService.getEquipments();
|
||||
final models = <String>{};
|
||||
|
||||
for (var doc in equipmentQuery.docs) {
|
||||
final data = doc.data();
|
||||
for (var data in equipmentsData) {
|
||||
final model = data['model'] as String?;
|
||||
if (model != null && model.isNotEmpty) {
|
||||
models.add(model);
|
||||
@@ -305,11 +263,10 @@ class EquipmentService {
|
||||
/// Récupérer toutes les marques uniques (pour l'indexation/autocomplete)
|
||||
Future<List<String>> getAllBrands() async {
|
||||
try {
|
||||
final equipmentQuery = await _firestore.collection('equipments').get();
|
||||
final equipmentsData = await _dataService.getEquipments();
|
||||
final brands = <String>{};
|
||||
|
||||
for (var doc in equipmentQuery.docs) {
|
||||
final data = doc.data();
|
||||
for (var data in equipmentsData) {
|
||||
final brand = data['brand'] as String?;
|
||||
if (brand != null && brand.isNotEmpty) {
|
||||
brands.add(brand);
|
||||
@@ -326,16 +283,15 @@ class EquipmentService {
|
||||
/// Récupérer les modèles filtrés par marque
|
||||
Future<List<String>> getModelsByBrand(String brand) async {
|
||||
try {
|
||||
final equipmentQuery = await _firestore.collection('equipments')
|
||||
.where('brand', isEqualTo: brand)
|
||||
.get();
|
||||
final equipmentsData = await _dataService.getEquipments();
|
||||
final models = <String>{};
|
||||
|
||||
for (var doc in equipmentQuery.docs) {
|
||||
final data = doc.data();
|
||||
final model = data['model'] as String?;
|
||||
if (model != null && model.isNotEmpty) {
|
||||
models.add(model);
|
||||
for (var data in equipmentsData) {
|
||||
if (data['brand'] == brand) {
|
||||
final model = data['model'] as String?;
|
||||
if (model != null && model.isNotEmpty) {
|
||||
models.add(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,8 +305,8 @@ class EquipmentService {
|
||||
/// Vérifier si un ID existe déjà
|
||||
Future<bool> isIdUnique(String id) async {
|
||||
try {
|
||||
final doc = await _firestore.collection('equipments').doc(id).get();
|
||||
return !doc.exists;
|
||||
final equipment = await getEquipmentById(id);
|
||||
return equipment == null;
|
||||
} catch (e) {
|
||||
print('Error checking ID uniqueness: $e');
|
||||
rethrow;
|
||||
@@ -381,27 +337,11 @@ class EquipmentService {
|
||||
try {
|
||||
if (ids.isEmpty) return [];
|
||||
|
||||
final equipments = <EquipmentModel>[];
|
||||
final equipmentsData = await _dataService.getEquipmentsByIds(ids);
|
||||
|
||||
// Firestore limite les requêtes whereIn à 10 éléments
|
||||
// On doit donc diviser en plusieurs requêtes si nécessaire
|
||||
for (int i = 0; i < ids.length; i += 10) {
|
||||
final batch = ids.skip(i).take(10).toList();
|
||||
final query = await _firestore.collection('equipments')
|
||||
.where(FieldPath.documentId, whereIn: batch)
|
||||
.get();
|
||||
|
||||
for (var doc in query.docs) {
|
||||
equipments.add(
|
||||
EquipmentModel.fromMap(
|
||||
doc.data(),
|
||||
doc.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return equipments;
|
||||
return equipmentsData
|
||||
.map((data) => EquipmentModel.fromMap(data, data['id'] as String))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
print('Error getting equipments by IDs: $e');
|
||||
rethrow;
|
||||
@@ -409,25 +349,13 @@ class EquipmentService {
|
||||
}
|
||||
|
||||
/// Récupérer les maintenances pour un équipement
|
||||
/// Note: Cette méthode est maintenant déléguée au MaintenanceService
|
||||
/// pour éviter la duplication de code
|
||||
Future<List<MaintenanceModel>> getMaintenancesForEquipment(String equipmentId) async {
|
||||
try {
|
||||
final maintenanceQuery = await _firestore
|
||||
.collection('maintenances')
|
||||
.where('equipmentIds', arrayContains: equipmentId)
|
||||
.orderBy('scheduledDate', descending: true)
|
||||
.get();
|
||||
|
||||
final maintenances = <MaintenanceModel>[];
|
||||
for (var doc in maintenanceQuery.docs) {
|
||||
maintenances.add(
|
||||
MaintenanceModel.fromMap(
|
||||
doc.data(),
|
||||
doc.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return maintenances;
|
||||
// Déléguer au MaintenanceService qui utilise déjà les Cloud Functions
|
||||
final maintenanceService = MaintenanceService();
|
||||
return await maintenanceService.getMaintenancesByEquipment(equipmentId);
|
||||
} catch (e) {
|
||||
print('Error getting maintenances for equipment: $e');
|
||||
rethrow;
|
||||
|
||||
Reference in New Issue
Block a user