Files
EM2_ERP/em2rp/lib/services/equipment_service.dart
ElPoyo b30ae0f10a feat: Sécurisation Firestore, gestion des prix HT/TTC et refactorisation majeure
Cette mise à jour verrouille l'accès direct à Firestore depuis le client pour renforcer la sécurité et introduit une gestion complète des prix HT/TTC dans toute l'application. Elle apporte également des améliorations significatives des permissions, des optimisations de performance et de nouvelles fonctionnalités.

### Sécurité et Backend
- **Firestore Rules :** Ajout de `firestore.rules` qui bloque par défaut tous les accès en lecture/écriture depuis le client. Toutes les opérations de données doivent maintenant passer par les Cloud Functions, renforçant considérablement la sécurité.
- **Index Firestore :** Création d'un fichier `firestore.indexes.json` pour optimiser les requêtes sur la collection `events`.
- **Cloud Functions :** Les fonctions de création/mise à jour d'événements ont été adaptées pour accepter des ID de documents (utilisateurs, type d'événement) et les convertir en `DocumentReference` côté serveur, simplifiant les appels depuis le client.

### Gestion des Prix HT/TTC
- **Calcul Automatisé :** Introduction d'un helper `PriceHelpers` et d'un widget `PriceHtTtcFields` pour calculer et synchroniser automatiquement les prix HT et TTC dans le formulaire d'événement.
- **Affichage Détaillé :**
    - Les détails des événements et des options affichent désormais les prix HT, la TVA et le TTC séparément pour plus de clarté.
    - Le prix de base (`basePrice`) est maintenant traité comme un prix TTC dans toute l'application.

### Permissions et Rôles
- **Centralisation (`AppPermission`) :** Création d'une énumération `AppPermission` pour centraliser toutes les permissions de l'application, avec descriptions et catégories.
- **Rôles Prédéfinis :** Définition de rôles standards (Admin, Manager, Technicien, User) avec des jeux de permissions prédéfinis.
- **Filtre par Utilisateur :** Ajout d'un filtre par utilisateur sur la page Calendrier, visible uniquement pour les utilisateurs ayant la permission `view_all_user_events`.

### Améliorations et Optimisations (Frontend)
- **`DebugLog` :** Ajout d'un utilitaire `DebugLog` pour gérer les logs, qui sont automatiquement désactivés en mode production.
- **Optimisation du Sélecteur d'Équipement :**
    - La boîte de dialogue de sélection d'équipement a été lourdement optimisée pour éviter les reconstructions complètes de la liste lors de la sélection/désélection d'items.
    - Utilisation de `ValueNotifier` et de caches locaux (`_cachedContainers`, `_cachedEquipment`) pour des mises à jour d'UI plus ciblées et fluides.
    - La position du scroll est désormais préservée.
- **Catégorie d'Équipement :** Ajout de la catégorie `Vehicle` (Véhicule) pour les équipements.
- **Formulaires :** Les formulaires de création/modification d'événements et d'équipements ont été nettoyés de leurs logs de débogage excessifs.
2026-01-14 17:32:58 +01:00

377 lines
12 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 {
if (equipment.id.isEmpty) {
throw Exception('L\'ID de l\'équipement est requis pour la création');
}
final data = equipment.toMap();
data['id'] = equipment.id; // S'assurer que l'ID est inclus
await _apiService.call('createEquipment', data);
} 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 {
if (data.isEmpty) {
throw Exception('Aucune donnée à mettre à jour');
}
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;
}
}
}