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,15 +1,11 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/services/api_service.dart';
import 'package:em2rp/services/data_service.dart';
class ContainerService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final ApiService _apiService = apiService;
// Collection references
CollectionReference get _containersCollection => _firestore.collection('containers');
CollectionReference get _equipmentCollection => _firestore.collection('equipments');
final DataService _dataService = DataService(apiService);
// ============================================================================
// CRUD Operations - Utilise le backend sécurisé
@@ -52,11 +48,10 @@ class ContainerService {
/// Récupérer un container par ID
Future<ContainerModel?> getContainerById(String id) async {
try {
final doc = await _containersCollection.doc(id).get();
if (doc.exists) {
return ContainerModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
}
return null;
final containersData = await _dataService.getContainersByIds([id]);
if (containersData.isEmpty) return null;
return ContainerModel.fromMap(containersData.first, id);
} catch (e) {
print('Error getting container: $e');
rethrow;
@@ -64,40 +59,40 @@ class ContainerService {
}
/// Récupérer tous les containers
Stream<List<ContainerModel>> getContainers({
Future<List<ContainerModel>> getContainers({
ContainerType? type,
EquipmentStatus? status,
String? searchQuery,
}) {
}) async {
try {
Query query = _containersCollection;
final containersData = await _dataService.getContainers();
// Filtre par type
var containerList = containersData
.map((data) => ContainerModel.fromMap(data, data['id'] as String))
.toList();
// Filtres côté client
if (type != null) {
query = query.where('type', isEqualTo: containerTypeToString(type));
}
// Filtre par statut
if (status != null) {
query = query.where('status', isEqualTo: equipmentStatusToString(status));
}
return query.snapshots().map((snapshot) {
List<ContainerModel> containerList = snapshot.docs
.map((doc) => ContainerModel.fromMap(doc.data() as Map<String, dynamic>, doc.id))
containerList = containerList
.where((c) => c.type == type)
.toList();
}
// Filtre par recherche texte (côté client)
if (searchQuery != null && searchQuery.isNotEmpty) {
final lowerSearch = searchQuery.toLowerCase();
containerList = containerList.where((container) {
return container.name.toLowerCase().contains(lowerSearch) ||
container.id.toLowerCase().contains(lowerSearch);
}).toList();
}
if (status != null) {
containerList = containerList
.where((c) => c.status == status)
.toList();
}
return containerList;
});
if (searchQuery != null && searchQuery.isNotEmpty) {
final lowerSearch = searchQuery.toLowerCase();
containerList = containerList.where((container) {
return container.name.toLowerCase().contains(lowerSearch) ||
container.id.toLowerCase().contains(lowerSearch);
}).toList();
}
return containerList;
} catch (e) {
print('Error getting containers: $e');
rethrow;
@@ -111,67 +106,16 @@ class ContainerService {
String? userId,
}) async {
try {
// Récupérer le container
final container = await getContainerById(containerId);
if (container == null) {
return {'success': false, 'message': 'Container non trouvé'};
}
// Vérifier si l'équipement n'est pas déjà dans ce container
if (container.equipmentIds.contains(equipmentId)) {
return {'success': false, 'message': 'Cet équipement est déjà dans ce container'};
}
// Récupérer l'équipement pour vérifier s'il est déjà dans d'autres containers
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
if (!equipmentDoc.exists) {
return {'success': false, 'message': 'Équipement non trouvé'};
}
final equipment = EquipmentModel.fromMap(
equipmentDoc.data() as Map<String, dynamic>,
equipmentDoc.id,
);
// Avertir si l'équipement est déjà dans d'autres containers
List<String> otherContainers = [];
if (equipment.parentBoxIds.isNotEmpty) {
for (final boxId in equipment.parentBoxIds) {
final box = await getContainerById(boxId);
if (box != null) {
otherContainers.add(box.name);
}
}
}
// Mettre à jour le container
final updatedEquipmentIds = [...container.equipmentIds, equipmentId];
await updateContainer(containerId, {
'equipmentIds': updatedEquipmentIds,
final response = await _apiService.call('addEquipmentToContainer', {
'containerId': containerId,
'equipmentId': equipmentId,
if (userId != null) 'userId': userId,
});
// Mettre à jour l'équipement
final updatedParentBoxIds = [...equipment.parentBoxIds, containerId];
await _equipmentCollection.doc(equipmentId).update({
'parentBoxIds': updatedParentBoxIds,
'updatedAt': Timestamp.fromDate(DateTime.now()),
});
// Ajouter une entrée dans l'historique
await _addHistoryEntry(
containerId: containerId,
action: 'equipment_added',
equipmentId: equipmentId,
newValue: equipmentId,
userId: userId,
);
return {
'success': true,
'message': 'Équipement ajouté avec succès',
'warnings': otherContainers.isNotEmpty
? 'Attention : cet équipement est également dans les boites suivants : ${otherContainers.join(", ")}'
: null,
'success': response['success'] ?? false,
'message': response['message'] ?? '',
'warnings': response['warnings'],
};
} catch (e) {
print('Error adding equipment to container: $e');
@@ -186,38 +130,11 @@ class ContainerService {
String? userId,
}) async {
try {
// Récupérer le container
final container = await getContainerById(containerId);
if (container == null) throw Exception('Container non trouvé');
// Mettre à jour le container
final updatedEquipmentIds = container.equipmentIds.where((id) => id != equipmentId).toList();
await updateContainer(containerId, {
'equipmentIds': updatedEquipmentIds,
await _apiService.call('removeEquipmentFromContainer', {
'containerId': containerId,
'equipmentId': equipmentId,
if (userId != null) 'userId': userId,
});
// Mettre à jour l'équipement
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
if (equipmentDoc.exists) {
final equipment = EquipmentModel.fromMap(
equipmentDoc.data() as Map<String, dynamic>,
equipmentDoc.id,
);
final updatedParentBoxIds = equipment.parentBoxIds.where((id) => id != containerId).toList();
await _equipmentCollection.doc(equipmentId).update({
'parentBoxIds': updatedParentBoxIds,
'updatedAt': Timestamp.fromDate(DateTime.now()),
});
}
// Ajouter une entrée dans l'historique
await _addHistoryEntry(
containerId: containerId,
action: 'equipment_removed',
equipmentId: equipmentId,
previousValue: equipmentId,
userId: userId,
);
} catch (e) {
print('Error removing equipment from container: $e');
rethrow;
@@ -247,14 +164,12 @@ class ContainerService {
// Vérifier la disponibilité de chaque équipement dans le container
List<String> unavailableEquipment = [];
for (final equipmentId in container.equipmentIds) {
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
if (equipmentDoc.exists) {
final equipment = EquipmentModel.fromMap(
equipmentDoc.data() as Map<String, dynamic>,
equipmentDoc.id,
);
if (container.equipmentIds.isNotEmpty) {
final equipmentsData = await _dataService.getEquipmentsByIds(container.equipmentIds);
for (var data in equipmentsData) {
final equipment = EquipmentModel.fromMap(data, data['id'] as String);
if (equipment.status != EquipmentStatus.available) {
unavailableEquipment.add('${equipment.name} (${equipment.status})');
}
@@ -282,15 +197,13 @@ class ContainerService {
final container = await getContainerById(containerId);
if (container == null) return [];
List<EquipmentModel> equipment = [];
for (final equipmentId in container.equipmentIds) {
final doc = await _equipmentCollection.doc(equipmentId).get();
if (doc.exists) {
equipment.add(EquipmentModel.fromMap(doc.data() as Map<String, dynamic>, doc.id));
}
}
if (container.equipmentIds.isEmpty) return [];
return equipment;
final equipmentsData = await _dataService.getEquipmentsByIds(container.equipmentIds);
return equipmentsData
.map((data) => EquipmentModel.fromMap(data, data['id'] as String))
.toList();
} catch (e) {
print('Error getting container equipment: $e');
rethrow;
@@ -300,12 +213,10 @@ class ContainerService {
/// Trouver tous les containers contenant un équipement spécifique
Future<List<ContainerModel>> findContainersWithEquipment(String equipmentId) async {
try {
final snapshot = await _containersCollection
.where('equipmentIds', arrayContains: equipmentId)
.get();
final containersData = await _dataService.getContainersByEquipment(equipmentId);
return snapshot.docs
.map((doc) => ContainerModel.fromMap(doc.data() as Map<String, dynamic>, doc.id))
return containersData
.map((data) => ContainerModel.fromMap(data, data['id'] as String))
.toList();
} catch (e) {
print('Error finding containers with equipment: $e');
@@ -354,8 +265,8 @@ class ContainerService {
/// Vérifier si un ID de container existe déjà
Future<bool> checkContainerIdExists(String id) async {
try {
final doc = await _containersCollection.doc(id).get();
return doc.exists;
final container = await getContainerById(id);
return container != null;
} catch (e) {
print('Error checking container ID: $e');
return false;