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:
ElPoyo
2026-01-14 12:05:03 +01:00
parent 4e4573f57b
commit fb3f41df4d
10 changed files with 915 additions and 858 deletions

View File

@@ -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;