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 11:18:49 +01:00
parent 4545bdba81
commit 4e4573f57b
14 changed files with 1201 additions and 638 deletions

View File

@@ -1,45 +1,21 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/services/equipment_status_calculator.dart';
import 'package:em2rp/services/api_service.dart';
/// Service étendu pour gérer les 4 étapes : Préparation, Chargement, Déchargement, Retour
class EventPreparationServiceExtended {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
CollectionReference get _eventsCollection => _firestore.collection('events');
CollectionReference get _equipmentCollection => _firestore.collection('equipments');
final ApiService _apiService = apiService;
// === CHARGEMENT (LOADING) ===
/// Valider un équipement individuel pour le chargement
Future<void> validateEquipmentLoading(String eventId, String equipmentId) async {
try {
final event = await _getEvent(eventId);
if (event == null) throw Exception('Event not found');
final updatedEquipment = event.assignedEquipment.map((eq) {
if (eq.equipmentId == equipmentId) {
return eq.copyWith(isLoaded: true);
}
return eq;
}).toList();
// Vérifier si tous les équipements sont chargés
final allLoaded = updatedEquipment.every((eq) => eq.isLoaded);
final updateData = <String, dynamic>{
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
};
// Si tous sont chargés, mettre à jour le statut
if (allLoaded) {
updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.completed);
} else {
updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.inProgress);
}
await _eventsCollection.doc(eventId).update(updateData);
await _apiService.call('validateEquipmentLoading', {
'eventId': eventId,
'equipmentId': equipmentId,
});
} catch (e) {
print('Error validating equipment loading: $e');
rethrow;
@@ -49,16 +25,8 @@ class EventPreparationServiceExtended {
/// Valider tous les équipements pour le chargement
Future<void> validateAllLoading(String eventId) async {
try {
final event = await _getEvent(eventId);
if (event == null) throw Exception('Event not found');
final updatedEquipment = event.assignedEquipment.map((eq) {
return eq.copyWith(isLoaded: true);
}).toList();
await _eventsCollection.doc(eventId).update({
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
'loadingStatus': loadingStatusToString(LoadingStatus.completed),
await _apiService.call('validateAllLoading', {
'eventId': eventId,
});
// Invalider le cache des statuts d'équipement
@@ -74,31 +42,10 @@ class EventPreparationServiceExtended {
/// Valider un équipement individuel pour le déchargement
Future<void> validateEquipmentUnloading(String eventId, String equipmentId) async {
try {
final event = await _getEvent(eventId);
if (event == null) throw Exception('Event not found');
final updatedEquipment = event.assignedEquipment.map((eq) {
if (eq.equipmentId == equipmentId) {
return eq.copyWith(isUnloaded: true);
}
return eq;
}).toList();
// Vérifier si tous les équipements sont déchargés
final allUnloaded = updatedEquipment.every((eq) => eq.isUnloaded);
final updateData = <String, dynamic>{
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
};
// Si tous sont déchargés, mettre à jour le statut
if (allUnloaded) {
updateData['unloadingStatus'] = unloadingStatusToString(UnloadingStatus.completed);
} else {
updateData['unloadingStatus'] = unloadingStatusToString(UnloadingStatus.inProgress);
}
await _eventsCollection.doc(eventId).update(updateData);
await _apiService.call('validateEquipmentUnloading', {
'eventId': eventId,
'equipmentId': equipmentId,
});
} catch (e) {
print('Error validating equipment unloading: $e');
rethrow;
@@ -108,16 +55,8 @@ class EventPreparationServiceExtended {
/// Valider tous les équipements pour le déchargement
Future<void> validateAllUnloading(String eventId) async {
try {
final event = await _getEvent(eventId);
if (event == null) throw Exception('Event not found');
final updatedEquipment = event.assignedEquipment.map((eq) {
return eq.copyWith(isUnloaded: true);
}).toList();
await _eventsCollection.doc(eventId).update({
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
'unloadingStatus': unloadingStatusToString(UnloadingStatus.completed),
await _apiService.call('validateAllUnloading', {
'eventId': eventId,
});
// Invalider le cache des statuts d'équipement
@@ -133,26 +72,13 @@ class EventPreparationServiceExtended {
/// Valider préparation ET chargement en même temps
Future<void> validateAllPreparationAndLoading(String eventId) async {
try {
final event = await _getEvent(eventId);
if (event == null) throw Exception('Event not found');
// Note: On pourrait créer une fonction cloud dédiée pour ça,
// mais pour l'instant on appelle les deux séquentiellement
await _apiService.call('validateAllPreparation', {'eventId': eventId});
await _apiService.call('validateAllLoading', {'eventId': eventId});
final updatedEquipment = event.assignedEquipment.map((eq) {
return eq.copyWith(isPrepared: true, isLoaded: true);
}).toList();
await _eventsCollection.doc(eventId).update({
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
'preparationStatus': preparationStatusToString(PreparationStatus.completed),
'loadingStatus': loadingStatusToString(LoadingStatus.completed),
});
// Mettre à jour le statut des équipements
for (var equipment in event.assignedEquipment) {
final doc = await _equipmentCollection.doc(equipment.equipmentId).get();
if (doc.exists) {
await _updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse);
}
}
// Invalider le cache
EquipmentStatusCalculator.invalidateGlobalCache();
} catch (e) {
print('Error validating all preparation and loading: $e');
rethrow;
@@ -167,81 +93,20 @@ class EventPreparationServiceExtended {
Map<String, int>? returnedQuantities,
) async {
try {
final event = await _getEvent(eventId);
if (event == null) throw Exception('Event not found');
final updatedEquipment = event.assignedEquipment.map((eq) {
final returnedQty = returnedQuantities?[eq.equipmentId] ??
eq.returnedQuantity ??
eq.quantity;
return eq.copyWith(
isUnloaded: true,
isReturned: true,
returnedQuantity: returnedQty,
);
}).toList();
await _eventsCollection.doc(eventId).update({
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
'unloadingStatus': unloadingStatusToString(UnloadingStatus.completed),
'returnStatus': returnStatusToString(ReturnStatus.completed),
// Note: On pourrait créer une fonction cloud dédiée pour ça,
// mais pour l'instant on appelle les deux séquentiellement
await _apiService.call('validateAllUnloading', {'eventId': eventId});
await _apiService.call('validateAllReturn', {
'eventId': eventId,
if (returnedQuantities != null) 'returnedQuantities': returnedQuantities,
});
// Mettre à jour les statuts et stocks
for (var equipment in updatedEquipment) {
final equipmentDoc = await _equipmentCollection.doc(equipment.equipmentId).get();
if (equipmentDoc.exists) {
final equipmentData = EquipmentModel.fromMap(
equipmentDoc.data() as Map<String, dynamic>,
equipmentDoc.id,
);
if (!equipmentData.hasQuantity) {
await _updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available);
}
if (equipmentData.hasQuantity && equipment.returnedQuantity != null) {
final currentAvailable = equipmentData.availableQuantity ?? 0;
await _equipmentCollection.doc(equipment.equipmentId).update({
'availableQuantity': currentAvailable + equipment.returnedQuantity!,
'updatedAt': Timestamp.fromDate(DateTime.now()),
});
}
}
}
// Invalider le cache
EquipmentStatusCalculator.invalidateGlobalCache();
} catch (e) {
print('Error validating all unloading and return: $e');
rethrow;
}
}
// === HELPERS ===
Future<void> _updateEquipmentStatus(String equipmentId, EquipmentStatus status) async {
try {
final doc = await _equipmentCollection.doc(equipmentId).get();
if (!doc.exists) return;
await _equipmentCollection.doc(equipmentId).update({
'status': equipmentStatusToString(status),
'updatedAt': Timestamp.fromDate(DateTime.now()),
});
} catch (e) {
print('Error updating equipment status: $e');
}
}
Future<EventModel?> _getEvent(String eventId) async {
try {
final doc = await _eventsCollection.doc(eventId).get();
if (doc.exists) {
return EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
}
return null;
} catch (e) {
print('Error getting event: $e');
rethrow;
}
}
}