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,16 +1,9 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/maintenance_model.dart';
import 'package:em2rp/models/alert_model.dart';
import 'package:em2rp/services/api_service.dart';
class MaintenanceService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final ApiService _apiService = apiService;
// Collection references
CollectionReference get _maintenancesCollection => _firestore.collection('maintenances');
CollectionReference get _equipmentCollection => _firestore.collection('equipments');
CollectionReference get _alertsCollection => _firestore.collection('alerts');
// ============================================================================
// CRUD Operations - Utilise le backend sécurisé
@@ -56,54 +49,54 @@ class MaintenanceService {
/// Récupérer une maintenance par ID
Future<MaintenanceModel?> getMaintenanceById(String id) async {
try {
final doc = await _maintenancesCollection.doc(id).get();
if (doc.exists) {
return MaintenanceModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
}
return null;
final response = await _apiService.call('getMaintenances', {
'maintenanceId': id,
});
final maintenances = (response['maintenances'] as List?)
?.map((m) => MaintenanceModel.fromMap(m as Map<String, dynamic>, m['id'] as String))
.toList();
return maintenances?.firstWhere(
(m) => m.id == id,
orElse: () => throw Exception('Maintenance not found'),
);
} catch (e) {
print('Error getting maintenance: $e');
rethrow;
return null;
}
}
/// Récupérer l'historique des maintenances pour un équipement
Stream<List<MaintenanceModel>> getMaintenances(String equipmentId) {
Future<List<MaintenanceModel>> getMaintenancesByEquipment(String equipmentId) async {
try {
return _maintenancesCollection
.where('equipmentIds', arrayContains: equipmentId)
.orderBy('scheduledDate', descending: true)
.snapshots()
.map((snapshot) {
return snapshot.docs
.map((doc) => MaintenanceModel.fromMap(
doc.data() as Map<String, dynamic>,
doc.id,
))
.toList();
final response = await _apiService.call('getMaintenances', {
'equipmentId': equipmentId,
});
final maintenances = (response['maintenances'] as List?)
?.map((m) => MaintenanceModel.fromMap(m as Map<String, dynamic>, m['id'] as String))
.toList() ?? [];
return maintenances;
} catch (e) {
print('Error streaming maintenances: $e');
print('Error getting maintenances: $e');
rethrow;
}
}
/// Récupérer toutes les maintenances
Stream<List<MaintenanceModel>> getAllMaintenances() {
Future<List<MaintenanceModel>> getAllMaintenances() async {
try {
return _maintenancesCollection
.orderBy('scheduledDate', descending: true)
.snapshots()
.map((snapshot) {
return snapshot.docs
.map((doc) => MaintenanceModel.fromMap(
doc.data() as Map<String, dynamic>,
doc.id,
))
.toList();
});
final response = await _apiService.call('getMaintenances', {});
final maintenances = (response['maintenances'] as List?)
?.map((m) => MaintenanceModel.fromMap(m as Map<String, dynamic>, m['id'] as String))
.toList() ?? [];
return maintenances;
} catch (e) {
print('Error streaming all maintenances: $e');
print('Error getting all maintenances: $e');
rethrow;
}
}
@@ -111,30 +104,11 @@ class MaintenanceService {
/// Marquer une maintenance comme complétée
Future<void> completeMaintenance(String id, {String? performedBy, double? cost}) async {
try {
final updateData = <String, dynamic>{
'completedDate': Timestamp.fromDate(DateTime.now()),
'updatedAt': Timestamp.fromDate(DateTime.now()),
};
if (performedBy != null) {
updateData['performedBy'] = performedBy;
}
if (cost != null) {
updateData['cost'] = cost;
}
await updateMaintenance(id, updateData);
// Mettre à jour la date de dernière maintenance des équipements
final maintenance = await getMaintenanceById(id);
if (maintenance != null) {
for (String equipmentId in maintenance.equipmentIds) {
await _equipmentCollection.doc(equipmentId).update({
'lastMaintenanceDate': Timestamp.fromDate(DateTime.now()),
});
}
}
await _apiService.call('completeMaintenance', {
'maintenanceId': id,
if (performedBy != null) 'performedBy': performedBy,
if (cost != null) 'cost': cost,
});
} catch (e) {
print('Error completing maintenance: $e');
rethrow;
@@ -144,73 +118,10 @@ class MaintenanceService {
/// Vérifier les maintenances à venir et créer des alertes
Future<void> checkUpcomingMaintenances() async {
try {
final sevenDaysFromNow = DateTime.now().add(const Duration(days: 7));
// Récupérer les maintenances planifiées dans les 7 prochains jours
final maintenancesQuery = await _maintenancesCollection
.where('scheduledDate', isLessThanOrEqualTo: Timestamp.fromDate(sevenDaysFromNow))
.where('completedDate', isNull: true)
.get();
for (var doc in maintenancesQuery.docs) {
final maintenance = MaintenanceModel.fromMap(
doc.data() as Map<String, dynamic>,
doc.id,
);
for (String equipmentId in maintenance.equipmentIds) {
await _createMaintenanceAlert(equipmentId, maintenance);
}
}
await _apiService.call('checkUpcomingMaintenances', {});
} catch (e) {
print('Error checking upcoming maintenances: $e');
rethrow;
}
}
/// Créer une alerte de maintenance à venir
Future<void> _createMaintenanceAlert(String equipmentId, MaintenanceModel maintenance) async {
try {
// Vérifier si une alerte existe déjà
final existingAlerts = await _alertsCollection
.where('equipmentId', isEqualTo: equipmentId)
.where('type', isEqualTo: alertTypeToString(AlertType.maintenanceDue))
.where('isRead', isEqualTo: false)
.get();
// Vérifier si l'alerte concerne la même maintenance
bool alertExists = false;
for (var alertDoc in existingAlerts.docs) {
final alertData = alertDoc.data() as Map<String, dynamic>;
if (alertData['message']?.contains(maintenance.name) ?? false) {
alertExists = true;
break;
}
}
if (!alertExists) {
// Récupérer l'équipement pour le nom
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
String equipmentName = equipmentId;
if (equipmentDoc.exists) {
final equipmentData = equipmentDoc.data() as Map<String, dynamic>;
equipmentName = equipmentData['name'] ?? equipmentId;
}
final daysUntil = maintenance.scheduledDate.difference(DateTime.now()).inDays;
final alert = AlertModel(
id: _alertsCollection.doc().id,
type: AlertType.maintenanceDue,
message: 'Maintenance "${maintenance.name}" prévue dans $daysUntil jour(s) pour $equipmentName',
equipmentId: equipmentId,
createdAt: DateTime.now(),
);
await _alertsCollection.doc(alert.id).set(alert.toMap());
}
} catch (e) {
print('Error creating maintenance alert: $e');
rethrow;
}
}
}