Files
EM2_ERP/em2rp/lib/services/equipment_service.dart
ElPoyo 3fab69cb00 feat: Gestion complète des containers et refactorisation du matériel
Ajout de la gestion des containers (création, édition, suppression, affichage des détails).
Introduction d'un système de génération de QR codes unifié et d'un mode de sélection multiple.

**Features:**
- **Gestion des Containers :**
    - Nouvelle page de gestion des containers (`container_management_page.dart`) avec recherche et filtres.
    - Formulaire de création/édition de containers (`container_form_page.dart`) avec génération d'ID automatique.
    - Page de détails d'un container (`container_detail_page.dart`) affichant son contenu et ses caractéristiques.
    - Ajout des routes et du provider (`ContainerProvider`) nécessaires.
- **Modèle de Données :**
    - Ajout du `ContainerModel` pour représenter les boîtes, flight cases, etc.
    - Le modèle `EquipmentModel` a été enrichi avec des caractéristiques physiques (poids, dimensions).
- **QR Codes :**
    - Nouveau service unifié (`UnifiedPDFGeneratorService`) pour générer des PDFs de QR codes pour n'importe quelle entité.
    - Services `PDFGeneratorService` et `ContainerPDFGeneratorService` transformés en wrappers pour maintenir la compatibilité.
    - Amélioration de la performance de la génération de QR codes en masse.
- **Interface Utilisateur (UI/UX) :**
    - Nouvelle page de détails pour le matériel (`equipment_detail_page.dart`).
    - Ajout d'un `SelectionModeMixin` pour gérer la sélection multiple dans les pages de gestion.
    - Dialogues réutilisables pour l'affichage de QR codes (`QRCodeDialog`) et la sélection de format d'impression (`QRCodeFormatSelectorDialog`).
    - Ajout d'un bouton "Gérer les boîtes" sur la page de gestion du matériel.

**Refactorisation :**
- L' `IdGenerator` a été déplacé dans le répertoire `utils` et étendu pour gérer les containers.
- Mise à jour de nombreuses dépendances `pubspec.yaml` vers des versions plus récentes.
- Séparation de la logique d'affichage des containers et du matériel dans des widgets dédiés (`ContainerHeaderCard`, `EquipmentParentContainers`, etc.).
2025-10-29 10:57:42 +01:00

433 lines
13 KiB
Dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/models/alert_model.dart';
import 'package:em2rp/models/maintenance_model.dart';
class EquipmentService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
// Collection references
CollectionReference get _equipmentCollection => _firestore.collection('equipments');
CollectionReference get _alertsCollection => _firestore.collection('alerts');
CollectionReference get _eventsCollection => _firestore.collection('events');
// CRUD Operations
/// Créer un nouvel équipement
Future<void> createEquipment(EquipmentModel equipment) async {
try {
await _equipmentCollection.doc(equipment.id).set(equipment.toMap());
} catch (e) {
print('Error creating equipment: $e');
rethrow;
}
}
/// Mettre à jour un équipement
Future<void> updateEquipment(String id, Map<String, dynamic> data) async {
try {
data['updatedAt'] = Timestamp.fromDate(DateTime.now());
await _equipmentCollection.doc(id).update(data);
} catch (e) {
print('Error updating equipment: $e');
rethrow;
}
}
/// Supprimer un équipement
Future<void> deleteEquipment(String id) async {
try {
await _equipmentCollection.doc(id).delete();
} catch (e) {
print('Error deleting equipment: $e');
rethrow;
}
}
/// Récupérer un équipement par ID
Future<EquipmentModel?> getEquipmentById(String id) async {
try {
final doc = await _equipmentCollection.doc(id).get();
if (doc.exists) {
return EquipmentModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
}
return null;
} catch (e) {
print('Error getting equipment: $e');
rethrow;
}
}
/// Récupérer les équipements avec filtres
Stream<List<EquipmentModel>> getEquipment({
EquipmentCategory? category,
EquipmentStatus? status,
String? model,
String? searchQuery,
}) {
try {
Query query = _equipmentCollection;
// Filtre par catégorie
if (category != null) {
query = query.where('category', isEqualTo: equipmentCategoryToString(category));
}
// Filtre par statut
if (status != null) {
query = query.where('status', isEqualTo: equipmentStatusToString(status));
}
// Filtre par modèle
if (model != null && model.isNotEmpty) {
query = query.where('model', isEqualTo: model);
}
return query.snapshots().map((snapshot) {
List<EquipmentModel> equipmentList = snapshot.docs
.map((doc) => EquipmentModel.fromMap(doc.data() as Map<String, dynamic>, doc.id))
.toList();
// Filtre par recherche texte (côté client car Firestore ne supporte pas les recherches texte complexes)
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 streaming equipment: $e');
rethrow;
}
}
/// Vérifier la disponibilité d'un équipement pour une période donnée
Future<List<String>> checkAvailability(
String equipmentId,
DateTime startDate,
DateTime endDate,
) async {
try {
final conflicts = <String>[];
// Récupérer tous les événements qui chevauchent la période
final eventsQuery = await _eventsCollection
.where('StartDateTime', isLessThanOrEqualTo: Timestamp.fromDate(endDate))
.where('EndDateTime', isGreaterThanOrEqualTo: Timestamp.fromDate(startDate))
.get();
for (var eventDoc in eventsQuery.docs) {
final eventData = eventDoc.data() as Map<String, dynamic>;
final assignedEquipmentRaw = eventData['assignedEquipment'] ?? [];
if (assignedEquipmentRaw is List) {
for (var eq in assignedEquipmentRaw) {
if (eq is Map && eq['equipmentId'] == equipmentId) {
conflicts.add(eventDoc.id);
break;
}
}
}
}
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 {
// Récupérer tous les équipements du même modèle
final equipmentQuery = await _equipmentCollection
.where('model', isEqualTo: model)
.get();
final alternatives = <EquipmentModel>[];
for (var doc in equipmentQuery.docs) {
final equipment = EquipmentModel.fromMap(
doc.data() as Map<String, dynamic>,
doc.id,
);
// Vérifier la disponibilité
final conflicts = await checkAvailability(equipment.id, startDate, endDate);
if (conflicts.isEmpty && equipment.status == EquipmentStatus.available) {
alternatives.add(equipment);
}
}
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 equipmentQuery = await _equipmentCollection
.where('category', whereIn: [
equipmentCategoryToString(EquipmentCategory.consumable),
equipmentCategoryToString(EquipmentCategory.cable),
])
.get();
for (var doc in equipmentQuery.docs) {
final equipment = EquipmentModel.fromMap(
doc.data() as Map<String, dynamic>,
doc.id,
);
if (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 {
// Vérifier si une alerte existe déjà pour cet équipement
final existingAlerts = await _alertsCollection
.where('equipmentId', isEqualTo: equipment.id)
.where('type', isEqualTo: alertTypeToString(AlertType.lowStock))
.where('isRead', isEqualTo: false)
.get();
if (existingAlerts.docs.isEmpty) {
final alert = AlertModel(
id: _alertsCollection.doc().id,
type: AlertType.lowStock,
message: 'Stock critique pour ${equipment.name} (${equipment.model ?? ""}): ${equipment.availableQuantity}/${equipment.criticalThreshold}',
equipmentId: equipment.id,
createdAt: DateTime.now(),
);
await _alertsCollection.doc(alert.id).set(alert.toMap());
}
} catch (e) {
print('Error creating low stock alert: $e');
rethrow;
}
}
/// 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 equipmentQuery = await _equipmentCollection.get();
final models = <String>{};
for (var doc in equipmentQuery.docs) {
final data = doc.data() as Map<String, dynamic>;
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 equipmentQuery = await _equipmentCollection.get();
final brands = <String>{};
for (var doc in equipmentQuery.docs) {
final data = doc.data() as Map<String, dynamic>;
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 equipmentQuery = await _equipmentCollection
.where('brand', isEqualTo: brand)
.get();
final models = <String>{};
for (var doc in equipmentQuery.docs) {
final data = doc.data() as Map<String, dynamic>;
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 doc = await _equipmentCollection.doc(id).get();
return !doc.exists;
} catch (e) {
print('Error checking ID uniqueness: $e');
rethrow;
}
}
/// Récupérer toutes les boîtes (équipements qui peuvent contenir d'autres équipements)
Future<List<EquipmentModel>> getBoxes() async {
try {
// Les boîtes sont généralement des équipements de catégorie "structure" ou "other"
// On pourrait aussi ajouter un champ spécifique "isBox" dans le modèle
final equipmentQuery = await _equipmentCollection
.where('category', whereIn: [
equipmentCategoryToString(EquipmentCategory.structure),
equipmentCategoryToString(EquipmentCategory.other),
])
.get();
final boxes = <EquipmentModel>[];
for (var doc in equipmentQuery.docs) {
final equipment = EquipmentModel.fromMap(
doc.data() as Map<String, dynamic>,
doc.id,
);
// On pourrait ajouter un filtre supplémentaire ici si besoin
boxes.add(equipment);
}
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 equipments = <EquipmentModel>[];
// Firestore limite les requêtes whereIn à 10 éléments
// On doit donc diviser en plusieurs requêtes si nécessaire
for (int i = 0; i < ids.length; i += 10) {
final batch = ids.skip(i).take(10).toList();
final query = await _equipmentCollection
.where(FieldPath.documentId, whereIn: batch)
.get();
for (var doc in query.docs) {
equipments.add(
EquipmentModel.fromMap(
doc.data() as Map<String, dynamic>,
doc.id,
),
);
}
}
return equipments;
} catch (e) {
print('Error getting equipments by IDs: $e');
rethrow;
}
}
/// Récupérer les maintenances pour un équipement
Future<List<MaintenanceModel>> getMaintenancesForEquipment(String equipmentId) async {
try {
final maintenanceQuery = await _firestore
.collection('maintenances')
.where('equipmentIds', arrayContains: equipmentId)
.orderBy('scheduledDate', descending: true)
.get();
final maintenances = <MaintenanceModel>[];
for (var doc in maintenanceQuery.docs) {
maintenances.add(
MaintenanceModel.fromMap(
doc.data(),
doc.id,
),
);
}
return maintenances;
} catch (e) {
print('Error getting maintenances for equipment: $e');
rethrow;
}
}
}