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.
366 lines
11 KiB
Dart
366 lines
11 KiB
Dart
import 'package:em2rp/models/equipment_model.dart';
|
|
import 'package:em2rp/models/maintenance_model.dart';
|
|
import 'package:em2rp/models/container_model.dart';
|
|
import 'package:em2rp/services/api_service.dart';
|
|
import 'package:em2rp/services/data_service.dart';
|
|
import 'package:em2rp/services/maintenance_service.dart';
|
|
|
|
class EquipmentService {
|
|
final ApiService _apiService = apiService;
|
|
final DataService _dataService = DataService(apiService);
|
|
|
|
// ============================================================================
|
|
// CRUD Operations - Utilise le backend sécurisé
|
|
// ============================================================================
|
|
|
|
/// Créer un nouvel équipement (via Cloud Function)
|
|
Future<void> createEquipment(EquipmentModel equipment) async {
|
|
try {
|
|
await _apiService.call('createEquipment', equipment.toMap()..['id'] = equipment.id);
|
|
} catch (e) {
|
|
print('Error creating equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Mettre à jour un équipement (via Cloud Function)
|
|
Future<void> updateEquipment(String id, Map<String, dynamic> data) async {
|
|
try {
|
|
await _apiService.call('updateEquipment', {
|
|
'equipmentId': id,
|
|
'data': data,
|
|
});
|
|
} catch (e) {
|
|
print('Error updating equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Supprimer un équipement (via Cloud Function)
|
|
Future<void> deleteEquipment(String id) async {
|
|
try {
|
|
await _apiService.call('deleteEquipment', {'equipmentId': id});
|
|
} catch (e) {
|
|
print('Error deleting equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// READ Operations - Utilise Firestore streams (temps réel)
|
|
// ============================================================================
|
|
|
|
/// Récupérer un équipement par ID
|
|
Future<EquipmentModel?> getEquipmentById(String id) async {
|
|
try {
|
|
final equipmentsData = await _dataService.getEquipmentsByIds([id]);
|
|
if (equipmentsData.isEmpty) return null;
|
|
|
|
return EquipmentModel.fromMap(equipmentsData.first, id);
|
|
} catch (e) {
|
|
print('Error getting equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Récupérer les équipements avec filtres
|
|
Future<List<EquipmentModel>> getEquipment({
|
|
EquipmentCategory? category,
|
|
EquipmentStatus? status,
|
|
String? model,
|
|
String? searchQuery,
|
|
}) async {
|
|
try {
|
|
final equipmentsData = await _dataService.getEquipments();
|
|
|
|
var equipmentList = equipmentsData
|
|
.map((data) => EquipmentModel.fromMap(data, data['id'] as String))
|
|
.toList();
|
|
|
|
// Filtres côté client
|
|
if (category != null) {
|
|
equipmentList = equipmentList
|
|
.where((e) => e.category == category)
|
|
.toList();
|
|
}
|
|
|
|
if (status != null) {
|
|
equipmentList = equipmentList
|
|
.where((e) => e.status == status)
|
|
.toList();
|
|
}
|
|
|
|
if (model != null && model.isNotEmpty) {
|
|
equipmentList = equipmentList
|
|
.where((e) => e.model == model)
|
|
.toList();
|
|
}
|
|
|
|
if (searchQuery != null && searchQuery.isNotEmpty) {
|
|
final lowerSearch = searchQuery.toLowerCase();
|
|
equipmentList = equipmentList.where((equipment) {
|
|
return equipment.name.toLowerCase().contains(lowerSearch) ||
|
|
(equipment.model?.toLowerCase().contains(lowerSearch) ?? false) ||
|
|
equipment.id.toLowerCase().contains(lowerSearch);
|
|
}).toList();
|
|
}
|
|
|
|
return equipmentList;
|
|
} catch (e) {
|
|
print('Error getting equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Availability & Stock Management - Logique métier côté client
|
|
// ============================================================================
|
|
|
|
/// Vérifier la disponibilité d'un équipement pour une période donnée
|
|
Future<List<Map<String, dynamic>>> checkAvailability(
|
|
String equipmentId,
|
|
DateTime startDate,
|
|
DateTime endDate,
|
|
) async {
|
|
try {
|
|
final response = await _apiService.call('checkEquipmentAvailability', {
|
|
'equipmentId': equipmentId,
|
|
'startDate': startDate.toIso8601String(),
|
|
'endDate': endDate.toIso8601String(),
|
|
});
|
|
|
|
final conflicts = (response['conflicts'] as List?)
|
|
?.map((c) => c as Map<String, dynamic>)
|
|
.toList() ?? [];
|
|
|
|
return conflicts;
|
|
} catch (e) {
|
|
print('Error checking availability: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Trouver des alternatives (même modèle) disponibles
|
|
Future<List<EquipmentModel>> findAlternatives(
|
|
String model,
|
|
DateTime startDate,
|
|
DateTime endDate,
|
|
) async {
|
|
try {
|
|
final response = await _apiService.call('findAlternativeEquipment', {
|
|
'model': model,
|
|
'startDate': startDate.toIso8601String(),
|
|
'endDate': endDate.toIso8601String(),
|
|
});
|
|
|
|
final alternatives = (response['alternatives'] as List?)
|
|
?.map((a) => EquipmentModel.fromMap(a as Map<String, dynamic>, a['id'] as String))
|
|
.toList() ?? [];
|
|
|
|
return alternatives;
|
|
} catch (e) {
|
|
print('Error finding alternatives: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Mettre à jour le stock d'un consommable/câble
|
|
Future<void> updateStock(String id, int quantityChange) async {
|
|
try {
|
|
final equipment = await getEquipmentById(id);
|
|
if (equipment == null) {
|
|
throw Exception('Equipment not found');
|
|
}
|
|
|
|
if (!equipment.hasQuantity) {
|
|
throw Exception('Equipment does not have quantity tracking');
|
|
}
|
|
|
|
final newAvailableQuantity = (equipment.availableQuantity ?? 0) + quantityChange;
|
|
|
|
await updateEquipment(id, {
|
|
'availableQuantity': newAvailableQuantity,
|
|
});
|
|
|
|
// Vérifier si le seuil critique est atteint
|
|
if (equipment.criticalThreshold != null &&
|
|
newAvailableQuantity <= equipment.criticalThreshold!) {
|
|
await _createLowStockAlert(equipment);
|
|
}
|
|
} catch (e) {
|
|
print('Error updating stock: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Vérifier les stocks critiques et créer des alertes
|
|
Future<void> checkCriticalStock() async {
|
|
try {
|
|
final equipmentsData = await _dataService.getEquipments();
|
|
|
|
for (var data in equipmentsData) {
|
|
final equipment = EquipmentModel.fromMap(data, data['id'] as String);
|
|
|
|
// Filtrer uniquement les consommables et câbles
|
|
if ((equipment.category == EquipmentCategory.consumable ||
|
|
equipment.category == EquipmentCategory.cable) &&
|
|
equipment.isCriticalStock) {
|
|
await _createLowStockAlert(equipment);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('Error checking critical stock: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Créer une alerte de stock faible
|
|
Future<void> _createLowStockAlert(EquipmentModel equipment) async {
|
|
try {
|
|
// Note: Cette fonction pourrait utiliser une Cloud Function dédiée dans le futur
|
|
// Pour l'instant, on utilise l'API directement pour éviter de créer trop de fonctions
|
|
// Cette méthode est appelée rarement et en arrière-plan
|
|
await _apiService.call('createAlert', {
|
|
'type': 'LOW_STOCK',
|
|
'title': 'Stock critique',
|
|
'message': 'Stock critique pour ${equipment.name} (${equipment.model ?? ""}): ${equipment.availableQuantity}/${equipment.criticalThreshold}',
|
|
'severity': 'HIGH',
|
|
'equipmentId': equipment.id,
|
|
});
|
|
} catch (e) {
|
|
print('Error creating low stock alert: $e');
|
|
// Ne pas rethrow pour ne pas bloquer le processus
|
|
}
|
|
}
|
|
|
|
/// Générer les données du QR code (ID de l'équipement)
|
|
String generateQRCodeData(String equipmentId) {
|
|
// Pour l'instant, on retourne simplement l'ID
|
|
// On pourrait aussi générer une URL complète : https://app.em2events.fr/equipment/$equipmentId
|
|
return equipmentId;
|
|
}
|
|
|
|
/// Récupérer tous les modèles uniques (pour l'indexation/autocomplete)
|
|
Future<List<String>> getAllModels() async {
|
|
try {
|
|
final equipmentsData = await _dataService.getEquipments();
|
|
final models = <String>{};
|
|
|
|
for (var data in equipmentsData) {
|
|
final model = data['model'] as String?;
|
|
if (model != null && model.isNotEmpty) {
|
|
models.add(model);
|
|
}
|
|
}
|
|
|
|
return models.toList()..sort();
|
|
} catch (e) {
|
|
print('Error getting all models: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Récupérer toutes les marques uniques (pour l'indexation/autocomplete)
|
|
Future<List<String>> getAllBrands() async {
|
|
try {
|
|
final equipmentsData = await _dataService.getEquipments();
|
|
final brands = <String>{};
|
|
|
|
for (var data in equipmentsData) {
|
|
final brand = data['brand'] as String?;
|
|
if (brand != null && brand.isNotEmpty) {
|
|
brands.add(brand);
|
|
}
|
|
}
|
|
|
|
return brands.toList()..sort();
|
|
} catch (e) {
|
|
print('Error getting all brands: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Récupérer les modèles filtrés par marque
|
|
Future<List<String>> getModelsByBrand(String brand) async {
|
|
try {
|
|
final equipmentsData = await _dataService.getEquipments();
|
|
final models = <String>{};
|
|
|
|
for (var data in equipmentsData) {
|
|
if (data['brand'] == brand) {
|
|
final model = data['model'] as String?;
|
|
if (model != null && model.isNotEmpty) {
|
|
models.add(model);
|
|
}
|
|
}
|
|
}
|
|
|
|
return models.toList()..sort();
|
|
} catch (e) {
|
|
print('Error getting models by brand: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Vérifier si un ID existe déjà
|
|
Future<bool> isIdUnique(String id) async {
|
|
try {
|
|
final equipment = await getEquipmentById(id);
|
|
return equipment == null;
|
|
} catch (e) {
|
|
print('Error checking ID uniqueness: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Récupérer toutes les boîtes/containers disponibles
|
|
Future<List<ContainerModel>> getBoxes() async {
|
|
try {
|
|
final containersData = await _dataService.getContainers();
|
|
|
|
final boxes = <ContainerModel>[];
|
|
for (var data in containersData) {
|
|
final id = data['id'] as String;
|
|
final container = ContainerModel.fromMap(data, id);
|
|
boxes.add(container);
|
|
}
|
|
|
|
return boxes;
|
|
} catch (e) {
|
|
print('Error getting boxes: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Récupérer plusieurs équipements par leurs IDs
|
|
Future<List<EquipmentModel>> getEquipmentsByIds(List<String> ids) async {
|
|
try {
|
|
if (ids.isEmpty) return [];
|
|
|
|
final equipmentsData = await _dataService.getEquipmentsByIds(ids);
|
|
|
|
return equipmentsData
|
|
.map((data) => EquipmentModel.fromMap(data, data['id'] as String))
|
|
.toList();
|
|
} catch (e) {
|
|
print('Error getting equipments by IDs: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Récupérer les maintenances pour un équipement
|
|
/// Note: Cette méthode est maintenant déléguée au MaintenanceService
|
|
/// pour éviter la duplication de code
|
|
Future<List<MaintenanceModel>> getMaintenancesForEquipment(String equipmentId) async {
|
|
try {
|
|
// Déléguer au MaintenanceService qui utilise déjà les Cloud Functions
|
|
final maintenanceService = MaintenanceService();
|
|
return await maintenanceService.getMaintenancesByEquipment(equipmentId);
|
|
} catch (e) {
|
|
print('Error getting maintenances for equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
}
|
|
|