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.
277 lines
8.4 KiB
Dart
277 lines
8.4 KiB
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 ApiService _apiService = apiService;
|
|
final DataService _dataService = DataService(apiService);
|
|
|
|
// ============================================================================
|
|
// CRUD Operations - Utilise le backend sécurisé
|
|
// ============================================================================
|
|
|
|
/// Créer un nouveau container (via Cloud Function)
|
|
Future<void> createContainer(ContainerModel container) async {
|
|
try {
|
|
await _apiService.call('createContainer', container.toMap()..['id'] = container.id);
|
|
} catch (e) {
|
|
print('Error creating container: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Mettre à jour un container (via Cloud Function)
|
|
Future<void> updateContainer(String id, Map<String, dynamic> data) async {
|
|
try {
|
|
await _apiService.call('updateContainer', {
|
|
'containerId': id,
|
|
'data': data,
|
|
});
|
|
} catch (e) {
|
|
print('Error updating container: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Supprimer un container (via Cloud Function)
|
|
Future<void> deleteContainer(String id) async {
|
|
try {
|
|
await _apiService.call('deleteContainer', {'containerId': id});
|
|
// Note: La Cloud Function gère maintenant la mise à jour des équipements
|
|
} catch (e) {
|
|
print('Error deleting container: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Récupérer un container par ID
|
|
Future<ContainerModel?> getContainerById(String id) async {
|
|
try {
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// Récupérer tous les containers
|
|
Future<List<ContainerModel>> getContainers({
|
|
ContainerType? type,
|
|
EquipmentStatus? status,
|
|
String? searchQuery,
|
|
}) async {
|
|
try {
|
|
final containersData = await _dataService.getContainers();
|
|
|
|
var containerList = containersData
|
|
.map((data) => ContainerModel.fromMap(data, data['id'] as String))
|
|
.toList();
|
|
|
|
// Filtres côté client
|
|
if (type != null) {
|
|
containerList = containerList
|
|
.where((c) => c.type == type)
|
|
.toList();
|
|
}
|
|
|
|
if (status != null) {
|
|
containerList = containerList
|
|
.where((c) => c.status == status)
|
|
.toList();
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// Ajouter un équipement à un container
|
|
Future<Map<String, dynamic>> addEquipmentToContainer({
|
|
required String containerId,
|
|
required String equipmentId,
|
|
String? userId,
|
|
}) async {
|
|
try {
|
|
final response = await _apiService.call('addEquipmentToContainer', {
|
|
'containerId': containerId,
|
|
'equipmentId': equipmentId,
|
|
if (userId != null) 'userId': userId,
|
|
});
|
|
|
|
return {
|
|
'success': response['success'] ?? false,
|
|
'message': response['message'] ?? '',
|
|
'warnings': response['warnings'],
|
|
};
|
|
} catch (e) {
|
|
print('Error adding equipment to container: $e');
|
|
return {'success': false, 'message': 'Erreur: $e'};
|
|
}
|
|
}
|
|
|
|
/// Retirer un équipement d'un container
|
|
Future<void> removeEquipmentFromContainer({
|
|
required String containerId,
|
|
required String equipmentId,
|
|
String? userId,
|
|
}) async {
|
|
try {
|
|
await _apiService.call('removeEquipmentFromContainer', {
|
|
'containerId': containerId,
|
|
'equipmentId': equipmentId,
|
|
if (userId != null) 'userId': userId,
|
|
});
|
|
} catch (e) {
|
|
print('Error removing equipment from container: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Vérifier la disponibilité d'un container et de son contenu pour un événement
|
|
Future<Map<String, dynamic>> checkContainerAvailability({
|
|
required String containerId,
|
|
required DateTime startDate,
|
|
required DateTime endDate,
|
|
String? excludeEventId,
|
|
}) async {
|
|
try {
|
|
final container = await getContainerById(containerId);
|
|
if (container == null) {
|
|
return {'available': false, 'message': 'Container non trouvé'};
|
|
}
|
|
|
|
// Vérifier le statut du container
|
|
if (container.status != EquipmentStatus.available) {
|
|
return {
|
|
'available': false,
|
|
'message': 'Container ${container.name} n\'est pas disponible (statut: ${container.status})',
|
|
};
|
|
}
|
|
|
|
// Vérifier la disponibilité de chaque équipement dans le container
|
|
List<String> unavailableEquipment = [];
|
|
|
|
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})');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (unavailableEquipment.isNotEmpty) {
|
|
return {
|
|
'available': false,
|
|
'message': 'Certains équipements ne sont pas disponibles',
|
|
'unavailableItems': unavailableEquipment,
|
|
};
|
|
}
|
|
|
|
return {'available': true, 'message': 'Container et tout son contenu disponibles'};
|
|
} catch (e) {
|
|
print('Error checking container availability: $e');
|
|
return {'available': false, 'message': 'Erreur: $e'};
|
|
}
|
|
}
|
|
|
|
/// Récupérer les équipements d'un container
|
|
Future<List<EquipmentModel>> getContainerEquipment(String containerId) async {
|
|
try {
|
|
final container = await getContainerById(containerId);
|
|
if (container == null) return [];
|
|
|
|
if (container.equipmentIds.isEmpty) return [];
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// Trouver tous les containers contenant un équipement spécifique
|
|
Future<List<ContainerModel>> findContainersWithEquipment(String equipmentId) async {
|
|
try {
|
|
final containersData = await _dataService.getContainersByEquipment(equipmentId);
|
|
|
|
return containersData
|
|
.map((data) => ContainerModel.fromMap(data, data['id'] as String))
|
|
.toList();
|
|
} catch (e) {
|
|
print('Error finding containers with equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Ajouter une entrée d'historique
|
|
Future<void> _addHistoryEntry({
|
|
required String containerId,
|
|
required String action,
|
|
String? equipmentId,
|
|
String? previousValue,
|
|
String? newValue,
|
|
String? userId,
|
|
}) async {
|
|
try {
|
|
final container = await getContainerById(containerId);
|
|
if (container == null) return;
|
|
|
|
final entry = ContainerHistoryEntry(
|
|
timestamp: DateTime.now(),
|
|
action: action,
|
|
equipmentId: equipmentId,
|
|
previousValue: previousValue,
|
|
newValue: newValue,
|
|
userId: userId,
|
|
);
|
|
|
|
final updatedHistory = [...container.history, entry];
|
|
|
|
// Limiter l'historique aux 100 dernières entrées
|
|
final limitedHistory = updatedHistory.length > 100
|
|
? updatedHistory.sublist(updatedHistory.length - 100)
|
|
: updatedHistory;
|
|
|
|
await updateContainer(containerId, {
|
|
'history': limitedHistory.map((e) => e.toMap()).toList(),
|
|
});
|
|
} catch (e) {
|
|
print('Error adding history entry: $e');
|
|
// Ne pas throw pour éviter de bloquer l'opération principale
|
|
}
|
|
}
|
|
|
|
/// Vérifier si un ID de container existe déjà
|
|
Future<bool> checkContainerIdExists(String id) async {
|
|
try {
|
|
final container = await getContainerById(id);
|
|
return container != null;
|
|
} catch (e) {
|
|
print('Error checking container ID: $e');
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|