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,14 +1,13 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/services/api_service.dart';
/// Service pour calculer dynamiquement le statut réel d'un équipement
/// basé sur les événements en cours
class EquipmentStatusCalculator {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final ApiService _apiService = apiService;
/// Cache des événements pour éviter de multiples requêtes
List<EventModel>? _cachedEvents;
/// Cache des statuts pour éviter de multiples requêtes
Map<String, EquipmentStatus>? _cachedStatuses;
DateTime? _cacheTime;
static const _cacheDuration = Duration(minutes: 1);
@@ -25,205 +24,57 @@ class EquipmentStatusCalculator {
Future<EquipmentStatus> calculateRealStatus(EquipmentModel equipment) async {
print('[StatusCalculator] Calculating status for: ${equipment.id}');
// Si l'équipement est marqué comme perdu ou HS, on garde ce statut
// car c'est une information métier importante
if (equipment.status == EquipmentStatus.lost ||
equipment.status == EquipmentStatus.outOfService) {
print('[StatusCalculator] ${equipment.id} is lost/outOfService -> keeping status');
try {
final statuses = await calculateMultipleStatuses([equipment]);
return statuses[equipment.id] ?? equipment.status;
} catch (e) {
print('[StatusCalculator] Error calculating status: $e');
return equipment.status;
}
// Charger les événements (avec cache)
await _loadEventsIfNeeded();
print('[StatusCalculator] Events loaded: ${_cachedEvents?.length ?? 0}');
// Vérifier si l'équipement est utilisé dans un événement en cours
final isInUse = await _isEquipmentInUse(equipment.id);
print('[StatusCalculator] ${equipment.id} isInUse: $isInUse');
if (isInUse) {
return EquipmentStatus.inUse;
}
// Vérifier si l'équipement est en maintenance
if (equipment.status == EquipmentStatus.maintenance) {
// On pourrait vérifier si la maintenance est toujours valide
// Pour l'instant on garde le statut
return EquipmentStatus.maintenance;
}
// Vérifier si l'équipement est loué
if (equipment.status == EquipmentStatus.rented) {
// On pourrait vérifier une date de retour prévue
// Pour l'instant on garde le statut
return EquipmentStatus.rented;
}
// Par défaut, l'équipement est disponible
print('[StatusCalculator] ${equipment.id} -> AVAILABLE');
return EquipmentStatus.available;
}
/// Calcule les statuts pour une liste d'équipements (optimisé)
Future<Map<String, EquipmentStatus>> calculateMultipleStatuses(
List<EquipmentModel> equipments,
) async {
await _loadEventsIfNeeded();
final statuses = <String, EquipmentStatus>{};
// Trouver tous les équipements en cours d'utilisation
final equipmentIdsInUse = <String>{};
final containerIdsInUse = <String>{};
for (var event in _cachedEvents ?? []) {
// Un équipement est "en prestation" dès que la préparation est complétée
// et jusqu'à ce que le retour soit complété
final isPrepared = event.preparationStatus == PreparationStatus.completed ||
event.preparationStatus == PreparationStatus.completedWithMissing;
final isReturned = event.returnStatus == ReturnStatus.completed ||
event.returnStatus == ReturnStatus.completedWithMissing;
final isInProgress = isPrepared && !isReturned;
if (isInProgress) {
// Ajouter les équipements directs
for (var eq in event.assignedEquipment) {
equipmentIdsInUse.add(eq.equipmentId);
}
// Ajouter les conteneurs
containerIdsInUse.addAll(event.assignedContainers);
}
}
// Récupérer les équipements dans les conteneurs en cours d'utilisation
if (containerIdsInUse.isNotEmpty) {
final containersSnapshot = await _firestore
.collection('containers')
.where(FieldPath.documentId, whereIn: containerIdsInUse.toList())
.get();
for (var doc in containersSnapshot.docs) {
final data = doc.data();
final equipmentIds = List<String>.from(data['equipmentIds'] ?? []);
equipmentIdsInUse.addAll(equipmentIds);
}
}
// Calculer le statut pour chaque équipement
for (var equipment in equipments) {
// Si perdu ou HS, on garde le statut
if (equipment.status == EquipmentStatus.lost ||
equipment.status == EquipmentStatus.outOfService) {
statuses[equipment.id] = equipment.status;
continue;
}
// Si en cours d'utilisation
if (equipmentIdsInUse.contains(equipment.id)) {
statuses[equipment.id] = EquipmentStatus.inUse;
continue;
}
// Si en maintenance ou loué, on garde le statut
if (equipment.status == EquipmentStatus.maintenance ||
equipment.status == EquipmentStatus.rented) {
statuses[equipment.id] = equipment.status;
continue;
}
// Par défaut, disponible
statuses[equipment.id] = EquipmentStatus.available;
}
return statuses;
}
/// Vérifie si un équipement est actuellement en cours d'utilisation
Future<bool> _isEquipmentInUse(String equipmentId) async {
print('[StatusCalculator] Checking if $equipmentId is in use...');
// Vérifier dans les événements directs
for (var event in _cachedEvents ?? []) {
// Un équipement est "en prestation" dès que la préparation est complétée
// et jusqu'à ce que le retour soit complété
final isPrepared = event.preparationStatus == PreparationStatus.completed ||
event.preparationStatus == PreparationStatus.completedWithMissing;
final isReturned = event.returnStatus == ReturnStatus.completed ||
event.returnStatus == ReturnStatus.completedWithMissing;
final isInProgress = isPrepared && !isReturned;
if (!isInProgress) continue;
print('[StatusCalculator] Event ${event.name} is IN PROGRESS (prepared and not returned)');
// Vérifier si l'équipement est directement assigné
if (event.assignedEquipment.any((eq) => eq.equipmentId == equipmentId)) {
print('[StatusCalculator] $equipmentId found DIRECTLY in event ${event.name}');
return true;
}
// Vérifier si l'équipement est dans un conteneur assigné
if (event.assignedContainers.isNotEmpty) {
print('[StatusCalculator] Checking containers for event ${event.name}: ${event.assignedContainers}');
final containersSnapshot = await _firestore
.collection('containers')
.where(FieldPath.documentId, whereIn: event.assignedContainers)
.get();
for (var doc in containersSnapshot.docs) {
final data = doc.data();
final equipmentIds = List<String>.from(data['equipmentIds'] ?? []);
print('[StatusCalculator] Container ${doc.id} contains: $equipmentIds');
if (equipmentIds.contains(equipmentId)) {
print('[StatusCalculator] $equipmentId found in CONTAINER ${doc.id}');
return true;
}
}
}
}
print('[StatusCalculator] $equipmentId is NOT in use');
return false;
}
/// Charge les événements si le cache est expiré
Future<void> _loadEventsIfNeeded() async {
if (_cachedEvents != null &&
_cacheTime != null &&
DateTime.now().difference(_cacheTime!) < _cacheDuration) {
return; // Cache encore valide
}
try {
final eventsSnapshot = await _firestore.collection('events').get();
final equipmentIds = equipments.map((e) => e.id).toList();
_cachedEvents = eventsSnapshot.docs
.map((doc) {
try {
return EventModel.fromMap(doc.data(), doc.id);
} catch (e) {
print('[EquipmentStatusCalculator] Error parsing event ${doc.id}: $e');
return null;
}
})
.whereType<EventModel>()
.where((event) => event.status != EventStatus.canceled) // Ignorer les événements annulés
.toList();
final response = await _apiService.call('calculateEquipmentStatuses', {
'equipmentIds': equipmentIds,
});
final statusesMap = response['statuses'] as Map<String, dynamic>?;
if (statusesMap == null) {
throw Exception('Invalid response from calculateEquipmentStatuses');
}
final statuses = <String, EquipmentStatus>{};
statusesMap.forEach((equipmentId, statusString) {
if (statusString != null) {
statuses[equipmentId] = equipmentStatusFromString(statusString as String);
}
});
// Mise en cache
_cachedStatuses = statuses;
_cacheTime = DateTime.now();
return statuses;
} catch (e) {
print('[EquipmentStatusCalculator] Error loading events: $e');
_cachedEvents = [];
print('[StatusCalculator] Error calculating multiple statuses: $e');
// En cas d'erreur, retourner les statuts actuels
final fallbackStatuses = <String, EquipmentStatus>{};
for (var equipment in equipments) {
fallbackStatuses[equipment.id] = equipment.status;
}
return fallbackStatuses;
}
}
/// Invalide le cache (à appeler après une modification d'événement)
void invalidateCache() {
_cachedEvents = null;
_cachedStatuses = null;
_cacheTime = null;
}