diff --git a/em2rp/lib/providers/equipment_provider.dart b/em2rp/lib/providers/equipment_provider.dart index 3a8009b..7b25f19 100644 --- a/em2rp/lib/providers/equipment_provider.dart +++ b/em2rp/lib/providers/equipment_provider.dart @@ -1,9 +1,11 @@ import 'package:flutter/foundation.dart'; import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/services/equipment_service.dart'; +import 'package:em2rp/services/equipment_status_calculator.dart'; class EquipmentProvider extends ChangeNotifier { final EquipmentService _service = EquipmentService(); + final EquipmentStatusCalculator _statusCalculator = EquipmentStatusCalculator(); List _equipment = []; List _models = []; @@ -179,6 +181,16 @@ class EquipmentProvider extends ChangeNotifier { } } + /// Calculer le statut réel d'un équipement (asynchrone) + Future calculateRealStatus(EquipmentModel equipment) async { + return await _statusCalculator.calculateRealStatus(equipment); + } + + /// Invalider le cache du calculateur de statut + void invalidateStatusCache() { + _statusCalculator.invalidateCache(); + } + // === FILTRES === /// Définir la catégorie sélectionnée diff --git a/em2rp/lib/providers/event_provider.dart b/em2rp/lib/providers/event_provider.dart index 23190d8..714110c 100644 --- a/em2rp/lib/providers/event_provider.dart +++ b/em2rp/lib/providers/event_provider.dart @@ -1,5 +1,6 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; +import 'package:em2rp/services/equipment_status_calculator.dart'; import '../models/event_model.dart'; class EventProvider with ChangeNotifier { @@ -120,6 +121,10 @@ class EventProvider with ChangeNotifier { try { await _firestore.collection('events').doc(eventId).delete(); _events.removeWhere((event) => event.id == eventId); + + // Invalider le cache des statuts d'équipement + EquipmentStatusCalculator.invalidateGlobalCache(); + notifyListeners(); } catch (e) { print('Error deleting event: $e'); diff --git a/em2rp/lib/services/equipment_status_calculator.dart b/em2rp/lib/services/equipment_status_calculator.dart new file mode 100644 index 0000000..ce823dc --- /dev/null +++ b/em2rp/lib/services/equipment_status_calculator.dart @@ -0,0 +1,235 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:em2rp/models/equipment_model.dart'; +import 'package:em2rp/models/event_model.dart'; + +/// Service pour calculer dynamiquement le statut réel d'un équipement +/// basé sur les événements en cours +class EquipmentStatusCalculator { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + /// Cache des événements pour éviter de multiples requêtes + List? _cachedEvents; + DateTime? _cacheTime; + static const _cacheDuration = Duration(minutes: 1); + + /// Instance statique pour permettre l'invalidation depuis n'importe où + static final EquipmentStatusCalculator _instance = EquipmentStatusCalculator._internal(); + + factory EquipmentStatusCalculator() { + return _instance; + } + + EquipmentStatusCalculator._internal(); + + /// Calcule le statut réel d'un équipement basé sur les événements + Future calculateRealStatus(EquipmentModel equipment) async { + print('[StatusCalculator] Calculating status for: ${equipment.id}'); + + // Si l'équipement est marqué comme perdu ou HS, on garde ce statut + // car c'est une information métier importante + if (equipment.status == EquipmentStatus.lost || + equipment.status == EquipmentStatus.outOfService) { + print('[StatusCalculator] ${equipment.id} is lost/outOfService -> keeping status'); + return equipment.status; + } + + // Charger les événements (avec cache) + await _loadEventsIfNeeded(); + print('[StatusCalculator] Events loaded: ${_cachedEvents?.length ?? 0}'); + + // Vérifier si l'équipement est utilisé dans un événement en cours + final isInUse = await _isEquipmentInUse(equipment.id); + print('[StatusCalculator] ${equipment.id} isInUse: $isInUse'); + + if (isInUse) { + return EquipmentStatus.inUse; + } + + // Vérifier si l'équipement est en maintenance + if (equipment.status == EquipmentStatus.maintenance) { + // On pourrait vérifier si la maintenance est toujours valide + // Pour l'instant on garde le statut + return EquipmentStatus.maintenance; + } + + // Vérifier si l'équipement est loué + if (equipment.status == EquipmentStatus.rented) { + // On pourrait vérifier une date de retour prévue + // Pour l'instant on garde le statut + return EquipmentStatus.rented; + } + + // Par défaut, l'équipement est disponible + print('[StatusCalculator] ${equipment.id} -> AVAILABLE'); + return EquipmentStatus.available; + } + + /// Calcule les statuts pour une liste d'équipements (optimisé) + Future> calculateMultipleStatuses( + List equipments, + ) async { + await _loadEventsIfNeeded(); + + final statuses = {}; + + // Trouver tous les équipements en cours d'utilisation + final equipmentIdsInUse = {}; + final containerIdsInUse = {}; + + for (var event in _cachedEvents ?? []) { + // Un équipement est "en prestation" dès que la préparation est complétée + // et jusqu'à ce que le retour soit complété + final isPrepared = event.preparationStatus == PreparationStatus.completed || + event.preparationStatus == PreparationStatus.completedWithMissing; + + final isReturned = event.returnStatus == ReturnStatus.completed || + event.returnStatus == ReturnStatus.completedWithMissing; + + final isInProgress = isPrepared && !isReturned; + + if (isInProgress) { + // Ajouter les équipements directs + for (var eq in event.assignedEquipment) { + equipmentIdsInUse.add(eq.equipmentId); + } + // Ajouter les conteneurs + containerIdsInUse.addAll(event.assignedContainers); + } + } + + // Récupérer les équipements dans les conteneurs en cours d'utilisation + if (containerIdsInUse.isNotEmpty) { + final containersSnapshot = await _firestore + .collection('containers') + .where(FieldPath.documentId, whereIn: containerIdsInUse.toList()) + .get(); + + for (var doc in containersSnapshot.docs) { + final data = doc.data(); + final equipmentIds = List.from(data['equipmentIds'] ?? []); + equipmentIdsInUse.addAll(equipmentIds); + } + } + + // Calculer le statut pour chaque équipement + for (var equipment in equipments) { + // Si perdu ou HS, on garde le statut + if (equipment.status == EquipmentStatus.lost || + equipment.status == EquipmentStatus.outOfService) { + statuses[equipment.id] = equipment.status; + continue; + } + + // Si en cours d'utilisation + if (equipmentIdsInUse.contains(equipment.id)) { + statuses[equipment.id] = EquipmentStatus.inUse; + continue; + } + + // Si en maintenance ou loué, on garde le statut + if (equipment.status == EquipmentStatus.maintenance || + equipment.status == EquipmentStatus.rented) { + statuses[equipment.id] = equipment.status; + continue; + } + + // Par défaut, disponible + statuses[equipment.id] = EquipmentStatus.available; + } + + return statuses; + } + + /// Vérifie si un équipement est actuellement en cours d'utilisation + Future _isEquipmentInUse(String equipmentId) async { + print('[StatusCalculator] Checking if $equipmentId is in use...'); + + // Vérifier dans les événements directs + for (var event in _cachedEvents ?? []) { + // Un équipement est "en prestation" dès que la préparation est complétée + // et jusqu'à ce que le retour soit complété + final isPrepared = event.preparationStatus == PreparationStatus.completed || + event.preparationStatus == PreparationStatus.completedWithMissing; + + final isReturned = event.returnStatus == ReturnStatus.completed || + event.returnStatus == ReturnStatus.completedWithMissing; + + final isInProgress = isPrepared && !isReturned; + + if (!isInProgress) continue; + + print('[StatusCalculator] Event ${event.name} is IN PROGRESS (prepared and not returned)'); + + // Vérifier si l'équipement est directement assigné + if (event.assignedEquipment.any((eq) => eq.equipmentId == equipmentId)) { + print('[StatusCalculator] $equipmentId found DIRECTLY in event ${event.name}'); + return true; + } + + // Vérifier si l'équipement est dans un conteneur assigné + if (event.assignedContainers.isNotEmpty) { + print('[StatusCalculator] Checking containers for event ${event.name}: ${event.assignedContainers}'); + final containersSnapshot = await _firestore + .collection('containers') + .where(FieldPath.documentId, whereIn: event.assignedContainers) + .get(); + + for (var doc in containersSnapshot.docs) { + final data = doc.data(); + final equipmentIds = List.from(data['equipmentIds'] ?? []); + print('[StatusCalculator] Container ${doc.id} contains: $equipmentIds'); + if (equipmentIds.contains(equipmentId)) { + print('[StatusCalculator] $equipmentId found in CONTAINER ${doc.id}'); + return true; + } + } + } + } + + print('[StatusCalculator] $equipmentId is NOT in use'); + return false; + } + + /// Charge les événements si le cache est expiré + Future _loadEventsIfNeeded() async { + if (_cachedEvents != null && + _cacheTime != null && + DateTime.now().difference(_cacheTime!) < _cacheDuration) { + return; // Cache encore valide + } + + try { + final eventsSnapshot = await _firestore.collection('events').get(); + + _cachedEvents = eventsSnapshot.docs + .map((doc) { + try { + return EventModel.fromMap(doc.data(), doc.id); + } catch (e) { + print('[EquipmentStatusCalculator] Error parsing event ${doc.id}: $e'); + return null; + } + }) + .whereType() + .where((event) => event.status != EventStatus.canceled) // Ignorer les événements annulés + .toList(); + + _cacheTime = DateTime.now(); + } catch (e) { + print('[EquipmentStatusCalculator] Error loading events: $e'); + _cachedEvents = []; + } + } + + /// Invalide le cache (à appeler après une modification d'événement) + void invalidateCache() { + _cachedEvents = null; + _cacheTime = null; + } + + /// Invalide le cache de l'instance globale (méthode statique) + static void invalidateGlobalCache() { + _instance.invalidateCache(); + } +} + diff --git a/em2rp/lib/services/event_preparation_service.dart b/em2rp/lib/services/event_preparation_service.dart index 9bb141e..83da91c 100644 --- a/em2rp/lib/services/event_preparation_service.dart +++ b/em2rp/lib/services/event_preparation_service.dart @@ -2,6 +2,7 @@ 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'; +import 'package:em2rp/services/equipment_status_calculator.dart'; class EventPreparationService { final FirebaseFirestore _firestore = FirebaseFirestore.instance; @@ -68,6 +69,9 @@ class EventPreparationService { 'preparationStatus': preparationStatusToString(PreparationStatus.completed), }); + // Invalider le cache des statuts d'équipement + EquipmentStatusCalculator.invalidateGlobalCache(); + // Mettre à jour le statut des équipements à "inUse" (seulement pour les équipements qui existent) for (var equipment in event.assignedEquipment) { // Vérifier si l'équipement existe avant de mettre à jour son statut @@ -231,6 +235,9 @@ class EventPreparationService { } } } + + // Invalider le cache des statuts d'équipement + EquipmentStatusCalculator.invalidateGlobalCache(); } catch (e) { print('Error validating all return: $e'); rethrow; diff --git a/em2rp/lib/services/event_preparation_service_extended.dart b/em2rp/lib/services/event_preparation_service_extended.dart index 5913c0e..91954e8 100644 --- a/em2rp/lib/services/event_preparation_service_extended.dart +++ b/em2rp/lib/services/event_preparation_service_extended.dart @@ -1,6 +1,7 @@ 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_status_calculator.dart'; /// Service étendu pour gérer les 4 étapes : Préparation, Chargement, Déchargement, Retour class EventPreparationServiceExtended { @@ -59,6 +60,9 @@ class EventPreparationServiceExtended { 'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(), 'loadingStatus': loadingStatusToString(LoadingStatus.completed), }); + + // Invalider le cache des statuts d'équipement + EquipmentStatusCalculator.invalidateGlobalCache(); } catch (e) { print('Error validating all loading: $e'); rethrow; @@ -115,6 +119,9 @@ class EventPreparationServiceExtended { 'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(), 'unloadingStatus': unloadingStatusToString(UnloadingStatus.completed), }); + + // Invalider le cache des statuts d'équipement + EquipmentStatusCalculator.invalidateGlobalCache(); } catch (e) { print('Error validating all unloading: $e'); rethrow; diff --git a/em2rp/lib/views/equipment_detail_page.dart b/em2rp/lib/views/equipment_detail_page.dart index c63eeeb..e7ec21a 100644 --- a/em2rp/lib/views/equipment_detail_page.dart +++ b/em2rp/lib/views/equipment_detail_page.dart @@ -16,6 +16,7 @@ import 'package:em2rp/views/widgets/equipment/equipment_header_section.dart'; import 'package:em2rp/views/widgets/equipment/equipment_main_info_section.dart'; import 'package:em2rp/views/widgets/equipment/equipment_notes_section.dart'; import 'package:em2rp/views/widgets/equipment/equipment_associated_events_section.dart'; +import 'package:em2rp/views/widgets/equipment/equipment_current_events_section.dart'; import 'package:em2rp/views/widgets/equipment/equipment_price_section.dart'; import 'package:em2rp/views/widgets/equipment/equipment_maintenance_history_section.dart'; import 'package:em2rp/views/widgets/equipment/equipment_dates_section.dart'; @@ -107,11 +108,15 @@ class _EquipmentDetailPageState extends State { const SizedBox(height: 24), ], - // 4. Événements associés + // 4. Événements en cours + EquipmentCurrentEventsSection(equipment: widget.equipment), + const SizedBox(height: 24), + + // 5. Événements passés / à venir EquipmentAssociatedEventsSection(equipment: widget.equipment), const SizedBox(height: 24), - // 5-7. Prix, Historique des maintenances, Dates en layout responsive + // 6-8. Prix, Historique des maintenances, Dates en layout responsive if (isDesktop) _buildDesktopTwoColumnLayout(hasManagePermission) else diff --git a/em2rp/lib/views/equipment_management_page.dart b/em2rp/lib/views/equipment_management_page.dart index 3bfdb80..cdf8e4d 100644 --- a/em2rp/lib/views/equipment_management_page.dart +++ b/em2rp/lib/views/equipment_management_page.dart @@ -10,6 +10,7 @@ import 'package:em2rp/views/equipment_form_page.dart'; import 'package:em2rp/views/equipment_detail_page.dart'; import 'package:em2rp/views/widgets/common/qr_code_dialog.dart'; import 'package:em2rp/views/widgets/common/qr_code_format_selector_dialog.dart'; +import 'package:em2rp/views/widgets/equipment/equipment_status_badge.dart'; import 'package:em2rp/mixins/selection_mode_mixin.dart'; import 'package:em2rp/views/widgets/management/management_list.dart'; @@ -428,9 +429,6 @@ class _EquipmentManagementPageState extends State _cachedEquipment = items; }, itemBuilder: (equipment) { - // Trier les équipements par nom - final sortedEquipment = List.from(_cachedEquipment ?? [equipment]); - sortedEquipment.sort((a, b) => a.name.compareTo(b.name)); return _buildEquipmentCard(equipment); }, ); @@ -444,7 +442,7 @@ class _EquipmentManagementPageState extends State return Card( margin: const EdgeInsets.only(bottom: 12), color: isSelectionMode && isSelected - ? AppColors.rouge.withOpacity(0.1) + ? AppColors.rouge.withValues(alpha: 0.1) : null, child: ListTile( leading: isSelectionMode @@ -454,11 +452,10 @@ class _EquipmentManagementPageState extends State activeColor: AppColors.rouge, ) : CircleAvatar( - backgroundColor: - equipment.status.color.withOpacity(0.2), + backgroundColor: equipment.category.color.withValues(alpha: 0.2), child: equipment.category.getIcon( size: 20, - color: Colors.black, + color: equipment.category.color, ), ), title: Row( @@ -469,10 +466,10 @@ class _EquipmentManagementPageState extends State style: const TextStyle(fontWeight: FontWeight.bold), ), ), - // Afficher le statut uniquement si ce n'est pas un consommable ou câble + // Afficher le badge de statut calculé dynamiquement if (equipment.category != EquipmentCategory.consumable && equipment.category != EquipmentCategory.cable) - _buildStatusBadge(equipment.status), + EquipmentStatusBadge(equipment: equipment), ], ), subtitle: Column( @@ -607,24 +604,6 @@ class _EquipmentManagementPageState extends State ); } - Widget _buildStatusBadge(EquipmentStatus status) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: status.color.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: status.color), - ), - child: Text( - status.label, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: status.color, - ), - ), - ); - } // Actions void _createNewEquipment() { diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart index 65ee2ec..5a7b548 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart @@ -87,40 +87,45 @@ class _EventPreparationButtonsState extends State { children: [ // Bouton de l'étape actuelle if (!isCompleted) - ElevatedButton.icon( - onPressed: () async { - final result = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EventPreparationPage( - initialEvent: event, + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: ElevatedButton.icon( + onPressed: () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EventPreparationPage( + initialEvent: event, + ), + ), + ); + + // Si la validation a réussi, le StreamBuilder se rechargera automatiquement + if (result == true && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Étape validée avec succès'), + backgroundColor: Colors.green, + ), + ); + } + }, + icon: Icon(buttonIcon), + label: Text( + buttonText, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, ), ), - ); - - // Si la validation a réussi, le StreamBuilder se rechargera automatiquement - if (result == true && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Étape validée avec succès'), - backgroundColor: Colors.green, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.bleuFonce, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), - ); - } - }, - icon: Icon(buttonIcon), - label: Text( - buttonText, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.bleuFonce, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + ), ), ), ), diff --git a/em2rp/lib/views/widgets/equipment/equipment_associated_events_section.dart b/em2rp/lib/views/widgets/equipment/equipment_associated_events_section.dart index b202a5a..3c9dbd5 100644 --- a/em2rp/lib/views/widgets/equipment/equipment_associated_events_section.dart +++ b/em2rp/lib/views/widgets/equipment/equipment_associated_events_section.dart @@ -6,9 +6,8 @@ import 'package:em2rp/utils/colors.dart'; import 'package:intl/intl.dart'; enum EventFilter { - current, // Événements en cours (préparés mais pas encore retournés) upcoming, // Événements à venir - past, // Événements passés + past, // Événements passés } /// Widget pour afficher les événements associés à un équipement @@ -27,7 +26,7 @@ class EquipmentAssociatedEventsSection extends StatefulWidget { class _EquipmentAssociatedEventsSectionState extends State { - EventFilter _selectedFilter = EventFilter.current; + EventFilter _selectedFilter = EventFilter.upcoming; List _events = []; bool _isLoading = true; @@ -41,38 +40,80 @@ class _EquipmentAssociatedEventsSectionState setState(() => _isLoading = true); try { + // Récupérer TOUS les événements car on ne peut pas faire arrayContains sur un objet final eventsSnapshot = await FirebaseFirestore.instance .collection('events') - .where('assignedEquipment', - arrayContains: {'equipmentId': widget.equipment.id}) .get(); - final events = eventsSnapshot.docs - .map((doc) => EventModel.fromMap(doc.data(), doc.id)) - .toList(); + final events = []; + + // Récupérer toutes les boîtes pour vérifier leur contenu + final containersSnapshot = await FirebaseFirestore.instance + .collection('containers') + .get(); + + final containersWithEquipment = []; + for (var containerDoc in containersSnapshot.docs) { + try { + final data = containerDoc.data(); + final equipmentIds = List.from(data['equipmentIds'] ?? []); + + if (equipmentIds.contains(widget.equipment.id)) { + containersWithEquipment.add(containerDoc.id); + } + } catch (e) { + print('[EquipmentAssociatedEventsSection] Error parsing container ${containerDoc.id}: $e'); + } + } + + // Filtrer manuellement les événements qui contiennent cet équipement + for (var doc in eventsSnapshot.docs) { + try { + final event = EventModel.fromMap(doc.data(), doc.id); + + // Vérifier si l'équipement est directement assigné + final hasEquipmentDirectly = event.assignedEquipment.any( + (eq) => eq.equipmentId == widget.equipment.id, + ); + + // Vérifier si l'équipement est dans une boîte assignée à l'événement + final hasEquipmentInAssignedContainer = event.assignedContainers.any( + (containerId) => containersWithEquipment.contains(containerId), + ); + + if (hasEquipmentDirectly || hasEquipmentInAssignedContainer) { + events.add(event); + } + } catch (e) { + print('[EquipmentAssociatedEventsSection] Error parsing event ${doc.id}: $e'); + } + } // Filtrer selon le statut final now = DateTime.now(); final filteredEvents = events.where((event) { - switch (_selectedFilter) { - case EventFilter.current: - // Événement en cours = préparation complétée ET retour pas encore complété - return (event.preparationStatus == PreparationStatus.completed || - event.preparationStatus == - PreparationStatus.completedWithMissing) && - (event.returnStatus == null || - event.returnStatus == ReturnStatus.notStarted || - event.returnStatus == ReturnStatus.inProgress); + // Un événement est EN COURS dès que la préparation est complétée + // et jusqu'à ce que le retour soit complété + final isPrepared = event.preparationStatus == PreparationStatus.completed || + event.preparationStatus == PreparationStatus.completedWithMissing; + final isReturned = event.returnStatus == ReturnStatus.completed || + event.returnStatus == ReturnStatus.completedWithMissing; + + final isInProgress = isPrepared && !isReturned; + + if (isInProgress) { + return false; // Les événements en cours sont affichés dans une autre section + } + + switch (_selectedFilter) { case EventFilter.upcoming: - // Événements à venir = date de début dans le futur OU préparation pas encore faite - return event.startDateTime.isAfter(now) || - event.preparationStatus == PreparationStatus.notStarted; + // Événements à venir = date de début dans le futur + return event.startDateTime.isAfter(now); case EventFilter.past: - // Événements passés = retour complété - return event.returnStatus == ReturnStatus.completed || - event.returnStatus == ReturnStatus.completedWithMissing; + // Événements passés = date de fin passée + return event.endDateTime.isBefore(now); } }).toList(); @@ -98,8 +139,6 @@ class _EquipmentAssociatedEventsSectionState String _getFilterLabel(EventFilter filter) { switch (filter) { - case EventFilter.current: - return 'En cours'; case EventFilter.upcoming: return 'À venir'; case EventFilter.past: @@ -109,8 +148,6 @@ class _EquipmentAssociatedEventsSectionState IconData _getFilterIcon(EventFilter filter) { switch (filter) { - case EventFilter.current: - return Icons.play_circle; case EventFilter.upcoming: return Icons.upcoming; case EventFilter.past: @@ -134,7 +171,7 @@ class _EquipmentAssociatedEventsSectionState const SizedBox(width: 8), Expanded( child: Text( - 'Événements associés', + 'Événements passés / à venir', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), @@ -225,11 +262,28 @@ class _EquipmentAssociatedEventsSectionState Widget _buildEventCard(EventModel event) { final dateFormat = DateFormat('dd/MM/yyyy HH:mm'); - final isInProgress = (event.preparationStatus == PreparationStatus.completed || - event.preparationStatus == PreparationStatus.completedWithMissing) && - (event.returnStatus == null || - event.returnStatus == ReturnStatus.notStarted || - event.returnStatus == ReturnStatus.inProgress); + + // Un événement est en cours dès que la préparation est complétée + // et jusqu'à ce que le retour soit complété + final isPrepared = event.preparationStatus == PreparationStatus.completed || + event.preparationStatus == PreparationStatus.completedWithMissing; + + final isReturned = event.returnStatus == ReturnStatus.completed || + event.returnStatus == ReturnStatus.completedWithMissing; + + final isInProgress = isPrepared && !isReturned; + + // Trouver la quantité utilisée pour les consommables/câbles + int? usedQuantity; + if (widget.equipment.hasQuantity) { + final assignedEquipment = event.assignedEquipment.firstWhere( + (eq) => eq.equipmentId == widget.equipment.id, + orElse: () => EventEquipment(equipmentId: ''), + ); + if (assignedEquipment.equipmentId.isNotEmpty) { + usedQuantity = assignedEquipment.quantity; + } + } return Card( margin: const EdgeInsets.only(bottom: 12), @@ -290,16 +344,46 @@ class _EquipmentAssociatedEventsSectionState children: [ const Icon(Icons.calendar_today, size: 14, color: Colors.grey), const SizedBox(width: 4), - Text( - '${dateFormat.format(event.startDateTime)} → ${dateFormat.format(event.endDateTime)}', - style: TextStyle( - fontSize: 13, - color: Colors.grey.shade700, + Expanded( + child: Text( + '${dateFormat.format(event.startDateTime)} → ${dateFormat.format(event.endDateTime)}', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + ), ), ), ], ), + // Quantité utilisée pour consommables/câbles + if (usedQuantity != null) ...[ + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.blue.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.inventory, size: 14, color: Colors.blue), + const SizedBox(width: 6), + Text( + 'Quantité utilisée: $usedQuantity', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + ], + ), + ), + ], + const SizedBox(height: 8), // Statuts de préparation et retour @@ -316,31 +400,6 @@ class _EquipmentAssociatedEventsSectionState ), ], ), - - // Boutons d'action - if (isInProgress && _selectedFilter == EventFilter.current) ...[ - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: () { - Navigator.pushNamed( - context, - '/event_preparation', - arguments: event.id, - ); - }, - icon: const Icon(Icons.logout, size: 16), - label: const Text('Check-out'), - style: OutlinedButton.styleFrom( - foregroundColor: AppColors.rouge, - ), - ), - ), - ], - ), - ], ], ), ), diff --git a/em2rp/lib/views/widgets/equipment/equipment_current_events_section.dart b/em2rp/lib/views/widgets/equipment/equipment_current_events_section.dart new file mode 100644 index 0000000..71db9b9 --- /dev/null +++ b/em2rp/lib/views/widgets/equipment/equipment_current_events_section.dart @@ -0,0 +1,284 @@ +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:em2rp/models/event_model.dart'; +import 'package:em2rp/models/equipment_model.dart'; +import 'package:em2rp/utils/colors.dart'; +import 'package:intl/intl.dart'; + +/// Widget pour afficher les événements EN COURS utilisant cet équipement +class EquipmentCurrentEventsSection extends StatefulWidget { + final EquipmentModel equipment; + + const EquipmentCurrentEventsSection({ + super.key, + required this.equipment, + }); + + @override + State createState() => + _EquipmentCurrentEventsSectionState(); +} + +class _EquipmentCurrentEventsSectionState + extends State { + List _events = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadCurrentEvents(); + } + + Future _loadCurrentEvents() async { + setState(() => _isLoading = true); + + try { + // Récupérer TOUS les événements + final eventsSnapshot = await FirebaseFirestore.instance + .collection('events') + .get(); + + final events = []; + + // Récupérer toutes les boîtes pour vérifier leur contenu + final containersSnapshot = await FirebaseFirestore.instance + .collection('containers') + .get(); + + final containersWithEquipment = []; + for (var containerDoc in containersSnapshot.docs) { + try { + final data = containerDoc.data(); + final equipmentIds = List.from(data['equipmentIds'] ?? []); + + if (equipmentIds.contains(widget.equipment.id)) { + containersWithEquipment.add(containerDoc.id); + } + } catch (e) { + print('[EquipmentCurrentEventsSection] Error parsing container ${containerDoc.id}: $e'); + } + } + + // Filtrer les événements en cours + for (var doc in eventsSnapshot.docs) { + try { + final event = EventModel.fromMap(doc.data(), doc.id); + + // Vérifier si l'équipement est directement assigné + final hasEquipmentDirectly = event.assignedEquipment.any( + (eq) => eq.equipmentId == widget.equipment.id, + ); + + // Vérifier si l'équipement est dans une boîte assignée à l'événement + final hasEquipmentInAssignedContainer = event.assignedContainers.any( + (containerId) => containersWithEquipment.contains(containerId), + ); + + if (hasEquipmentDirectly || hasEquipmentInAssignedContainer) { + // Un événement est EN COURS dès que la préparation est complétée + // et jusqu'à ce que le retour soit complété + final isPrepared = event.preparationStatus == PreparationStatus.completed || + event.preparationStatus == PreparationStatus.completedWithMissing; + + final isReturned = event.returnStatus == ReturnStatus.completed || + event.returnStatus == ReturnStatus.completedWithMissing; + + final isInProgress = isPrepared && !isReturned; + + if (isInProgress) { + events.add(event); + } + } + } catch (e) { + print('[EquipmentCurrentEventsSection] Error parsing event ${doc.id}: $e'); + } + } + + // Trier par date + events.sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); + + setState(() { + _events = events; + _isLoading = false; + }); + } catch (e) { + setState(() => _isLoading = false); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors du chargement: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + // Ne rien afficher si pas d'événements en cours + if (!_isLoading && _events.isEmpty) { + return const SizedBox.shrink(); + } + + return Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // En-tête + Row( + children: [ + const Icon(Icons.play_circle, color: AppColors.rouge), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Événements en cours utilisant ce matériel', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const Divider(height: 24), + + // Liste des événements + if (_isLoading) + const Center( + child: Padding( + padding: EdgeInsets.all(32.0), + child: CircularProgressIndicator(), + ), + ) + else + Column( + children: _events.map((event) => _buildEventCard(event)).toList(), + ), + ], + ), + ), + ); + } + + Widget _buildEventCard(EventModel event) { + final dateFormat = DateFormat('dd/MM/yyyy HH:mm'); + + // Trouver la quantité utilisée pour les consommables/câbles + int? usedQuantity; + if (widget.equipment.hasQuantity) { + final assignedEquipment = event.assignedEquipment.firstWhere( + (eq) => eq.equipmentId == widget.equipment.id, + orElse: () => EventEquipment(equipmentId: ''), + ); + if (assignedEquipment.equipmentId.isNotEmpty) { + usedQuantity = assignedEquipment.quantity; + } + } + + return Card( + margin: const EdgeInsets.only(bottom: 12), + elevation: 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: const BorderSide(color: AppColors.rouge, width: 2), + ), + child: InkWell( + onTap: () { + // Navigation vers les détails de l'événement si nécessaire + }, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Titre de l'événement + Row( + children: [ + Expanded( + child: Text( + event.name, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppColors.rouge, + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + 'EN COURS', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + + // Dates + Row( + children: [ + const Icon(Icons.calendar_today, size: 14, color: Colors.grey), + const SizedBox(width: 4), + Expanded( + child: Text( + '${dateFormat.format(event.startDateTime)} → ${dateFormat.format(event.endDateTime)}', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + ), + ), + ), + ], + ), + + // Quantité utilisée pour consommables/câbles + if (usedQuantity != null) ...[ + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.blue.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.inventory, size: 14, color: Colors.blue), + const SizedBox(width: 6), + Text( + 'Quantité utilisée: $usedQuantity', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + ], + ), + ), + ], + ], + ), + ), + ), + ); + } +} + diff --git a/em2rp/lib/views/widgets/equipment/equipment_header_section.dart b/em2rp/lib/views/widgets/equipment/equipment_header_section.dart index 2b487f5..940e98b 100644 --- a/em2rp/lib/views/widgets/equipment/equipment_header_section.dart +++ b/em2rp/lib/views/widgets/equipment/equipment_header_section.dart @@ -40,7 +40,7 @@ class EquipmentHeaderSection extends StatelessWidget { radius: 30, child: equipment.category.getIcon( size: 32, - color: AppColors.rouge, + color: equipment.category.color, ), ), const SizedBox(width: 16), diff --git a/em2rp/lib/views/widgets/equipment/equipment_status_badge.dart b/em2rp/lib/views/widgets/equipment/equipment_status_badge.dart new file mode 100644 index 0000000..85c11c4 --- /dev/null +++ b/em2rp/lib/views/widgets/equipment/equipment_status_badge.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:em2rp/models/equipment_model.dart'; +import 'package:em2rp/providers/equipment_provider.dart'; + +/// Widget qui affiche le badge de statut d'un équipement +/// Le statut est calculé de manière asynchrone basé sur les événements en cours +class EquipmentStatusBadge extends StatelessWidget { + final EquipmentModel equipment; + + const EquipmentStatusBadge({ + super.key, + required this.equipment, + }); + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context, listen: false); + print('[EquipmentStatusBadge] Building badge for ${equipment.id}'); + + return FutureBuilder( + // On calcule le statut réel de manière asynchrone + future: provider.calculateRealStatus(equipment), + // En attendant, on affiche le statut stocké + initialData: equipment.status, + builder: (context, snapshot) { + // Utiliser le statut calculé s'il est disponible, sinon le statut stocké + final status = snapshot.data ?? equipment.status; + print('[EquipmentStatusBadge] ${equipment.id} - Status: ${status.label} (hasData: ${snapshot.hasData}, connectionState: ${snapshot.connectionState})'); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: status.color.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: status.color), + ), + child: Text( + status.label, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: status.color, + ), + ), + ); + }, + ); + } +} +