Add equipment management features (and qr generation support)
This commit is contained in:
		| @@ -1,8 +1,12 @@ | ||||
| import 'package:em2rp/providers/users_provider.dart'; | ||||
| import 'package:em2rp/providers/event_provider.dart'; | ||||
| import 'package:em2rp/providers/equipment_provider.dart'; | ||||
| import 'package:em2rp/providers/maintenance_provider.dart'; | ||||
| import 'package:em2rp/providers/alert_provider.dart'; | ||||
| import 'package:em2rp/utils/auth_guard_widget.dart'; | ||||
| import 'package:em2rp/views/calendar_page.dart'; | ||||
| import 'package:em2rp/views/login_page.dart'; | ||||
| import 'package:em2rp/views/equipment_management_page.dart'; | ||||
| import 'package:firebase_auth/firebase_auth.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:firebase_core/firebase_core.dart'; | ||||
| @@ -43,6 +47,17 @@ void main() async { | ||||
|         ChangeNotifierProvider<EventProvider>( | ||||
|           create: (context) => EventProvider(), | ||||
|         ), | ||||
|  | ||||
|         // Providers pour la gestion du matériel | ||||
|         ChangeNotifierProvider<EquipmentProvider>( | ||||
|           create: (context) => EquipmentProvider(), | ||||
|         ), | ||||
|         ChangeNotifierProvider<MaintenanceProvider>( | ||||
|           create: (context) => MaintenanceProvider(), | ||||
|         ), | ||||
|         ChangeNotifierProvider<AlertProvider>( | ||||
|           create: (context) => AlertProvider(), | ||||
|         ), | ||||
|       ], | ||||
|       child: const MyApp(), | ||||
|     ), | ||||
| @@ -105,6 +120,9 @@ class MyApp extends StatelessWidget { | ||||
|             actionCode: args['actionCode'] as String, | ||||
|           ); | ||||
|         }, | ||||
|         '/equipment_management': (context) => const AuthGuard( | ||||
|             requiredPermission: "view_equipment", | ||||
|             child: EquipmentManagementPage()), | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										89
									
								
								em2rp/lib/models/alert_model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								em2rp/lib/models/alert_model.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| import 'package:cloud_firestore/cloud_firestore.dart'; | ||||
|  | ||||
| enum AlertType { | ||||
|   lowStock,       // Stock faible | ||||
|   maintenanceDue, // Maintenance à venir | ||||
|   conflict        // Conflit disponibilité | ||||
| } | ||||
|  | ||||
| String alertTypeToString(AlertType type) { | ||||
|   switch (type) { | ||||
|     case AlertType.lowStock: | ||||
|       return 'LOW_STOCK'; | ||||
|     case AlertType.maintenanceDue: | ||||
|       return 'MAINTENANCE_DUE'; | ||||
|     case AlertType.conflict: | ||||
|       return 'CONFLICT'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| AlertType alertTypeFromString(String? type) { | ||||
|   switch (type) { | ||||
|     case 'LOW_STOCK': | ||||
|       return AlertType.lowStock; | ||||
|     case 'MAINTENANCE_DUE': | ||||
|       return AlertType.maintenanceDue; | ||||
|     case 'CONFLICT': | ||||
|       return AlertType.conflict; | ||||
|     default: | ||||
|       return AlertType.conflict; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class AlertModel { | ||||
|   final String id;                  // ID généré automatiquement | ||||
|   final AlertType type;             // Type d'alerte | ||||
|   final String message;             // Message de l'alerte | ||||
|   final String? equipmentId;        // ID de l'équipement concerné (optionnel) | ||||
|   final DateTime createdAt;         // Date de création | ||||
|   final bool isRead;                // Statut lu/non lu | ||||
|  | ||||
|   AlertModel({ | ||||
|     required this.id, | ||||
|     required this.type, | ||||
|     required this.message, | ||||
|     this.equipmentId, | ||||
|     required this.createdAt, | ||||
|     this.isRead = false, | ||||
|   }); | ||||
|  | ||||
|   factory AlertModel.fromMap(Map<String, dynamic> map, String id) { | ||||
|     return AlertModel( | ||||
|       id: id, | ||||
|       type: alertTypeFromString(map['type']), | ||||
|       message: map['message'] ?? '', | ||||
|       equipmentId: map['equipmentId'], | ||||
|       createdAt: (map['createdAt'] as Timestamp?)?.toDate() ?? DateTime.now(), | ||||
|       isRead: map['isRead'] ?? false, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Map<String, dynamic> toMap() { | ||||
|     return { | ||||
|       'type': alertTypeToString(type), | ||||
|       'message': message, | ||||
|       'equipmentId': equipmentId, | ||||
|       'createdAt': Timestamp.fromDate(createdAt), | ||||
|       'isRead': isRead, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   AlertModel copyWith({ | ||||
|     String? id, | ||||
|     AlertType? type, | ||||
|     String? message, | ||||
|     String? equipmentId, | ||||
|     DateTime? createdAt, | ||||
|     bool? isRead, | ||||
|   }) { | ||||
|     return AlertModel( | ||||
|       id: id ?? this.id, | ||||
|       type: type ?? this.type, | ||||
|       message: message ?? this.message, | ||||
|       equipmentId: equipmentId ?? this.equipmentId, | ||||
|       createdAt: createdAt ?? this.createdAt, | ||||
|       isRead: isRead ?? this.isRead, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										279
									
								
								em2rp/lib/models/equipment_model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								em2rp/lib/models/equipment_model.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,279 @@ | ||||
| import 'package:cloud_firestore/cloud_firestore.dart'; | ||||
|  | ||||
| enum EquipmentStatus { | ||||
|   available,      // Disponible | ||||
|   inUse,          // En prestation | ||||
|   rented,         // Loué | ||||
|   lost,           // Perdu | ||||
|   outOfService,   // HS | ||||
|   maintenance,    // En maintenance | ||||
| } | ||||
|  | ||||
| String equipmentStatusToString(EquipmentStatus status) { | ||||
|   switch (status) { | ||||
|     case EquipmentStatus.available: | ||||
|       return 'AVAILABLE'; | ||||
|     case EquipmentStatus.inUse: | ||||
|       return 'IN_USE'; | ||||
|     case EquipmentStatus.rented: | ||||
|       return 'RENTED'; | ||||
|     case EquipmentStatus.lost: | ||||
|       return 'LOST'; | ||||
|     case EquipmentStatus.outOfService: | ||||
|       return 'OUT_OF_SERVICE'; | ||||
|     case EquipmentStatus.maintenance: | ||||
|       return 'MAINTENANCE'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| EquipmentStatus equipmentStatusFromString(String? status) { | ||||
|   switch (status) { | ||||
|     case 'AVAILABLE': | ||||
|       return EquipmentStatus.available; | ||||
|     case 'IN_USE': | ||||
|       return EquipmentStatus.inUse; | ||||
|     case 'RENTED': | ||||
|       return EquipmentStatus.rented; | ||||
|     case 'LOST': | ||||
|       return EquipmentStatus.lost; | ||||
|     case 'OUT_OF_SERVICE': | ||||
|       return EquipmentStatus.outOfService; | ||||
|     case 'MAINTENANCE': | ||||
|       return EquipmentStatus.maintenance; | ||||
|     default: | ||||
|       return EquipmentStatus.available; | ||||
|   } | ||||
| } | ||||
|  | ||||
| enum EquipmentCategory { | ||||
|   lighting,       // Lumière | ||||
|   sound,          // Son | ||||
|   video,          // Vidéo | ||||
|   effect,        // Effets spéciaux | ||||
|   structure,      // Structure | ||||
|   consumable,     // Consommable | ||||
|   cable,          // Câble | ||||
|   other           // Autre | ||||
| } | ||||
|  | ||||
| String equipmentCategoryToString(EquipmentCategory category) { | ||||
|   switch (category) { | ||||
|     case EquipmentCategory.lighting: | ||||
|       return 'LIGHTING'; | ||||
|     case EquipmentCategory.sound: | ||||
|       return 'SOUND'; | ||||
|     case EquipmentCategory.video: | ||||
|       return 'VIDEO'; | ||||
|     case EquipmentCategory.structure: | ||||
|       return 'STRUCTURE'; | ||||
|     case EquipmentCategory.consumable: | ||||
|       return 'CONSUMABLE'; | ||||
|     case EquipmentCategory.cable: | ||||
|       return 'CABLE'; | ||||
|     case EquipmentCategory.other: | ||||
|       return 'OTHER'; | ||||
|     case EquipmentCategory.effect: | ||||
|       return 'EFFECT'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| EquipmentCategory equipmentCategoryFromString(String? category) { | ||||
|   switch (category) { | ||||
|     case 'LIGHTING': | ||||
|       return EquipmentCategory.lighting; | ||||
|     case 'SOUND': | ||||
|       return EquipmentCategory.sound; | ||||
|     case 'VIDEO': | ||||
|       return EquipmentCategory.video; | ||||
|     case 'STRUCTURE': | ||||
|       return EquipmentCategory.structure; | ||||
|     case 'CONSUMABLE': | ||||
|       return EquipmentCategory.consumable; | ||||
|     case 'CABLE': | ||||
|       return EquipmentCategory.cable; | ||||
|     case 'EFFECT': | ||||
|       return EquipmentCategory.effect; | ||||
|     case 'OTHER': | ||||
|     default: | ||||
|       return EquipmentCategory.other; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class EquipmentModel { | ||||
|   final String id;                          // Identifiant unique (clé) | ||||
|   final String name;                        // Nom de l'équipement | ||||
|   final String? brand;                      // Marque (indexé) | ||||
|   final String? model;                      // Modèle (indexé) | ||||
|   final EquipmentCategory category;         // Catégorie | ||||
|   final EquipmentStatus status;             // Statut actuel | ||||
|  | ||||
|   // Prix (visible uniquement avec manage_equipment) | ||||
|   final double? purchasePrice;              // Prix d'achat | ||||
|   final double? rentalPrice;                // Prix de location | ||||
|  | ||||
|   // Quantité (pour consommables/câbles) | ||||
|   final int? totalQuantity;                 // Quantité totale | ||||
|   final int? availableQuantity;             // Quantité disponible | ||||
|   final int? criticalThreshold;             // Seuil critique pour alerte | ||||
|  | ||||
|   // Boîtes parentes (plusieurs possibles) | ||||
|   final List<String> parentBoxIds;          // IDs des boîtes contenant cet équipement | ||||
|  | ||||
|   // Dates & maintenance | ||||
|   final DateTime? purchaseDate;             // Date d'achat | ||||
|   final DateTime? lastMaintenanceDate;      // Dernière maintenance | ||||
|   final DateTime? nextMaintenanceDate;      // Prochaine maintenance prévue | ||||
|  | ||||
|   // Maintenances (références) | ||||
|   final List<String> maintenanceIds;        // IDs des opérations de maintenance | ||||
|  | ||||
|   // Image | ||||
|   final String? imageUrl;                   // URL de l'image (Storage /materiel) | ||||
|  | ||||
|   // Métadonnées | ||||
|   final String? notes;                      // Notes additionnelles | ||||
|   final DateTime createdAt;                 // Date de création | ||||
|   final DateTime updatedAt;                 // Date de mise à jour | ||||
|  | ||||
|   EquipmentModel({ | ||||
|     required this.id, | ||||
|     required this.name, | ||||
|     this.brand, | ||||
|     this.model, | ||||
|     required this.category, | ||||
|     this.status = EquipmentStatus.available, | ||||
|     this.purchasePrice, | ||||
|     this.rentalPrice, | ||||
|     this.totalQuantity, | ||||
|     this.availableQuantity, | ||||
|     this.criticalThreshold, | ||||
|     this.parentBoxIds = const [], | ||||
|     this.purchaseDate, | ||||
|     this.lastMaintenanceDate, | ||||
|     this.nextMaintenanceDate, | ||||
|     this.maintenanceIds = const [], | ||||
|     this.imageUrl, | ||||
|     this.notes, | ||||
|     required this.createdAt, | ||||
|     required this.updatedAt, | ||||
|   }); | ||||
|  | ||||
|   factory EquipmentModel.fromMap(Map<String, dynamic> map, String id) { | ||||
|     // Gestion des listes | ||||
|     final List<dynamic> parentBoxIdsRaw = map['parentBoxIds'] ?? []; | ||||
|     final List<String> parentBoxIds = parentBoxIdsRaw.map((e) => e.toString()).toList(); | ||||
|  | ||||
|     final List<dynamic> maintenanceIdsRaw = map['maintenanceIds'] ?? []; | ||||
|     final List<String> maintenanceIds = maintenanceIdsRaw.map((e) => e.toString()).toList(); | ||||
|  | ||||
|     return EquipmentModel( | ||||
|       id: id, | ||||
|       name: map['name'] ?? '', | ||||
|       brand: map['brand'], | ||||
|       model: map['model'], | ||||
|       category: equipmentCategoryFromString(map['category']), | ||||
|       status: equipmentStatusFromString(map['status']), | ||||
|       purchasePrice: map['purchasePrice']?.toDouble(), | ||||
|       rentalPrice: map['rentalPrice']?.toDouble(), | ||||
|       totalQuantity: map['totalQuantity']?.toInt(), | ||||
|       availableQuantity: map['availableQuantity']?.toInt(), | ||||
|       criticalThreshold: map['criticalThreshold']?.toInt(), | ||||
|       parentBoxIds: parentBoxIds, | ||||
|       purchaseDate: (map['purchaseDate'] as Timestamp?)?.toDate(), | ||||
|       nextMaintenanceDate: (map['nextMaintenanceDate'] as Timestamp?)?.toDate(), | ||||
|       maintenanceIds: maintenanceIds, | ||||
|       imageUrl: map['imageUrl'], | ||||
|       notes: map['notes'], | ||||
|       createdAt: (map['createdAt'] as Timestamp?)?.toDate() ?? DateTime.now(), | ||||
|       updatedAt: (map['updatedAt'] as Timestamp?)?.toDate() ?? DateTime.now(), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Map<String, dynamic> toMap() { | ||||
|     return { | ||||
|       'name': name, | ||||
|       'brand': brand, | ||||
|       'model': model, | ||||
|       'category': equipmentCategoryToString(category), | ||||
|       'status': equipmentStatusToString(status), | ||||
|       'purchasePrice': purchasePrice, | ||||
|       'rentalPrice': rentalPrice, | ||||
|       'totalQuantity': totalQuantity, | ||||
|       'availableQuantity': availableQuantity, | ||||
|       'criticalThreshold': criticalThreshold, | ||||
|       'parentBoxIds': parentBoxIds, | ||||
|       'lastMaintenanceDate': lastMaintenanceDate != null ? Timestamp.fromDate(lastMaintenanceDate!) : null, | ||||
|       'purchaseDate': purchaseDate != null ? Timestamp.fromDate(purchaseDate!) : null, | ||||
|       'nextMaintenanceDate': nextMaintenanceDate != null ? Timestamp.fromDate(nextMaintenanceDate!) : null, | ||||
|       'maintenanceIds': maintenanceIds, | ||||
|       'imageUrl': imageUrl, | ||||
|       'notes': notes, | ||||
|       'createdAt': Timestamp.fromDate(createdAt), | ||||
|       'updatedAt': Timestamp.fromDate(updatedAt), | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   EquipmentModel copyWith({ | ||||
|     String? id, | ||||
|     String? brand, | ||||
|     String? name, | ||||
|     String? model, | ||||
|     EquipmentCategory? category, | ||||
|     EquipmentStatus? status, | ||||
|     double? purchasePrice, | ||||
|     double? rentalPrice, | ||||
|     int? totalQuantity, | ||||
|     int? availableQuantity, | ||||
|     int? criticalThreshold, | ||||
|     List<String>? parentBoxIds, | ||||
|     DateTime? purchaseDate, | ||||
|     DateTime? lastMaintenanceDate, | ||||
|     DateTime? nextMaintenanceDate, | ||||
|     List<String>? maintenanceIds, | ||||
|     String? imageUrl, | ||||
|     String? notes, | ||||
|     DateTime? createdAt, | ||||
|     DateTime? updatedAt, | ||||
|   }) { | ||||
|     return EquipmentModel( | ||||
|       id: id ?? this.id, | ||||
|       brand: brand ?? this.brand, | ||||
|       name: name ?? this.name, | ||||
|       model: model ?? this.model, | ||||
|       category: category ?? this.category, | ||||
|       status: status ?? this.status, | ||||
|       purchasePrice: purchasePrice ?? this.purchasePrice, | ||||
|       rentalPrice: rentalPrice ?? this.rentalPrice, | ||||
|       totalQuantity: totalQuantity ?? this.totalQuantity, | ||||
|       availableQuantity: availableQuantity ?? this.availableQuantity, | ||||
|       criticalThreshold: criticalThreshold ?? this.criticalThreshold, | ||||
|       parentBoxIds: parentBoxIds ?? this.parentBoxIds, | ||||
|       lastMaintenanceDate: lastMaintenanceDate ?? this.lastMaintenanceDate, | ||||
|       purchaseDate: purchaseDate ?? this.purchaseDate, | ||||
|       nextMaintenanceDate: nextMaintenanceDate ?? this.nextMaintenanceDate, | ||||
|       maintenanceIds: maintenanceIds ?? this.maintenanceIds, | ||||
|       imageUrl: imageUrl ?? this.imageUrl, | ||||
|       notes: notes ?? this.notes, | ||||
|       createdAt: createdAt ?? this.createdAt, | ||||
|       updatedAt: updatedAt ?? this.updatedAt, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   // Helper pour vérifier si c'est un consommable/câble avec quantité | ||||
|   bool get hasQuantity => category == EquipmentCategory.consumable || category == EquipmentCategory.cable; | ||||
|  | ||||
|   // Helper pour vérifier si le stock est critique | ||||
|   bool get isCriticalStock { | ||||
|     if (!hasQuantity || criticalThreshold == null || availableQuantity == null) { | ||||
|       return false; | ||||
|     } | ||||
|     return availableQuantity! <= criticalThreshold!; | ||||
|   } | ||||
|  | ||||
|   // Helper pour vérifier si la maintenance est à venir | ||||
|   bool get isMaintenanceDue { | ||||
|     if (nextMaintenanceDate == null) return false; | ||||
|     return nextMaintenanceDate!.isBefore(DateTime.now().add(const Duration(days: 7))); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -30,6 +30,128 @@ EventStatus eventStatusFromString(String? status) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| enum PreparationStatus { | ||||
|   notStarted, | ||||
|   inProgress, | ||||
|   completed, | ||||
|   completedWithMissing | ||||
| } | ||||
|  | ||||
| String preparationStatusToString(PreparationStatus status) { | ||||
|   switch (status) { | ||||
|     case PreparationStatus.notStarted: | ||||
|       return 'NOT_STARTED'; | ||||
|     case PreparationStatus.inProgress: | ||||
|       return 'IN_PROGRESS'; | ||||
|     case PreparationStatus.completed: | ||||
|       return 'COMPLETED'; | ||||
|     case PreparationStatus.completedWithMissing: | ||||
|       return 'COMPLETED_WITH_MISSING'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| PreparationStatus preparationStatusFromString(String? status) { | ||||
|   switch (status) { | ||||
|     case 'NOT_STARTED': | ||||
|       return PreparationStatus.notStarted; | ||||
|     case 'IN_PROGRESS': | ||||
|       return PreparationStatus.inProgress; | ||||
|     case 'COMPLETED': | ||||
|       return PreparationStatus.completed; | ||||
|     case 'COMPLETED_WITH_MISSING': | ||||
|       return PreparationStatus.completedWithMissing; | ||||
|     default: | ||||
|       return PreparationStatus.notStarted; | ||||
|   } | ||||
| } | ||||
|  | ||||
| enum ReturnStatus { | ||||
|   notStarted, | ||||
|   inProgress, | ||||
|   completed, | ||||
|   completedWithMissing | ||||
| } | ||||
|  | ||||
| String returnStatusToString(ReturnStatus status) { | ||||
|   switch (status) { | ||||
|     case ReturnStatus.notStarted: | ||||
|       return 'NOT_STARTED'; | ||||
|     case ReturnStatus.inProgress: | ||||
|       return 'IN_PROGRESS'; | ||||
|     case ReturnStatus.completed: | ||||
|       return 'COMPLETED'; | ||||
|     case ReturnStatus.completedWithMissing: | ||||
|       return 'COMPLETED_WITH_MISSING'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| ReturnStatus returnStatusFromString(String? status) { | ||||
|   switch (status) { | ||||
|     case 'NOT_STARTED': | ||||
|       return ReturnStatus.notStarted; | ||||
|     case 'IN_PROGRESS': | ||||
|       return ReturnStatus.inProgress; | ||||
|     case 'COMPLETED': | ||||
|       return ReturnStatus.completed; | ||||
|     case 'COMPLETED_WITH_MISSING': | ||||
|       return ReturnStatus.completedWithMissing; | ||||
|     default: | ||||
|       return ReturnStatus.notStarted; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class EventEquipment { | ||||
|   final String equipmentId;         // ID de l'équipement | ||||
|   final int quantity;               // Quantité (pour consommables) | ||||
|   final bool isPrepared;            // Validé en préparation | ||||
|   final bool isReturned;            // Validé au retour | ||||
|   final int? returnedQuantity;      // Quantité retournée (pour consommables) | ||||
|  | ||||
|   EventEquipment({ | ||||
|     required this.equipmentId, | ||||
|     this.quantity = 1, | ||||
|     this.isPrepared = false, | ||||
|     this.isReturned = false, | ||||
|     this.returnedQuantity, | ||||
|   }); | ||||
|  | ||||
|   factory EventEquipment.fromMap(Map<String, dynamic> map) { | ||||
|     return EventEquipment( | ||||
|       equipmentId: map['equipmentId'] ?? '', | ||||
|       quantity: map['quantity'] ?? 1, | ||||
|       isPrepared: map['isPrepared'] ?? false, | ||||
|       isReturned: map['isReturned'] ?? false, | ||||
|       returnedQuantity: map['returnedQuantity'], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Map<String, dynamic> toMap() { | ||||
|     return { | ||||
|       'equipmentId': equipmentId, | ||||
|       'quantity': quantity, | ||||
|       'isPrepared': isPrepared, | ||||
|       'isReturned': isReturned, | ||||
|       'returnedQuantity': returnedQuantity, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   EventEquipment copyWith({ | ||||
|     String? equipmentId, | ||||
|     int? quantity, | ||||
|     bool? isPrepared, | ||||
|     bool? isReturned, | ||||
|     int? returnedQuantity, | ||||
|   }) { | ||||
|     return EventEquipment( | ||||
|       equipmentId: equipmentId ?? this.equipmentId, | ||||
|       quantity: quantity ?? this.quantity, | ||||
|       isPrepared: isPrepared ?? this.isPrepared, | ||||
|       isReturned: isReturned ?? this.isReturned, | ||||
|       returnedQuantity: returnedQuantity ?? this.returnedQuantity, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class EventModel { | ||||
|   final String id; | ||||
|   final String name; | ||||
| @@ -50,6 +172,11 @@ class EventModel { | ||||
|   final List<Map<String, dynamic>> options; | ||||
|   final EventStatus status; | ||||
|  | ||||
|   // Nouveaux champs pour la gestion du matériel | ||||
|   final List<EventEquipment> assignedEquipment; | ||||
|   final PreparationStatus? preparationStatus; | ||||
|   final ReturnStatus? returnStatus; | ||||
|  | ||||
|   EventModel({ | ||||
|     required this.id, | ||||
|     required this.name, | ||||
| @@ -69,6 +196,9 @@ class EventModel { | ||||
|     required this.documents, | ||||
|     this.options = const [], | ||||
|     this.status = EventStatus.waitingForApproval, | ||||
|     this.assignedEquipment = const [], | ||||
|     this.preparationStatus, | ||||
|     this.returnStatus, | ||||
|   }); | ||||
|  | ||||
|   factory EventModel.fromMap(Map<String, dynamic> map, String id) { | ||||
| @@ -149,6 +279,22 @@ class EventModel { | ||||
|         customerId = map['customer'] as String; | ||||
|       } | ||||
|  | ||||
|       // Gestion des équipements assignés | ||||
|       final assignedEquipmentRaw = map['assignedEquipment'] ?? []; | ||||
|       final List<EventEquipment> assignedEquipment = []; | ||||
|  | ||||
|       if (assignedEquipmentRaw is List) { | ||||
|         for (var e in assignedEquipmentRaw) { | ||||
|           try { | ||||
|             if (e is Map) { | ||||
|               assignedEquipment.add(EventEquipment.fromMap(Map<String, dynamic>.from(e))); | ||||
|             } | ||||
|           } catch (equipmentError) { | ||||
|             print('Warning: Failed to parse equipment in event $id: $equipmentError'); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       return EventModel( | ||||
|         id: id, | ||||
|         name: (map['Name'] ?? '').toString().trim(), | ||||
| @@ -168,6 +314,9 @@ class EventModel { | ||||
|         documents: docs, | ||||
|         options: options, | ||||
|         status: eventStatusFromString(map['status'] as String?), | ||||
|         assignedEquipment: assignedEquipment, | ||||
|         preparationStatus: preparationStatusFromString(map['preparationStatus'] as String?), | ||||
|         returnStatus: returnStatusFromString(map['returnStatus'] as String?), | ||||
|       ); | ||||
|     } catch (e) { | ||||
|       print('Error parsing event $id: $e'); | ||||
| @@ -220,6 +369,9 @@ class EventModel { | ||||
|       'documents': documents, | ||||
|       'options': options, | ||||
|       'status': eventStatusToString(status), | ||||
|       'assignedEquipment': assignedEquipment.map((e) => e.toMap()).toList(), | ||||
|       'preparationStatus': preparationStatus != null ? preparationStatusToString(preparationStatus!) : null, | ||||
|       'returnStatus': returnStatus != null ? returnStatusToString(returnStatus!) : null, | ||||
|     }; | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										138
									
								
								em2rp/lib/models/maintenance_model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								em2rp/lib/models/maintenance_model.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,138 @@ | ||||
| import 'package:cloud_firestore/cloud_firestore.dart'; | ||||
|  | ||||
| enum MaintenanceType { | ||||
|   preventive,     // Préventive | ||||
|   corrective,     // Corrective | ||||
|   inspection      // Inspection | ||||
| } | ||||
|  | ||||
| String maintenanceTypeToString(MaintenanceType type) { | ||||
|   switch (type) { | ||||
|     case MaintenanceType.preventive: | ||||
|       return 'PREVENTIVE'; | ||||
|     case MaintenanceType.corrective: | ||||
|       return 'CORRECTIVE'; | ||||
|     case MaintenanceType.inspection: | ||||
|       return 'INSPECTION'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| MaintenanceType maintenanceTypeFromString(String? type) { | ||||
|   switch (type) { | ||||
|     case 'PREVENTIVE': | ||||
|       return MaintenanceType.preventive; | ||||
|     case 'CORRECTIVE': | ||||
|       return MaintenanceType.corrective; | ||||
|     case 'INSPECTION': | ||||
|       return MaintenanceType.inspection; | ||||
|     default: | ||||
|       return MaintenanceType.preventive; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class MaintenanceModel { | ||||
|   final String id;                      // ID aléatoire | ||||
|   final List<String> equipmentIds;      // IDs des équipements concernés (peut être multiple) | ||||
|   final MaintenanceType type;           // Type de maintenance | ||||
|   final DateTime scheduledDate;         // Date planifiée | ||||
|   final DateTime? completedDate;        // Date de réalisation (null si pas encore effectuée) | ||||
|   final String name;                    // Nom de l'opération | ||||
|   final String description;             // Description détaillée | ||||
|   final String? performedBy;            // ID de l'utilisateur qui a effectué la maintenance | ||||
|   final double? cost;                   // Coût de la maintenance | ||||
|   final String? notes;                  // Notes additionnelles | ||||
|   final DateTime createdAt;             // Date de création | ||||
|   final DateTime updatedAt;             // Date de mise à jour | ||||
|  | ||||
|   MaintenanceModel({ | ||||
|     required this.id, | ||||
|     required this.equipmentIds, | ||||
|     required this.type, | ||||
|     required this.scheduledDate, | ||||
|     this.completedDate, | ||||
|     required this.name, | ||||
|     required this.description, | ||||
|     this.performedBy, | ||||
|     this.cost, | ||||
|     this.notes, | ||||
|     required this.createdAt, | ||||
|     required this.updatedAt, | ||||
|   }); | ||||
|  | ||||
|   factory MaintenanceModel.fromMap(Map<String, dynamic> map, String id) { | ||||
|     // Gestion de la liste des équipements | ||||
|     final List<dynamic> equipmentIdsRaw = map['equipmentIds'] ?? []; | ||||
|     final List<String> equipmentIds = equipmentIdsRaw.map((e) => e.toString()).toList(); | ||||
|  | ||||
|     return MaintenanceModel( | ||||
|       id: id, | ||||
|       equipmentIds: equipmentIds, | ||||
|       type: maintenanceTypeFromString(map['type']), | ||||
|       scheduledDate: (map['scheduledDate'] as Timestamp?)?.toDate() ?? DateTime.now(), | ||||
|       completedDate: (map['completedDate'] as Timestamp?)?.toDate(), | ||||
|       name: map['name'] ?? '', | ||||
|       description: map['description'] ?? '', | ||||
|       performedBy: map['performedBy'], | ||||
|       cost: map['cost']?.toDouble(), | ||||
|       notes: map['notes'], | ||||
|       createdAt: (map['createdAt'] as Timestamp?)?.toDate() ?? DateTime.now(), | ||||
|       updatedAt: (map['updatedAt'] as Timestamp?)?.toDate() ?? DateTime.now(), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Map<String, dynamic> toMap() { | ||||
|     return { | ||||
|       'equipmentIds': equipmentIds, | ||||
|       'type': maintenanceTypeToString(type), | ||||
|       'scheduledDate': Timestamp.fromDate(scheduledDate), | ||||
|       'completedDate': completedDate != null ? Timestamp.fromDate(completedDate!) : null, | ||||
|       'name': name, | ||||
|       'description': description, | ||||
|       'performedBy': performedBy, | ||||
|       'cost': cost, | ||||
|       'notes': notes, | ||||
|       'createdAt': Timestamp.fromDate(createdAt), | ||||
|       'updatedAt': Timestamp.fromDate(updatedAt), | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   MaintenanceModel copyWith({ | ||||
|     String? id, | ||||
|     List<String>? equipmentIds, | ||||
|     MaintenanceType? type, | ||||
|     DateTime? scheduledDate, | ||||
|     DateTime? completedDate, | ||||
|     String? name, | ||||
|     String? description, | ||||
|     String? performedBy, | ||||
|     double? cost, | ||||
|     String? notes, | ||||
|     DateTime? createdAt, | ||||
|     DateTime? updatedAt, | ||||
|   }) { | ||||
|     return MaintenanceModel( | ||||
|       id: id ?? this.id, | ||||
|       equipmentIds: equipmentIds ?? this.equipmentIds, | ||||
|       type: type ?? this.type, | ||||
|       scheduledDate: scheduledDate ?? this.scheduledDate, | ||||
|       completedDate: completedDate ?? this.completedDate, | ||||
|       name: name ?? this.name, | ||||
|       description: description ?? this.description, | ||||
|       performedBy: performedBy ?? this.performedBy, | ||||
|       cost: cost ?? this.cost, | ||||
|       notes: notes ?? this.notes, | ||||
|       createdAt: createdAt ?? this.createdAt, | ||||
|       updatedAt: updatedAt ?? this.updatedAt, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   // Helper pour vérifier si la maintenance est complétée | ||||
|   bool get isCompleted => completedDate != null; | ||||
|  | ||||
|   // Helper pour vérifier si la maintenance est en retard | ||||
|   bool get isOverdue { | ||||
|     if (isCompleted) return false; | ||||
|     return scheduledDate.isBefore(DateTime.now()); | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										128
									
								
								em2rp/lib/providers/alert_provider.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								em2rp/lib/providers/alert_provider.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:cloud_firestore/cloud_firestore.dart'; | ||||
| import 'package:em2rp/models/alert_model.dart'; | ||||
|  | ||||
| class AlertProvider extends ChangeNotifier { | ||||
|   final FirebaseFirestore _firestore = FirebaseFirestore.instance; | ||||
|  | ||||
|   List<AlertModel> _alerts = []; | ||||
|  | ||||
|   // Getters | ||||
|   List<AlertModel> get alerts => _alerts; | ||||
|  | ||||
|   /// Nombre d'alertes non lues | ||||
|   int get unreadCount => _alerts.where((alert) => !alert.isRead).length; | ||||
|  | ||||
|   /// Alertes non lues uniquement | ||||
|   List<AlertModel> get unreadAlerts => _alerts.where((alert) => !alert.isRead).toList(); | ||||
|  | ||||
|   /// Alertes de stock critique | ||||
|   List<AlertModel> get lowStockAlerts => _alerts.where((alert) => alert.type == AlertType.lowStock).toList(); | ||||
|  | ||||
|   /// Alertes de maintenance | ||||
|   List<AlertModel> get maintenanceAlerts => _alerts.where((alert) => alert.type == AlertType.maintenanceDue).toList(); | ||||
|  | ||||
|   /// Alertes de conflit | ||||
|   List<AlertModel> get conflictAlerts => _alerts.where((alert) => alert.type == AlertType.conflict).toList(); | ||||
|  | ||||
|   /// Stream des alertes | ||||
|   Stream<List<AlertModel>> get alertsStream { | ||||
|     return _firestore | ||||
|         .collection('alerts') | ||||
|         .orderBy('createdAt', descending: true) | ||||
|         .snapshots() | ||||
|         .map((snapshot) { | ||||
|       _alerts = snapshot.docs | ||||
|           .map((doc) => AlertModel.fromMap(doc.data(), doc.id)) | ||||
|           .toList(); | ||||
|       return _alerts; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /// Marquer une alerte comme lue | ||||
|   Future<void> markAsRead(String alertId) async { | ||||
|     try { | ||||
|       await _firestore.collection('alerts').doc(alertId).update({ | ||||
|         'isRead': true, | ||||
|       }); | ||||
|       notifyListeners(); | ||||
|     } catch (e) { | ||||
|       print('Error marking alert as read: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Marquer toutes les alertes comme lues | ||||
|   Future<void> markAllAsRead() async { | ||||
|     try { | ||||
|       final batch = _firestore.batch(); | ||||
|  | ||||
|       for (var alert in _alerts.where((a) => !a.isRead)) { | ||||
|         batch.update( | ||||
|           _firestore.collection('alerts').doc(alert.id), | ||||
|           {'isRead': true}, | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       await batch.commit(); | ||||
|       notifyListeners(); | ||||
|     } catch (e) { | ||||
|       print('Error marking all alerts as read: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Supprimer une alerte | ||||
|   Future<void> deleteAlert(String alertId) async { | ||||
|     try { | ||||
|       await _firestore.collection('alerts').doc(alertId).delete(); | ||||
|       notifyListeners(); | ||||
|     } catch (e) { | ||||
|       print('Error deleting alert: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Supprimer toutes les alertes lues | ||||
|   Future<void> deleteReadAlerts() async { | ||||
|     try { | ||||
|       final batch = _firestore.batch(); | ||||
|  | ||||
|       for (var alert in _alerts.where((a) => a.isRead)) { | ||||
|         batch.delete(_firestore.collection('alerts').doc(alert.id)); | ||||
|       } | ||||
|  | ||||
|       await batch.commit(); | ||||
|       notifyListeners(); | ||||
|     } catch (e) { | ||||
|       print('Error deleting read alerts: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Créer une alerte (utilisé principalement par les services) | ||||
|   Future<void> createAlert(AlertModel alert) async { | ||||
|     try { | ||||
|       await _firestore.collection('alerts').doc(alert.id).set(alert.toMap()); | ||||
|       notifyListeners(); | ||||
|     } catch (e) { | ||||
|       print('Error creating alert: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Récupérer les alertes pour un équipement spécifique | ||||
|   Stream<List<AlertModel>> getAlertsForEquipment(String equipmentId) { | ||||
|     return _firestore | ||||
|         .collection('alerts') | ||||
|         .where('equipmentId', isEqualTo: equipmentId) | ||||
|         .orderBy('createdAt', descending: true) | ||||
|         .snapshots() | ||||
|         .map((snapshot) { | ||||
|       return snapshot.docs | ||||
|           .map((doc) => AlertModel.fromMap(doc.data(), doc.id)) | ||||
|           .toList(); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										217
									
								
								em2rp/lib/providers/equipment_provider.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								em2rp/lib/providers/equipment_provider.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,217 @@ | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:em2rp/models/equipment_model.dart'; | ||||
| import 'package:em2rp/services/equipment_service.dart'; | ||||
|  | ||||
| class EquipmentProvider extends ChangeNotifier { | ||||
|   final EquipmentService _service = EquipmentService(); | ||||
|  | ||||
|   List<EquipmentModel> _equipment = []; | ||||
|   List<String> _models = []; | ||||
|   List<String> _brands = []; | ||||
|  | ||||
|   EquipmentCategory? _selectedCategory; | ||||
|   EquipmentStatus? _selectedStatus; | ||||
|   String? _selectedModel; | ||||
|   String _searchQuery = ''; | ||||
|  | ||||
|   // Getters | ||||
|   List<EquipmentModel> get equipment => _equipment; | ||||
|   List<String> get models => _models; | ||||
|   List<String> get brands => _brands; | ||||
|   EquipmentCategory? get selectedCategory => _selectedCategory; | ||||
|   EquipmentStatus? get selectedStatus => _selectedStatus; | ||||
|   String? get selectedModel => _selectedModel; | ||||
|   String get searchQuery => _searchQuery; | ||||
|  | ||||
|   /// Stream des équipements avec filtres appliqués | ||||
|   Stream<List<EquipmentModel>> get equipmentStream { | ||||
|     return _service.getEquipment( | ||||
|       category: _selectedCategory, | ||||
|       status: _selectedStatus, | ||||
|       model: _selectedModel, | ||||
|       searchQuery: _searchQuery, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   /// Charger tous les modèles uniques | ||||
|   Future<void> loadModels() async { | ||||
|     try { | ||||
|       _models = await _service.getAllModels(); | ||||
|       notifyListeners(); | ||||
|     } catch (e) { | ||||
|       print('Error loading models: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Charger toutes les marques uniques | ||||
|   Future<void> loadBrands() async { | ||||
|     try { | ||||
|       _brands = await _service.getAllBrands(); | ||||
|       notifyListeners(); | ||||
|     } catch (e) { | ||||
|       print('Error loading brands: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Charger les modèles filtrés par marque | ||||
|   Future<List<String>> loadModelsByBrand(String brand) async { | ||||
|     try { | ||||
|       return await _service.getModelsByBrand(brand); | ||||
|     } catch (e) { | ||||
|       print('Error loading models by brand: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /// Ajouter un équipement | ||||
|   Future<void> addEquipment(EquipmentModel equipment) async { | ||||
|     try { | ||||
|       await _service.createEquipment(equipment); | ||||
|  | ||||
|       // Recharger les modèles si un nouveau modèle a été ajouté | ||||
|       if (equipment.model != null && !_models.contains(equipment.model)) { | ||||
|         await loadModels(); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       print('Error adding equipment: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Mettre à jour un équipement | ||||
|   Future<void> updateEquipment(String id, Map<String, dynamic> data) async { | ||||
|     try { | ||||
|       await _service.updateEquipment(id, data); | ||||
|  | ||||
|       // Recharger les modèles si le modèle a changé | ||||
|       if (data.containsKey('model')) { | ||||
|         await loadModels(); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       print('Error updating equipment: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Supprimer un équipement | ||||
|   Future<void> deleteEquipment(String id) async { | ||||
|     try { | ||||
|       await _service.deleteEquipment(id); | ||||
|     } catch (e) { | ||||
|       print('Error deleting equipment: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Récupérer un équipement par ID | ||||
|   Future<EquipmentModel?> getEquipmentById(String id) async { | ||||
|     try { | ||||
|       return await _service.getEquipmentById(id); | ||||
|     } catch (e) { | ||||
|       print('Error getting equipment: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Trouver des alternatives disponibles | ||||
|   Future<List<EquipmentModel>> findAlternatives( | ||||
|     String model, | ||||
|     DateTime startDate, | ||||
|     DateTime endDate, | ||||
|   ) async { | ||||
|     try { | ||||
|       return await _service.findAlternatives(model, startDate, endDate); | ||||
|     } catch (e) { | ||||
|       print('Error finding alternatives: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Vérifier la disponibilité d'un équipement | ||||
|   Future<List<String>> checkAvailability( | ||||
|     String equipmentId, | ||||
|     DateTime startDate, | ||||
|     DateTime endDate, | ||||
|   ) async { | ||||
|     try { | ||||
|       return await _service.checkAvailability(equipmentId, startDate, endDate); | ||||
|     } catch (e) { | ||||
|       print('Error checking availability: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Mettre à jour le stock d'un consommable | ||||
|   Future<void> updateStock(String id, int quantityChange) async { | ||||
|     try { | ||||
|       await _service.updateStock(id, quantityChange); | ||||
|     } catch (e) { | ||||
|       print('Error updating stock: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Vérifier les stocks critiques | ||||
|   Future<void> checkCriticalStock() async { | ||||
|     try { | ||||
|       await _service.checkCriticalStock(); | ||||
|     } catch (e) { | ||||
|       print('Error checking critical stock: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Générer les données du QR code | ||||
|   String generateQRCodeData(String equipmentId) { | ||||
|     return _service.generateQRCodeData(equipmentId); | ||||
|   } | ||||
|  | ||||
|   /// Vérifier si un ID est unique | ||||
|   Future<bool> isIdUnique(String id) async { | ||||
|     try { | ||||
|       return await _service.isIdUnique(id); | ||||
|     } catch (e) { | ||||
|       print('Error checking ID uniqueness: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // === FILTRES === | ||||
|  | ||||
|   /// Définir la catégorie sélectionnée | ||||
|   void setSelectedCategory(EquipmentCategory? category) { | ||||
|     _selectedCategory = category; | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   /// Définir le statut sélectionné | ||||
|   void setSelectedStatus(EquipmentStatus? status) { | ||||
|     _selectedStatus = status; | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   /// Définir le modèle sélectionné | ||||
|   void setSelectedModel(String? model) { | ||||
|     _selectedModel = model; | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   /// Définir la recherche | ||||
|   void setSearchQuery(String query) { | ||||
|     _searchQuery = query; | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   /// Réinitialiser tous les filtres | ||||
|   void resetFilters() { | ||||
|     _selectedCategory = null; | ||||
|     _selectedStatus = null; | ||||
|     _selectedModel = null; | ||||
|     _searchQuery = ''; | ||||
|     notifyListeners(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -30,9 +30,7 @@ class EventProvider with ChangeNotifier { | ||||
|       for (var doc in eventsSnapshot.docs) { | ||||
|         try { | ||||
|           final data = doc.data() as Map<String, dynamic>; | ||||
|           print('Processing event ${doc.id}: ${data['Name'] ?? 'Unknown'}'); | ||||
|  | ||||
|           final event = EventModel.fromMap(data, doc.id); | ||||
|                     final event = EventModel.fromMap(data, doc.id); | ||||
|           allEvents.add(event); | ||||
|         } catch (e) { | ||||
|           print('Failed to parse event ${doc.id}: $e'); | ||||
| @@ -41,8 +39,6 @@ class EventProvider with ChangeNotifier { | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       print('Successfully parsed ${allEvents.length} events, failed: $failedCount'); | ||||
|  | ||||
|       // Filtrage amélioré pour les utilisateurs non-admin | ||||
|       if (canViewAllEvents) { | ||||
|         _events = allEvents; | ||||
| @@ -63,8 +59,6 @@ class EventProvider with ChangeNotifier { | ||||
|  | ||||
|           return isInWorkforce; | ||||
|         }).toList(); | ||||
|  | ||||
|         print('Non-admin user: showing ${_events.length} events out of ${allEvents.length}'); | ||||
|       } | ||||
|  | ||||
|       _isLoading = false; | ||||
|   | ||||
							
								
								
									
										106
									
								
								em2rp/lib/providers/maintenance_provider.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								em2rp/lib/providers/maintenance_provider.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:em2rp/models/maintenance_model.dart'; | ||||
| import 'package:em2rp/services/maintenance_service.dart'; | ||||
|  | ||||
| class MaintenanceProvider extends ChangeNotifier { | ||||
|   final MaintenanceService _service = MaintenanceService(); | ||||
|  | ||||
|   List<MaintenanceModel> _maintenances = []; | ||||
|  | ||||
|   // Getters | ||||
|   List<MaintenanceModel> get maintenances => _maintenances; | ||||
|  | ||||
|   /// Stream des maintenances pour un équipement spécifique | ||||
|   Stream<List<MaintenanceModel>> getMaintenancesStream(String equipmentId) { | ||||
|     return _service.getMaintenances(equipmentId); | ||||
|   } | ||||
|  | ||||
|   /// Stream de toutes les maintenances | ||||
|   Stream<List<MaintenanceModel>> get allMaintenancesStream { | ||||
|     return _service.getAllMaintenances(); | ||||
|   } | ||||
|  | ||||
|   /// Créer une nouvelle maintenance | ||||
|   Future<void> createMaintenance(MaintenanceModel maintenance) async { | ||||
|     try { | ||||
|       await _service.createMaintenance(maintenance); | ||||
|       notifyListeners(); | ||||
|     } catch (e) { | ||||
|       print('Error creating maintenance: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Mettre à jour une maintenance | ||||
|   Future<void> updateMaintenance(String id, Map<String, dynamic> data) async { | ||||
|     try { | ||||
|       await _service.updateMaintenance(id, data); | ||||
|       notifyListeners(); | ||||
|     } catch (e) { | ||||
|       print('Error updating maintenance: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Supprimer une maintenance | ||||
|   Future<void> deleteMaintenance(String id) async { | ||||
|     try { | ||||
|       await _service.deleteMaintenance(id); | ||||
|       notifyListeners(); | ||||
|     } catch (e) { | ||||
|       print('Error deleting maintenance: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Récupérer une maintenance par ID | ||||
|   Future<MaintenanceModel?> getMaintenanceById(String id) async { | ||||
|     try { | ||||
|       return await _service.getMaintenanceById(id); | ||||
|     } catch (e) { | ||||
|       print('Error getting maintenance: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Marquer une maintenance comme complétée | ||||
|   Future<void> completeMaintenance( | ||||
|     String id, { | ||||
|     String? performedBy, | ||||
|     double? cost, | ||||
|   }) async { | ||||
|     try { | ||||
|       await _service.completeMaintenance(id, performedBy: performedBy, cost: cost); | ||||
|       notifyListeners(); | ||||
|     } catch (e) { | ||||
|       print('Error completing maintenance: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Vérifier les maintenances à venir | ||||
|   Future<void> checkUpcomingMaintenances() async { | ||||
|     try { | ||||
|       await _service.checkUpcomingMaintenances(); | ||||
|     } catch (e) { | ||||
|       print('Error checking upcoming maintenances: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Récupérer les maintenances en retard | ||||
|   List<MaintenanceModel> get overdueMaintances { | ||||
|     return _maintenances.where((m) => m.isOverdue).toList(); | ||||
|   } | ||||
|  | ||||
|   /// Récupérer les maintenances complétées | ||||
|   List<MaintenanceModel> get completedMaintenances { | ||||
|     return _maintenances.where((m) => m.isCompleted).toList(); | ||||
|   } | ||||
|  | ||||
|   /// Récupérer les maintenances à venir | ||||
|   List<MaintenanceModel> get upcomingMaintenances { | ||||
|     return _maintenances.where((m) => !m.isCompleted && !m.isOverdue).toList(); | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										373
									
								
								em2rp/lib/services/equipment_service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										373
									
								
								em2rp/lib/services/equipment_service.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,373 @@ | ||||
| import 'package:cloud_firestore/cloud_firestore.dart'; | ||||
| import 'package:em2rp/models/equipment_model.dart'; | ||||
| import 'package:em2rp/models/alert_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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										363
									
								
								em2rp/lib/services/event_preparation_service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										363
									
								
								em2rp/lib/services/event_preparation_service.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,363 @@ | ||||
| import 'package:cloud_firestore/cloud_firestore.dart'; | ||||
| import 'package:em2rp/models/event_model.dart'; | ||||
| import 'package:em2rp/models/equipment_model.dart'; | ||||
| import 'package:em2rp/services/equipment_service.dart'; | ||||
|  | ||||
| class EventPreparationService { | ||||
|   final FirebaseFirestore _firestore = FirebaseFirestore.instance; | ||||
|   final EquipmentService _equipmentService = EquipmentService(); | ||||
|  | ||||
|   // Collection references | ||||
|   CollectionReference get _eventsCollection => _firestore.collection('events'); | ||||
|   CollectionReference get _equipmentCollection => _firestore.collection('equipment'); | ||||
|  | ||||
|   // === PRÉPARATION === | ||||
|  | ||||
|    /// Valider un équipement individuel en préparation | ||||
|   Future<void> validateEquipmentPreparation(String eventId, String equipmentId) async { | ||||
|     try { | ||||
|       final event = await _getEvent(eventId); | ||||
|       if (event == null) { | ||||
|         throw Exception('Event not found'); | ||||
|       } | ||||
|  | ||||
|       // Mettre à jour le statut de l'équipement dans la liste | ||||
|       final updatedEquipment = event.assignedEquipment.map((eq) { | ||||
|         if (eq.equipmentId == equipmentId) { | ||||
|           return eq.copyWith(isPrepared: true); | ||||
|         } | ||||
|         return eq; | ||||
|       }).toList(); | ||||
|  | ||||
|       await _eventsCollection.doc(eventId).update({ | ||||
|         'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(), | ||||
|       }); | ||||
|     } catch (e) { | ||||
|       print('Error validating equipment preparation: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Valider tous les équipements en préparation | ||||
|   Future<void> validateAllPreparation(String eventId) async { | ||||
|     try { | ||||
|       final event = await _getEvent(eventId); | ||||
|       if (event == null) { | ||||
|         throw Exception('Event not found'); | ||||
|       } | ||||
|  | ||||
|       // Marquer tous les équipements comme préparés | ||||
|       final updatedEquipment = event.assignedEquipment.map((eq) { | ||||
|         return eq.copyWith(isPrepared: true); | ||||
|       }).toList(); | ||||
|  | ||||
|       await _eventsCollection.doc(eventId).update({ | ||||
|         'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(), | ||||
|         'preparationStatus': preparationStatusToString(PreparationStatus.completed), | ||||
|       }); | ||||
|  | ||||
|       // Mettre à jour le statut des équipements à "inUse" | ||||
|       for (var equipment in event.assignedEquipment) { | ||||
|         await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       print('Error validating all preparation: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Finaliser la préparation avec des équipements manquants | ||||
|   Future<void> completePreparationWithMissing( | ||||
|     String eventId, | ||||
|     List<String> missingEquipmentIds, | ||||
|   ) async { | ||||
|     try { | ||||
|       final event = await _getEvent(eventId); | ||||
|       if (event == null) { | ||||
|         throw Exception('Event not found'); | ||||
|       } | ||||
|  | ||||
|       // Marquer comme complété avec manquants | ||||
|       await _eventsCollection.doc(eventId).update({ | ||||
|         'preparationStatus': preparationStatusToString(PreparationStatus.completedWithMissing), | ||||
|       }); | ||||
|  | ||||
|       // Mettre à jour le statut des équipements préparés à "inUse" | ||||
|       for (var equipment in event.assignedEquipment) { | ||||
|         if (equipment.isPrepared && !missingEquipmentIds.contains(equipment.equipmentId)) { | ||||
|           await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse); | ||||
|         } | ||||
|       } | ||||
|     } catch (e) { | ||||
|       print('Error completing preparation with missing: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // === RETOUR === | ||||
|  | ||||
|  | ||||
|     /// Valider le retour d'un équipement individuel | ||||
|   Future<void> validateEquipmentReturn( | ||||
|     String eventId, | ||||
|     String equipmentId, { | ||||
|     int? returnedQuantity, | ||||
|   }) async { | ||||
|     try { | ||||
|       final event = await _getEvent(eventId); | ||||
|       if (event == null) { | ||||
|         throw Exception('Event not found'); | ||||
|       } | ||||
|  | ||||
|       // Mettre à jour le statut de l'équipement dans la liste | ||||
|       final updatedEquipment = event.assignedEquipment.map((eq) { | ||||
|         if (eq.equipmentId == equipmentId) { | ||||
|           return eq.copyWith( | ||||
|             isReturned: true, | ||||
|             returnedQuantity: returnedQuantity, | ||||
|           ); | ||||
|         } | ||||
|         return eq; | ||||
|       }).toList(); | ||||
|  | ||||
|       await _eventsCollection.doc(eventId).update({ | ||||
|         'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(), | ||||
|       }); | ||||
|  | ||||
|       // Mettre à jour le stock si c'est un consommable | ||||
|       if (returnedQuantity != null) { | ||||
|         final equipmentDoc = await _equipmentCollection.doc(equipmentId).get(); | ||||
|         if (equipmentDoc.exists) { | ||||
|           final equipment = EquipmentModel.fromMap( | ||||
|             equipmentDoc.data() as Map<String, dynamic>, | ||||
|             equipmentDoc.id, | ||||
|           ); | ||||
|  | ||||
|           if (equipment.hasQuantity) { | ||||
|             final currentAvailable = equipment.availableQuantity ?? 0; | ||||
|             await _equipmentCollection.doc(equipmentId).update({ | ||||
|               'availableQuantity': currentAvailable + returnedQuantity, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } catch (e) { | ||||
|       print('Error validating equipment return: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Valider tous les retours | ||||
|   Future<void> validateAllReturn(String eventId) async { | ||||
|     try { | ||||
|       final event = await _getEvent(eventId); | ||||
|       if (event == null) { | ||||
|         throw Exception('Event not found'); | ||||
|       } | ||||
|  | ||||
|       // Marquer tous les équipements comme retournés | ||||
|       final updatedEquipment = event.assignedEquipment.map((eq) { | ||||
|         return eq.copyWith( | ||||
|           isReturned: true, | ||||
|           returnedQuantity: eq.returnedQuantity ?? eq.quantity, | ||||
|         ); | ||||
|       }).toList(); | ||||
|  | ||||
|       await _eventsCollection.doc(eventId).update({ | ||||
|         'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(), | ||||
|         'returnStatus': returnStatusToString(ReturnStatus.completed), | ||||
|       }); | ||||
|  | ||||
|       // Mettre à jour le statut des équipements à "available" et gérer les stocks | ||||
|       for (var equipment in updatedEquipment) { | ||||
|         await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available); | ||||
|  | ||||
|         // Restaurer le stock pour les consommables | ||||
|         final equipmentDoc = await _equipmentCollection.doc(equipment.equipmentId).get(); | ||||
|         if (equipmentDoc.exists) { | ||||
|           final equipmentData = EquipmentModel.fromMap( | ||||
|             equipmentDoc.data() as Map<String, dynamic>, | ||||
|             equipmentDoc.id, | ||||
|           ); | ||||
|  | ||||
|           if (equipmentData.hasQuantity && equipment.returnedQuantity != null) { | ||||
|             final currentAvailable = equipmentData.availableQuantity ?? 0; | ||||
|             await _equipmentCollection.doc(equipment.equipmentId).update({ | ||||
|               'availableQuantity': currentAvailable + equipment.returnedQuantity!, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } catch (e) { | ||||
|       print('Error validating all return: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Finaliser le retour avec des équipements manquants | ||||
|   Future<void> completeReturnWithMissing( | ||||
|     String eventId, | ||||
|     List<String> missingEquipmentIds, | ||||
|   ) async { | ||||
|     try { | ||||
|       final event = await _getEvent(eventId); | ||||
|       if (event == null) { | ||||
|         throw Exception('Event not found'); | ||||
|       } | ||||
|  | ||||
|       // Marquer comme complété avec manquants | ||||
|       await _eventsCollection.doc(eventId).update({ | ||||
|         'returnStatus': returnStatusToString(ReturnStatus.completedWithMissing), | ||||
|       }); | ||||
|  | ||||
|       // Mettre à jour le statut des équipements retournés à "available" | ||||
|       for (var equipment in event.assignedEquipment) { | ||||
|         if (equipment.isReturned && !missingEquipmentIds.contains(equipment.equipmentId)) { | ||||
|           await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available); | ||||
|  | ||||
|           // Restaurer le stock pour les consommables | ||||
|           final equipmentDoc = await _equipmentCollection.doc(equipment.equipmentId).get(); | ||||
|           if (equipmentDoc.exists) { | ||||
|             final equipmentData = EquipmentModel.fromMap( | ||||
|               equipmentDoc.data() as Map<String, dynamic>, | ||||
|               equipmentDoc.id, | ||||
|             ); | ||||
|  | ||||
|             if (equipmentData.hasQuantity && equipment.returnedQuantity != null) { | ||||
|               final currentAvailable = equipmentData.availableQuantity ?? 0; | ||||
|               await _equipmentCollection.doc(equipment.equipmentId).update({ | ||||
|                 'availableQuantity': currentAvailable + equipment.returnedQuantity!, | ||||
|               }); | ||||
|             } | ||||
|           } | ||||
|         } else if (missingEquipmentIds.contains(equipment.equipmentId)) { | ||||
|           // Marquer comme perdu | ||||
|           await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.lost); | ||||
|         } | ||||
|       } | ||||
|     } catch (e) { | ||||
|       print('Error completing return with missing: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // === HELPERS === | ||||
|  | ||||
|   /// Mettre à jour le statut d'un équipement | ||||
|   Future<void> updateEquipmentStatus(String equipmentId, EquipmentStatus status) async { | ||||
|     try { | ||||
|       await _equipmentCollection.doc(equipmentId).update({ | ||||
|         'status': equipmentStatusToString(status), | ||||
|         'updatedAt': Timestamp.fromDate(DateTime.now()), | ||||
|       }); | ||||
|     } catch (e) { | ||||
|       print('Error updating equipment status: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Récupérer un événement | ||||
|   Future<EventModel?> _getEvent(String eventId) async { | ||||
|     try { | ||||
|       final doc = await _eventsCollection.doc(eventId).get(); | ||||
|       if (doc.exists) { | ||||
|         return EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id); | ||||
|       } | ||||
|       return null; | ||||
|     } catch (e) { | ||||
|       print('Error getting event: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Ajouter un équipement à un événement | ||||
|   Future<void> addEquipmentToEvent( | ||||
|     String eventId, | ||||
|     String equipmentId, { | ||||
|     int quantity = 1, | ||||
|   }) async { | ||||
|     try { | ||||
|       final event = await _getEvent(eventId); | ||||
|       if (event == null) { | ||||
|         throw Exception('Event not found'); | ||||
|       } | ||||
|  | ||||
|       // Vérifier que l'équipement n'est pas déjà ajouté | ||||
|       final alreadyAdded = event.assignedEquipment.any((eq) => eq.equipmentId == equipmentId); | ||||
|       if (alreadyAdded) { | ||||
|         throw Exception('Equipment already added to event'); | ||||
|       } | ||||
|  | ||||
|       final newEquipment = EventEquipment( | ||||
|         equipmentId: equipmentId, | ||||
|         quantity: quantity, | ||||
|       ); | ||||
|  | ||||
|       final updatedEquipment = [...event.assignedEquipment, newEquipment]; | ||||
|  | ||||
|       await _eventsCollection.doc(eventId).update({ | ||||
|         'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(), | ||||
|       }); | ||||
|  | ||||
|       // Décrémenter le stock pour les consommables | ||||
|       final equipmentDoc = await _equipmentCollection.doc(equipmentId).get(); | ||||
|       if (equipmentDoc.exists) { | ||||
|         final equipmentData = EquipmentModel.fromMap( | ||||
|           equipmentDoc.data() as Map<String, dynamic>, | ||||
|           equipmentDoc.id, | ||||
|         ); | ||||
|  | ||||
|         if (equipmentData.hasQuantity) { | ||||
|           final currentAvailable = equipmentData.availableQuantity ?? 0; | ||||
|           await _equipmentCollection.doc(equipmentId).update({ | ||||
|             'availableQuantity': currentAvailable - quantity, | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|     } catch (e) { | ||||
|       print('Error adding equipment to event: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Retirer un équipement d'un événement | ||||
|   Future<void> removeEquipmentFromEvent(String eventId, String equipmentId) async { | ||||
|     try { | ||||
|       final event = await _getEvent(eventId); | ||||
|       if (event == null) { | ||||
|         throw Exception('Event not found'); | ||||
|       } | ||||
|  | ||||
|       final equipmentToRemove = event.assignedEquipment.firstWhere( | ||||
|         (eq) => eq.equipmentId == equipmentId, | ||||
|       ); | ||||
|  | ||||
|       final updatedEquipment = event.assignedEquipment | ||||
|           .where((eq) => eq.equipmentId != equipmentId) | ||||
|           .toList(); | ||||
|  | ||||
|       await _eventsCollection.doc(eventId).update({ | ||||
|         'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(), | ||||
|       }); | ||||
|  | ||||
|       // Restaurer le stock pour les consommables | ||||
|       final equipmentDoc = await _equipmentCollection.doc(equipmentId).get(); | ||||
|       if (equipmentDoc.exists) { | ||||
|         final equipmentData = EquipmentModel.fromMap( | ||||
|           equipmentDoc.data() as Map<String, dynamic>, | ||||
|           equipmentDoc.id, | ||||
|         ); | ||||
|  | ||||
|         if (equipmentData.hasQuantity) { | ||||
|           final currentAvailable = equipmentData.availableQuantity ?? 0; | ||||
|           await _equipmentCollection.doc(equipmentId).update({ | ||||
|             'availableQuantity': currentAvailable + equipmentToRemove.quantity, | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|     } catch (e) { | ||||
|       print('Error removing equipment from event: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										281
									
								
								em2rp/lib/services/maintenance_service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										281
									
								
								em2rp/lib/services/maintenance_service.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,281 @@ | ||||
| import 'package:cloud_firestore/cloud_firestore.dart'; | ||||
| import 'package:em2rp/models/maintenance_model.dart'; | ||||
| import 'package:em2rp/models/alert_model.dart'; | ||||
| import 'package:em2rp/models/equipment_model.dart'; | ||||
| import 'package:em2rp/services/equipment_service.dart'; | ||||
|  | ||||
| class MaintenanceService { | ||||
|   final FirebaseFirestore _firestore = FirebaseFirestore.instance; | ||||
|   final EquipmentService _equipmentService = EquipmentService(); | ||||
|  | ||||
|   // Collection references | ||||
|   CollectionReference get _maintenancesCollection => _firestore.collection('maintenances'); | ||||
|   CollectionReference get _equipmentCollection => _firestore.collection('equipment'); | ||||
|   CollectionReference get _alertsCollection => _firestore.collection('alerts'); | ||||
|  | ||||
|   // CRUD Operations | ||||
|  | ||||
|   /// Créer une nouvelle maintenance | ||||
|   Future<void> createMaintenance(MaintenanceModel maintenance) async { | ||||
|     try { | ||||
|       await _maintenancesCollection.doc(maintenance.id).set(maintenance.toMap()); | ||||
|  | ||||
|       // Mettre à jour les équipements concernés | ||||
|       for (String equipmentId in maintenance.equipmentIds) { | ||||
|         await _updateEquipmentMaintenanceList(equipmentId, maintenance.id); | ||||
|  | ||||
|         // Si la maintenance est planifiée dans les 7 prochains jours, créer une alerte | ||||
|         if (maintenance.scheduledDate.isBefore(DateTime.now().add(const Duration(days: 7)))) { | ||||
|           await _createMaintenanceAlert(equipmentId, maintenance); | ||||
|         } | ||||
|       } | ||||
|     } catch (e) { | ||||
|       print('Error creating maintenance: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Mettre à jour une maintenance | ||||
|   Future<void> updateMaintenance(String id, Map<String, dynamic> data) async { | ||||
|     try { | ||||
|       data['updatedAt'] = Timestamp.fromDate(DateTime.now()); | ||||
|       await _maintenancesCollection.doc(id).update(data); | ||||
|     } catch (e) { | ||||
|       print('Error updating maintenance: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Supprimer une maintenance | ||||
|   Future<void> deleteMaintenance(String id) async { | ||||
|     try { | ||||
|       // Récupérer la maintenance pour connaître les équipements | ||||
|       final doc = await _maintenancesCollection.doc(id).get(); | ||||
|       if (doc.exists) { | ||||
|         final maintenance = MaintenanceModel.fromMap( | ||||
|           doc.data() as Map<String, dynamic>, | ||||
|           doc.id, | ||||
|         ); | ||||
|  | ||||
|         // Retirer la maintenance des équipements | ||||
|         for (String equipmentId in maintenance.equipmentIds) { | ||||
|           await _removeMaintenanceFromEquipment(equipmentId, id); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       await _maintenancesCollection.doc(id).delete(); | ||||
|     } catch (e) { | ||||
|       print('Error deleting maintenance: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// 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; | ||||
|     } catch (e) { | ||||
|       print('Error getting maintenance: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Récupérer l'historique des maintenances pour un équipement | ||||
|   Stream<List<MaintenanceModel>> getMaintenances(String equipmentId) { | ||||
|     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(); | ||||
|       }); | ||||
|     } catch (e) { | ||||
|       print('Error streaming maintenances: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Récupérer toutes les maintenances | ||||
|   Stream<List<MaintenanceModel>> getAllMaintenances() { | ||||
|     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(); | ||||
|       }); | ||||
|     } catch (e) { | ||||
|       print('Error streaming all maintenances: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// 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()), | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|     } catch (e) { | ||||
|       print('Error completing maintenance: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// 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); | ||||
|         } | ||||
|       } | ||||
|     } 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; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Mettre à jour la liste des maintenances d'un équipement | ||||
|   Future<void> _updateEquipmentMaintenanceList(String equipmentId, String maintenanceId) async { | ||||
|     try { | ||||
|       final equipmentDoc = await _equipmentCollection.doc(equipmentId).get(); | ||||
|       if (equipmentDoc.exists) { | ||||
|         final equipment = EquipmentModel.fromMap( | ||||
|           equipmentDoc.data() as Map<String, dynamic>, | ||||
|           equipmentDoc.id, | ||||
|         ); | ||||
|  | ||||
|         final updatedMaintenanceIds = List<String>.from(equipment.maintenanceIds); | ||||
|         if (!updatedMaintenanceIds.contains(maintenanceId)) { | ||||
|           updatedMaintenanceIds.add(maintenanceId); | ||||
|  | ||||
|           await _equipmentCollection.doc(equipmentId).update({ | ||||
|             'maintenanceIds': updatedMaintenanceIds, | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|     } catch (e) { | ||||
|       print('Error updating equipment maintenance list: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Retirer une maintenance de la liste d'un équipement | ||||
|   Future<void> _removeMaintenanceFromEquipment(String equipmentId, String maintenanceId) async { | ||||
|     try { | ||||
|       final equipmentDoc = await _equipmentCollection.doc(equipmentId).get(); | ||||
|       if (equipmentDoc.exists) { | ||||
|         final equipment = EquipmentModel.fromMap( | ||||
|           equipmentDoc.data() as Map<String, dynamic>, | ||||
|           equipmentDoc.id, | ||||
|         ); | ||||
|  | ||||
|         final updatedMaintenanceIds = List<String>.from(equipment.maintenanceIds); | ||||
|         updatedMaintenanceIds.remove(maintenanceId); | ||||
|  | ||||
|         await _equipmentCollection.doc(equipmentId).update({ | ||||
|           'maintenanceIds': updatedMaintenanceIds, | ||||
|         }); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       print('Error removing maintenance from equipment: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										133
									
								
								em2rp/lib/views/equipment_form/brand_model_selector.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								em2rp/lib/views/equipment_form/brand_model_selector.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:em2rp/providers/equipment_provider.dart'; | ||||
|  | ||||
| class BrandModelSelector extends StatefulWidget { | ||||
|   final TextEditingController brandController; | ||||
|   final TextEditingController modelController; | ||||
|   final ValueChanged<String?>? onBrandChanged; | ||||
|   final List<String> filteredModels; | ||||
|   final String? selectedBrand; | ||||
|   final Function(List<String>) onModelsChanged; | ||||
|  | ||||
|   const BrandModelSelector({ | ||||
|     super.key, | ||||
|     required this.brandController, | ||||
|     required this.modelController, | ||||
|     this.onBrandChanged, | ||||
|     required this.filteredModels, | ||||
|     required this.selectedBrand, | ||||
|     required this.onModelsChanged, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<BrandModelSelector> createState() => _BrandModelSelectorState(); | ||||
| } | ||||
|  | ||||
| class _BrandModelSelectorState extends State<BrandModelSelector> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Row( | ||||
|       children: [ | ||||
|         Expanded( | ||||
|           child: Consumer<EquipmentProvider>( | ||||
|             builder: (context, provider, child) { | ||||
|               return Autocomplete<String>( | ||||
|                 initialValue: TextEditingValue(text: widget.brandController.text), | ||||
|                 optionsBuilder: (TextEditingValue textEditingValue) { | ||||
|                   if (textEditingValue.text.isEmpty) { | ||||
|                     return provider.brands; | ||||
|                   } | ||||
|                   return provider.brands.where((String brand) { | ||||
|                     return brand.toLowerCase().contains( | ||||
|                       textEditingValue.text.toLowerCase(), | ||||
|                     ); | ||||
|                   }); | ||||
|                 }, | ||||
|                 onSelected: (String selection) async { | ||||
|                   widget.brandController.text = selection; | ||||
|                   widget.onBrandChanged?.call(selection); | ||||
|                   final equipmentProvider = Provider.of<EquipmentProvider>(context, listen: false); | ||||
|                   final models = await equipmentProvider.loadModelsByBrand(selection); | ||||
|                   widget.onModelsChanged(models); | ||||
|                 }, | ||||
|                 fieldViewBuilder: (context, controller, focusNode, onEditingComplete) { | ||||
|                   if (controller.text != widget.brandController.text) { | ||||
|                     controller.text = widget.brandController.text; | ||||
|                   } | ||||
|                   return TextFormField( | ||||
|                     controller: controller, | ||||
|                     focusNode: focusNode, | ||||
|                     decoration: const InputDecoration( | ||||
|                       labelText: 'Marque *', | ||||
|                       border: OutlineInputBorder(), | ||||
|                       prefixIcon: Icon(Icons.business), | ||||
|                       helperText: 'Champ obligatoire', | ||||
|                     ), | ||||
|                     onChanged: (value) async { | ||||
|                       widget.brandController.text = value; | ||||
|                       widget.modelController.clear(); | ||||
|                       widget.onBrandChanged?.call(value.isNotEmpty ? value : null); | ||||
|                       if (value.isNotEmpty) { | ||||
|                         final equipmentProvider = Provider.of<EquipmentProvider>(context, listen: false); | ||||
|                         final models = await equipmentProvider.loadModelsByBrand(value); | ||||
|                         widget.onModelsChanged(models); | ||||
|                       } else { | ||||
|                         widget.onModelsChanged([]); | ||||
|                       } | ||||
|                     }, | ||||
|                   ); | ||||
|                 }, | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|         ), | ||||
|         const SizedBox(width: 16), | ||||
|         Expanded( | ||||
|           child: Autocomplete<String>( | ||||
|             initialValue: TextEditingValue(text: widget.modelController.text), | ||||
|             optionsBuilder: (TextEditingValue textEditingValue) { | ||||
|               if (widget.selectedBrand == null || widget.selectedBrand!.isEmpty) { | ||||
|                 return const Iterable<String>.empty(); | ||||
|               } | ||||
|               if (textEditingValue.text.isEmpty) { | ||||
|                 return widget.filteredModels; | ||||
|               } | ||||
|               return widget.filteredModels.where((String model) { | ||||
|                 return model.toLowerCase().contains( | ||||
|                   textEditingValue.text.toLowerCase(), | ||||
|                 ); | ||||
|               }); | ||||
|             }, | ||||
|             onSelected: (String selection) { | ||||
|               widget.modelController.text = selection; | ||||
|             }, | ||||
|             fieldViewBuilder: (context, controller, focusNode, onEditingComplete) { | ||||
|               if (controller.text != widget.modelController.text) { | ||||
|                 controller.text = widget.modelController.text; | ||||
|               } | ||||
|               return TextFormField( | ||||
|                 controller: controller, | ||||
|                 focusNode: focusNode, | ||||
|                 enabled: widget.selectedBrand != null && widget.selectedBrand!.isNotEmpty, | ||||
|                 decoration: InputDecoration( | ||||
|                   labelText: 'Modèle *', | ||||
|                   border: const OutlineInputBorder(), | ||||
|                   prefixIcon: const Icon(Icons.inventory_2), | ||||
|                   hintText: widget.selectedBrand == null || widget.selectedBrand!.isEmpty | ||||
|                       ? 'Marque requise' | ||||
|                       : 'Saisissez le modèle', | ||||
|                   helperText: 'Champ obligatoire', | ||||
|                 ), | ||||
|                 onChanged: (value) { | ||||
|                   widget.modelController.text = value; | ||||
|                 }, | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										26
									
								
								em2rp/lib/views/equipment_form/id_generator.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								em2rp/lib/views/equipment_form/id_generator.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:em2rp/services/equipment_service.dart'; | ||||
|  | ||||
| class EquipmentIdGenerator { | ||||
|   static String generate({required String brand, required String model, int? number}) { | ||||
|     final brandTrim = brand.trim().replaceAll(' ', '_'); | ||||
|     final modelTrim = model.trim().replaceAll(' ', '_'); | ||||
|     if (brandTrim.isEmpty && modelTrim.isEmpty) { | ||||
|       return 'EQ-${DateTime.now().millisecondsSinceEpoch}${number != null ? '_$number' : ''}'; | ||||
|     } | ||||
|     final brandPrefix = brandTrim.length >= 4 ? brandTrim.substring(0, 4) : brandTrim; | ||||
|     String baseId = modelTrim.isNotEmpty ? '${brandPrefix}_$modelTrim' : (brandPrefix.isNotEmpty ? brandPrefix : 'EQ'); | ||||
|     if (number != null) { | ||||
|       baseId += '_#$number'; | ||||
|     } | ||||
|     return baseId; | ||||
|   } | ||||
|  | ||||
|   static Future<String> ensureUniqueId(String baseId, EquipmentService service) async { | ||||
|     if (await service.isIdUnique(baseId)) { | ||||
|       return baseId; | ||||
|     } | ||||
|     return '${baseId}_${DateTime.now().millisecondsSinceEpoch}'; | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										691
									
								
								em2rp/lib/views/equipment_form_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										691
									
								
								em2rp/lib/views/equipment_form_page.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,691 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:em2rp/models/equipment_model.dart'; | ||||
| import 'package:em2rp/providers/equipment_provider.dart'; | ||||
| import 'package:em2rp/providers/local_user_provider.dart'; | ||||
| import 'package:em2rp/services/equipment_service.dart'; | ||||
| import 'package:em2rp/utils/colors.dart'; | ||||
| import 'package:em2rp/views/widgets/nav/custom_app_bar.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
| import 'package:em2rp/views/equipment_form/brand_model_selector.dart'; | ||||
| import 'package:em2rp/views/equipment_form/id_generator.dart'; | ||||
|  | ||||
| class EquipmentFormPage extends StatefulWidget { | ||||
|   final EquipmentModel? equipment; | ||||
|  | ||||
|   const EquipmentFormPage({super.key, this.equipment}); | ||||
|  | ||||
|   @override | ||||
|   State<EquipmentFormPage> createState() => _EquipmentFormPageState(); | ||||
| } | ||||
|  | ||||
| class _EquipmentFormPageState extends State<EquipmentFormPage> { | ||||
|   final _formKey = GlobalKey<FormState>(); | ||||
|   final EquipmentService _equipmentService = EquipmentService(); | ||||
|  | ||||
|   // Controllers | ||||
|   final TextEditingController _identifierController = TextEditingController(); | ||||
|   final TextEditingController _brandController = TextEditingController(); | ||||
|   final TextEditingController _modelController = TextEditingController(); | ||||
|   final TextEditingController _purchasePriceController = TextEditingController(); | ||||
|   final TextEditingController _rentalPriceController = TextEditingController(); | ||||
|   final TextEditingController _totalQuantityController = TextEditingController(); | ||||
|   final TextEditingController _criticalThresholdController = TextEditingController(); | ||||
|   final TextEditingController _notesController = TextEditingController(); | ||||
|   final TextEditingController _quantityToAddController = TextEditingController(text: '1'); | ||||
|  | ||||
|   // State variables | ||||
|   EquipmentCategory _selectedCategory = EquipmentCategory.other; | ||||
|   EquipmentStatus _selectedStatus = EquipmentStatus.available; | ||||
|   DateTime? _purchaseDate; | ||||
|   DateTime? _lastMaintenanceDate; | ||||
|   DateTime? _nextMaintenanceDate; | ||||
|   List<String> _selectedParentBoxIds = []; | ||||
|   List<EquipmentModel> _availableBoxes = []; | ||||
|   bool _isLoading = false; | ||||
|   bool _isLoadingBoxes = true; | ||||
|   bool _addMultiple = false; | ||||
|   String? _selectedBrand; | ||||
|   List<String> _filteredModels = []; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _loadAvailableBoxes(); | ||||
|     WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|       final provider = Provider.of<EquipmentProvider>(context, listen: false); | ||||
|       provider.loadBrands(); | ||||
|       provider.loadModels(); | ||||
|     }); | ||||
|     if (widget.equipment != null) { | ||||
|       _populateFields(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _populateFields() { | ||||
|     final equipment = widget.equipment!; | ||||
|     _identifierController.text = equipment.id; | ||||
|     _brandController.text = equipment.brand ?? ''; | ||||
|     _selectedBrand = equipment.brand; | ||||
|     _modelController.text = equipment.model ?? ''; | ||||
|     _selectedCategory = equipment.category; | ||||
|     _selectedStatus = equipment.status; | ||||
|     _purchasePriceController.text = equipment.purchasePrice?.toStringAsFixed(2) ?? ''; | ||||
|     _rentalPriceController.text = equipment.rentalPrice?.toStringAsFixed(2) ?? ''; | ||||
|     _totalQuantityController.text = equipment.totalQuantity?.toString() ?? ''; | ||||
|     _criticalThresholdController.text = equipment.criticalThreshold?.toString() ?? ''; | ||||
|     _purchaseDate = equipment.purchaseDate; | ||||
|     _lastMaintenanceDate = equipment.lastMaintenanceDate; | ||||
|     _nextMaintenanceDate = equipment.nextMaintenanceDate; | ||||
|     _selectedParentBoxIds = List.from(equipment.parentBoxIds); | ||||
|     _notesController.text = equipment.notes ?? ''; | ||||
|  | ||||
|     if (_selectedBrand != null && _selectedBrand!.isNotEmpty) { | ||||
|       _loadFilteredModels(_selectedBrand!); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _loadAvailableBoxes() async { | ||||
|     try { | ||||
|       final boxes = await _equipmentService.getBoxes(); | ||||
|       setState(() { | ||||
|         _availableBoxes = boxes; | ||||
|         _isLoadingBoxes = false; | ||||
|       }); | ||||
|     } catch (e) { | ||||
|       setState(() { | ||||
|         _isLoadingBoxes = false; | ||||
|       }); | ||||
|       if (mounted) { | ||||
|         ScaffoldMessenger.of(context).showSnackBar( | ||||
|           SnackBar(content: Text('Erreur lors du chargement des boîtes : $e')), | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _loadFilteredModels(String brand) async { | ||||
|     try { | ||||
|       final equipmentProvider = Provider.of<EquipmentProvider>(context, listen: false); | ||||
|       final models = await equipmentProvider.loadModelsByBrand(brand); | ||||
|       setState(() { | ||||
|         _filteredModels = models; | ||||
|       }); | ||||
|     } catch (e) { | ||||
|       setState(() { | ||||
|         _filteredModels = []; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _identifierController.dispose(); | ||||
|     _brandController.dispose(); | ||||
|     _modelController.dispose(); | ||||
|     _purchasePriceController.dispose(); | ||||
|     _rentalPriceController.dispose(); | ||||
|     _totalQuantityController.dispose(); | ||||
|     _criticalThresholdController.dispose(); | ||||
|     _notesController.dispose(); | ||||
|     _quantityToAddController.dispose(); | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
|   bool get _isConsumable => _selectedCategory == EquipmentCategory.consumable || _selectedCategory == EquipmentCategory.cable; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final localUserProvider = Provider.of<LocalUserProvider>(context); | ||||
|     final hasManagePermission = localUserProvider.hasPermission('manage_equipment'); | ||||
|     final isEditing = widget.equipment != null; | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: CustomAppBar( | ||||
|         title: isEditing ? 'Modifier l\'équipement' : 'Nouvel équipement', | ||||
|       ), | ||||
|       body: _isLoading | ||||
|           ? const Center(child: CircularProgressIndicator()) | ||||
|           : SingleChildScrollView( | ||||
|               padding: const EdgeInsets.all(24.0), | ||||
|               child: Form( | ||||
|                 key: _formKey, | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                   children: [ | ||||
|                     // Identifiant (généré ou saisi) | ||||
|                     TextFormField( | ||||
|                       controller: _identifierController, | ||||
|                       decoration: InputDecoration( | ||||
|                         labelText: 'Identifiant *', | ||||
|                         border: const OutlineInputBorder(), | ||||
|                         prefixIcon: const Icon(Icons.tag), | ||||
|                         hintText: isEditing ? null : 'Laissez vide pour générer automatiquement', | ||||
|                         helperText: isEditing ? 'Non modifiable' : 'Format auto: {Marque4Chars}_{Modèle}', | ||||
|                       ), | ||||
|                       enabled: !isEditing, | ||||
|                     ), | ||||
|                     const SizedBox(height: 16), | ||||
|  | ||||
|                     // Case à cocher "Ajouter plusieurs" (uniquement en mode création) | ||||
|                     if (!isEditing) ...[ | ||||
|                       Row( | ||||
|                         children: [ | ||||
|                           Expanded( | ||||
|                             flex: 2, | ||||
|                             child: CheckboxListTile( | ||||
|                               title: const Text('Ajouter plusieurs équipements'), | ||||
|                               subtitle: const Text('Créer plusieurs équipements numérotés'), | ||||
|                               value: _addMultiple, | ||||
|                               contentPadding: EdgeInsets.zero, | ||||
|                               onChanged: (bool? value) { | ||||
|                                 setState(() { | ||||
|                                   _addMultiple = value ?? false; | ||||
|                                 }); | ||||
|                               }, | ||||
|                             ), | ||||
|                           ), | ||||
|                           if (_addMultiple) ...[ | ||||
|                             const SizedBox(width: 16), | ||||
|                             Expanded( | ||||
|                               child: TextFormField( | ||||
|                                 controller: _quantityToAddController, | ||||
|                                 decoration: const InputDecoration( | ||||
|                                   labelText: 'Quantité ou range', | ||||
|                                   border: OutlineInputBorder(), | ||||
|                                   prefixIcon: Icon(Icons.plus_one), | ||||
|                                   hintText: '5 ou 6-18', | ||||
|                                   helperText: 'Ex: 5 ou 6-18', | ||||
|                                 ), | ||||
|                                 keyboardType: TextInputType.text, | ||||
|                                 validator: (value) { | ||||
|                                   if (_addMultiple) { | ||||
|                                     if (value == null || value.isEmpty) return 'Requis'; | ||||
|                                     // Vérifier si c'est un nombre simple ou une range | ||||
|                                     if (value.contains('-')) { | ||||
|                                       final parts = value.split('-'); | ||||
|                                       if (parts.length != 2) return 'Format invalide'; | ||||
|                                       final start = int.tryParse(parts[0].trim()); | ||||
|                                       final end = int.tryParse(parts[1].trim()); | ||||
|                                       if (start == null || end == null) return 'Nombres invalides'; | ||||
|                                       if (start >= end) return 'Le début doit être < fin'; | ||||
|                                       if (end - start > 100) return 'Max 100 équipements'; | ||||
|                                     } else { | ||||
|                                       final num = int.tryParse(value); | ||||
|                                       if (num == null || num < 1 || num > 100) return '1-100'; | ||||
|                                     } | ||||
|                                   } | ||||
|                                   return null; | ||||
|                                 }, | ||||
|                               ), | ||||
|                             ), | ||||
|                           ], | ||||
|                         ], | ||||
|                       ), | ||||
|                       const SizedBox(height: 16), | ||||
|                     ], | ||||
|  | ||||
|                     // Sélecteur Marque/Modèle | ||||
|                     BrandModelSelector( | ||||
|                       brandController: _brandController, | ||||
|                       modelController: _modelController, | ||||
|                       selectedBrand: _selectedBrand, | ||||
|                       filteredModels: _filteredModels, | ||||
|                       onBrandChanged: (brand) { | ||||
|                         setState(() { | ||||
|                           _selectedBrand = brand; | ||||
|                         }); | ||||
|                         if (brand != null && brand.isNotEmpty) { | ||||
|                           _loadFilteredModels(brand); | ||||
|                         } else { | ||||
|                           setState(() { | ||||
|                             _filteredModels = []; | ||||
|                           }); | ||||
|                         } | ||||
|                       }, | ||||
|                       onModelsChanged: (models) { | ||||
|                         setState(() { | ||||
|                           _filteredModels = models; | ||||
|                         }); | ||||
|                       }, | ||||
|                     ), | ||||
|                     const SizedBox(height: 16), | ||||
|  | ||||
|                     // Catégorie et Statut | ||||
|                     Row( | ||||
|                       children: [ | ||||
|                         Expanded( | ||||
|                           child: DropdownButtonFormField<EquipmentCategory>( | ||||
|                             value: _selectedCategory, | ||||
|                             decoration: const InputDecoration( | ||||
|                               labelText: 'Catégorie *', | ||||
|                               border: OutlineInputBorder(), | ||||
|                               prefixIcon: Icon(Icons.category), | ||||
|                             ), | ||||
|                             items: EquipmentCategory.values.map((category) { | ||||
|                               return DropdownMenuItem( | ||||
|                                 value: category, | ||||
|                                 child: Text(_getCategoryLabel(category)), | ||||
|                               ); | ||||
|                             }).toList(), | ||||
|                             onChanged: (value) { | ||||
|                               if (value != null) { | ||||
|                                 setState(() { | ||||
|                                   _selectedCategory = value; | ||||
|                                 }); | ||||
|                               } | ||||
|                             }, | ||||
|                           ), | ||||
|                         ), | ||||
|                         // Afficher le statut uniquement si ce n'est pas un consommable ou câble | ||||
|                         if (!_isConsumable) ...[ | ||||
|                           const SizedBox(width: 16), | ||||
|                           Expanded( | ||||
|                             child: DropdownButtonFormField<EquipmentStatus>( | ||||
|                               value: _selectedStatus, | ||||
|                               decoration: const InputDecoration( | ||||
|                                 labelText: 'Statut *', | ||||
|                                 border: OutlineInputBorder(), | ||||
|                                 prefixIcon: Icon(Icons.info), | ||||
|                               ), | ||||
|                               items: EquipmentStatus.values.map((status) { | ||||
|                                 return DropdownMenuItem( | ||||
|                                   value: status, | ||||
|                                   child: Text(_getStatusLabel(status)), | ||||
|                                 ); | ||||
|                               }).toList(), | ||||
|                               onChanged: (value) { | ||||
|                                 if (value != null) { | ||||
|                                   setState(() { | ||||
|                                     _selectedStatus = value; | ||||
|                                   }); | ||||
|                                 } | ||||
|                               }, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ], | ||||
|                     ), | ||||
|                     const SizedBox(height: 16), | ||||
|  | ||||
|                     // Prix | ||||
|                     if (hasManagePermission) ...[ | ||||
|                       Row( | ||||
|                         children: [ | ||||
|                           Expanded( | ||||
|                             child: TextFormField( | ||||
|                               controller: _purchasePriceController, | ||||
|                               decoration: const InputDecoration( | ||||
|                                 labelText: 'Prix d\'achat (€)', | ||||
|                                 border: OutlineInputBorder(), | ||||
|                                 prefixIcon: Icon(Icons.euro), | ||||
|                               ), | ||||
|                               keyboardType: const TextInputType.numberWithOptions(decimal: true), | ||||
|                               inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))], | ||||
|                             ), | ||||
|                           ), | ||||
|                           const SizedBox(width: 16), | ||||
|                           Expanded( | ||||
|                             child: TextFormField( | ||||
|                               controller: _rentalPriceController, | ||||
|                               decoration: const InputDecoration( | ||||
|                                 labelText: 'Prix de location (€)', | ||||
|                                 border: OutlineInputBorder(), | ||||
|                                 prefixIcon: Icon(Icons.attach_money), | ||||
|                               ), | ||||
|                               keyboardType: const TextInputType.numberWithOptions(decimal: true), | ||||
|                               inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))], | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                       const SizedBox(height: 16), | ||||
|                     ], | ||||
|  | ||||
|                     // Quantités pour consommables | ||||
|                     if (_isConsumable) ...[ | ||||
|                       const Divider(), | ||||
|                       const Text('Gestion des quantités', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), | ||||
|                       const SizedBox(height: 16), | ||||
|                       Row( | ||||
|                         children: [ | ||||
|                           Expanded( | ||||
|                             child: TextFormField( | ||||
|                               controller: _totalQuantityController, | ||||
|                               decoration: const InputDecoration( | ||||
|                                 labelText: 'Quantité totale', | ||||
|                                 border: OutlineInputBorder(), | ||||
|                                 prefixIcon: Icon(Icons.inventory), | ||||
|                               ), | ||||
|                               keyboardType: TextInputType.number, | ||||
|                               inputFormatters: [FilteringTextInputFormatter.digitsOnly], | ||||
|                             ), | ||||
|                           ), | ||||
|                           const SizedBox(width: 16), | ||||
|                           Expanded( | ||||
|                             child: TextFormField( | ||||
|                               controller: _criticalThresholdController, | ||||
|                               decoration: const InputDecoration( | ||||
|                                 labelText: 'Seuil critique', | ||||
|                                 border: OutlineInputBorder(), | ||||
|                                 prefixIcon: Icon(Icons.warning), | ||||
|                               ), | ||||
|                               keyboardType: TextInputType.number, | ||||
|                               inputFormatters: [FilteringTextInputFormatter.digitsOnly], | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                       const SizedBox(height: 16), | ||||
|                     ], | ||||
|  | ||||
|                     // Boîtes parentes | ||||
|                     const Divider(), | ||||
|                     const Text('Boîtes parentes', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), | ||||
|                     const SizedBox(height: 8), | ||||
|                     _isLoadingBoxes | ||||
|                         ? const Center(child: CircularProgressIndicator()) | ||||
|                         : _buildParentBoxesSelector(), | ||||
|                     const SizedBox(height: 16), | ||||
|  | ||||
|                     // Dates | ||||
|                     const Divider(), | ||||
|                     const Text('Dates', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), | ||||
|                     const SizedBox(height: 16), | ||||
|                     _buildDateField(label: 'Date d\'achat', icon: Icons.shopping_cart, value: _purchaseDate, onTap: () => _selectDate(context, 'purchase')), | ||||
|                     const SizedBox(height: 16), | ||||
|                     _buildDateField(label: 'Dernière maintenance', icon: Icons.build, value: _lastMaintenanceDate, onTap: () => _selectDate(context, 'lastMaintenance')), | ||||
|                     const SizedBox(height: 16), | ||||
|                     _buildDateField(label: 'Prochaine maintenance', icon: Icons.event, value: _nextMaintenanceDate, onTap: () => _selectDate(context, 'nextMaintenance')), | ||||
|                     const SizedBox(height: 16), | ||||
|  | ||||
|                     // Notes | ||||
|                     const Divider(), | ||||
|                     TextFormField( | ||||
|                       controller: _notesController, | ||||
|                       decoration: const InputDecoration( | ||||
|                         labelText: 'Notes', | ||||
|                         border: OutlineInputBorder(), | ||||
|                         prefixIcon: Icon(Icons.notes), | ||||
|                       ), | ||||
|                       maxLines: 3, | ||||
|                     ), | ||||
|                     const SizedBox(height: 24), | ||||
|  | ||||
|                     // Boutons | ||||
|                     Row( | ||||
|                       mainAxisAlignment: MainAxisAlignment.end, | ||||
|                       children: [ | ||||
|                         TextButton( | ||||
|                           onPressed: () => Navigator.pop(context), | ||||
|                           child: const Text('Annuler'), | ||||
|                         ), | ||||
|                         const SizedBox(width: 16), | ||||
|                         ElevatedButton( | ||||
|                           onPressed: _saveEquipment, | ||||
|                           style: ElevatedButton.styleFrom( | ||||
|                             backgroundColor: AppColors.rouge, | ||||
|                             padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), | ||||
|                           ), | ||||
|                           child: Text(isEditing ? 'Enregistrer' : 'Créer', style: const TextStyle(color: Colors.white)), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildParentBoxesSelector() { | ||||
|     if (_availableBoxes.isEmpty) { | ||||
|       return const Card( | ||||
|         child: Padding( | ||||
|           padding: EdgeInsets.all(16.0), | ||||
|           child: Text('Aucune boîte disponible'), | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Card( | ||||
|       child: Column( | ||||
|         children: _availableBoxes.map((box) { | ||||
|           final isSelected = _selectedParentBoxIds.contains(box.id); | ||||
|           return CheckboxListTile( | ||||
|             title: Text(box.name), | ||||
|             subtitle: box.model != null ? Text('Modèle: {box.model}') : null, | ||||
|             value: isSelected, | ||||
|             onChanged: (bool? value) { | ||||
|               setState(() { | ||||
|                 if (value == true) { | ||||
|                   _selectedParentBoxIds.add(box.id); | ||||
|                 } else { | ||||
|                   _selectedParentBoxIds.remove(box.id); | ||||
|                 } | ||||
|               }); | ||||
|             }, | ||||
|           ); | ||||
|         }).toList(), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildDateField({required String label, required IconData icon, required DateTime? value, required VoidCallback onTap}) { | ||||
|     return InkWell( | ||||
|       onTap: onTap, | ||||
|       child: InputDecorator( | ||||
|         decoration: InputDecoration( | ||||
|           labelText: label, | ||||
|           border: const OutlineInputBorder(), | ||||
|           prefixIcon: Icon(icon), | ||||
|           suffixIcon: value != null | ||||
|               ? IconButton( | ||||
|                   icon: const Icon(Icons.clear), | ||||
|                   onPressed: () { | ||||
|                     setState(() { | ||||
|                       if (label.contains('achat')) { | ||||
|                         _purchaseDate = null; | ||||
|                       } else if (label.contains('Dernière')) { | ||||
|                         _lastMaintenanceDate = null; | ||||
|                       } else if (label.contains('Prochaine')) { | ||||
|                         _nextMaintenanceDate = null; | ||||
|                       } | ||||
|                     }); | ||||
|                   }, | ||||
|                 ) | ||||
|               : null, | ||||
|         ), | ||||
|         child: Text( | ||||
|           value != null ? DateFormat('dd/MM/yyyy').format(value) : 'Sélectionner une date', | ||||
|           style: TextStyle(color: value != null ? Colors.black : Colors.grey), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<void> _selectDate(BuildContext context, String field) async { | ||||
|     final DateTime? picked = await showDatePicker( | ||||
|       context: context, | ||||
|       initialDate: DateTime.now(), | ||||
|       firstDate: DateTime(2000), | ||||
|       lastDate: DateTime(2100), | ||||
|     ); | ||||
|  | ||||
|     if (picked != null) { | ||||
|       setState(() { | ||||
|         switch (field) { | ||||
|           case 'purchase': | ||||
|             _purchaseDate = picked; | ||||
|             break; | ||||
|           case 'lastMaintenance': | ||||
|             _lastMaintenanceDate = picked; | ||||
|             break; | ||||
|           case 'nextMaintenance': | ||||
|             _nextMaintenanceDate = picked; | ||||
|             break; | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _saveEquipment() async { | ||||
|     if (!_formKey.currentState!.validate()) return; | ||||
|  | ||||
|     setState(() => _isLoading = true); | ||||
|  | ||||
|     try { | ||||
|       final equipmentProvider = Provider.of<EquipmentProvider>(context, listen: false); | ||||
|       final isEditing = widget.equipment != null; | ||||
|  | ||||
|       int? availableQuantity; | ||||
|       if (_isConsumable && _totalQuantityController.text.isNotEmpty) { | ||||
|         final totalQuantity = int.parse(_totalQuantityController.text); | ||||
|         if (isEditing && widget.equipment!.availableQuantity != null) { | ||||
|           availableQuantity = widget.equipment!.availableQuantity; | ||||
|         } else { | ||||
|           availableQuantity = totalQuantity; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Validation marque/modèle obligatoires | ||||
|       String brand = _brandController.text.trim(); | ||||
|       String model = _modelController.text.trim(); | ||||
|  | ||||
|       if (brand.isEmpty || model.isEmpty) { | ||||
|         ScaffoldMessenger.of(context).showSnackBar( | ||||
|           const SnackBar(content: Text('La marque et le modèle sont obligatoires')), | ||||
|         ); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // Génération d'identifiant si vide | ||||
|       List<String> ids = []; | ||||
|       List<int> numbers = []; | ||||
|  | ||||
|       if (!isEditing && _identifierController.text.isEmpty) { | ||||
|         // Gérer la range ou nombre simple | ||||
|         final quantityText = _quantityToAddController.text.trim(); | ||||
|         if (_addMultiple && quantityText.contains('-')) { | ||||
|           // Range: ex "6-18" | ||||
|           final parts = quantityText.split('-'); | ||||
|           final start = int.parse(parts[0].trim()); | ||||
|           final end = int.parse(parts[1].trim()); | ||||
|           for (int i = start; i <= end; i++) { | ||||
|             numbers.add(i); | ||||
|           } | ||||
|         } else if (_addMultiple) { | ||||
|           // Nombre simple | ||||
|           final nbToAdd = int.tryParse(quantityText) ?? 1; | ||||
|           for (int i = 1; i <= nbToAdd; i++) { | ||||
|             numbers.add(i); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         // Générer les IDs | ||||
|         if (numbers.isEmpty) { | ||||
|           String baseId = EquipmentIdGenerator.generate(brand: brand, model: model, number: null); | ||||
|           String uniqueId = await EquipmentIdGenerator.ensureUniqueId(baseId, _equipmentService); | ||||
|           ids.add(uniqueId); | ||||
|         } else { | ||||
|           for (final num in numbers) { | ||||
|             String baseId = EquipmentIdGenerator.generate(brand: brand, model: model, number: num); | ||||
|             String uniqueId = await EquipmentIdGenerator.ensureUniqueId(baseId, _equipmentService); | ||||
|             ids.add(uniqueId); | ||||
|           } | ||||
|         } | ||||
|       } else { | ||||
|         ids.add(_identifierController.text.trim()); | ||||
|       } | ||||
|  | ||||
|       // Création des équipements | ||||
|       for (final id in ids) { | ||||
|         final now = DateTime.now(); | ||||
|         final equipment = EquipmentModel( | ||||
|           id: id, | ||||
|           name: id, // Utilisation de l'identifiant comme nom | ||||
|           brand: brand, | ||||
|           model: model, | ||||
|           category: _selectedCategory, | ||||
|           status: _selectedStatus, | ||||
|           purchasePrice: _purchasePriceController.text.isNotEmpty ? double.tryParse(_purchasePriceController.text) : null, | ||||
|           rentalPrice: _rentalPriceController.text.isNotEmpty ? double.tryParse(_rentalPriceController.text) : null, | ||||
|           totalQuantity: _isConsumable ? int.tryParse(_totalQuantityController.text) : null, | ||||
|           criticalThreshold: _isConsumable ? int.tryParse(_criticalThresholdController.text) : null, | ||||
|           purchaseDate: _purchaseDate, | ||||
|           lastMaintenanceDate: _lastMaintenanceDate, | ||||
|           nextMaintenanceDate: _nextMaintenanceDate, | ||||
|           parentBoxIds: _selectedParentBoxIds, | ||||
|           notes: _notesController.text, | ||||
|           createdAt: isEditing ? (widget.equipment?.createdAt ?? now) : now, | ||||
|           updatedAt: now, | ||||
|           availableQuantity: availableQuantity, | ||||
|         ); | ||||
|         if (isEditing) { | ||||
|           await equipmentProvider.updateEquipment( | ||||
|             equipment.id, | ||||
|             equipment.toMap(), | ||||
|           ); | ||||
|         } else { | ||||
|           await equipmentProvider.addEquipment(equipment); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (mounted) { | ||||
|         Navigator.pop(context, true); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       if (mounted) { | ||||
|         ScaffoldMessenger.of(context).showSnackBar( | ||||
|           SnackBar(content: Text('Erreur lors de l\'enregistrement : $e')), | ||||
|         ); | ||||
|       } | ||||
|     } finally { | ||||
|       if (mounted) setState(() => _isLoading = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Correction des enums dans _getCategoryLabel | ||||
|   String _getCategoryLabel(EquipmentCategory category) { | ||||
|     switch (category) { | ||||
|       case EquipmentCategory.lighting: | ||||
|         return 'Lumière'; | ||||
|       case EquipmentCategory.sound: | ||||
|         return 'Son'; | ||||
|       case EquipmentCategory.video: | ||||
|         return 'Vidéo'; | ||||
|       case EquipmentCategory.effect: | ||||
|         return 'Effet'; | ||||
|       case EquipmentCategory.structure: | ||||
|         return 'Structure'; | ||||
|       case EquipmentCategory.consumable: | ||||
|         return 'Consommable'; | ||||
|       case EquipmentCategory.cable: | ||||
|         return 'Câble'; | ||||
|       case EquipmentCategory.other: | ||||
|       default: | ||||
|         return 'Autre'; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Correction des enums dans _getStatusLabel | ||||
|   String _getStatusLabel(EquipmentStatus status) { | ||||
|     switch (status) { | ||||
|       case EquipmentStatus.available: | ||||
|         return 'Disponible'; | ||||
|       case EquipmentStatus.inUse: | ||||
|         return 'En prestation'; | ||||
|       case EquipmentStatus.rented: | ||||
|         return 'Loué'; | ||||
|       case EquipmentStatus.lost: | ||||
|         return 'Perdu'; | ||||
|       case EquipmentStatus.outOfService: | ||||
|         return 'HS'; | ||||
|       case EquipmentStatus.maintenance: | ||||
|         return 'Maintenance'; | ||||
|       default: | ||||
|         return 'Autre'; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										1474
									
								
								em2rp/lib/views/equipment_management_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1474
									
								
								em2rp/lib/views/equipment_management_page.dart
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -4,6 +4,7 @@ import 'package:em2rp/views/calendar_page.dart'; | ||||
| import 'package:em2rp/views/my_account_page.dart'; | ||||
| import 'package:em2rp/views/user_management_page.dart'; | ||||
| import 'package:em2rp/views/data_management_page.dart'; | ||||
| import 'package:em2rp/views/equipment_management_page.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:em2rp/views/widgets/image/profile_picture.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| @@ -151,6 +152,24 @@ class MainDrawer extends StatelessWidget { | ||||
|                         }, | ||||
|                       ), | ||||
|                     ), | ||||
|                     PermissionGate( | ||||
|                       requiredPermissions: const ['view_equipment'], | ||||
|                       child: ListTile( | ||||
|                         leading: const Icon(Icons.inventory), | ||||
|                         title: const Text('Gestion du Matériel'), | ||||
|                         selected: currentPage == '/equipment_management', | ||||
|                         selectedColor: AppColors.rouge, | ||||
|                         onTap: () { | ||||
|                           Navigator.pop(context); | ||||
|                           Navigator.pushReplacement( | ||||
|                             context, | ||||
|                             MaterialPageRoute( | ||||
|                                 builder: (context) => | ||||
|                                     const EquipmentManagementPage()), | ||||
|                           ); | ||||
|                         }, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 ElPoyo
					ElPoyo