feat: add current events section for equipment with dynamic status calculation

This commit is contained in:
ElPoyo
2026-01-06 12:13:09 +01:00
parent 25d395b41a
commit fb6a271f66
12 changed files with 773 additions and 124 deletions

View File

@@ -1,9 +1,11 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/services/equipment_service.dart'; import 'package:em2rp/services/equipment_service.dart';
import 'package:em2rp/services/equipment_status_calculator.dart';
class EquipmentProvider extends ChangeNotifier { class EquipmentProvider extends ChangeNotifier {
final EquipmentService _service = EquipmentService(); final EquipmentService _service = EquipmentService();
final EquipmentStatusCalculator _statusCalculator = EquipmentStatusCalculator();
List<EquipmentModel> _equipment = []; List<EquipmentModel> _equipment = [];
List<String> _models = []; List<String> _models = [];
@@ -179,6 +181,16 @@ class EquipmentProvider extends ChangeNotifier {
} }
} }
/// Calculer le statut réel d'un équipement (asynchrone)
Future<EquipmentStatus> calculateRealStatus(EquipmentModel equipment) async {
return await _statusCalculator.calculateRealStatus(equipment);
}
/// Invalider le cache du calculateur de statut
void invalidateStatusCache() {
_statusCalculator.invalidateCache();
}
// === FILTRES === // === FILTRES ===
/// Définir la catégorie sélectionnée /// Définir la catégorie sélectionnée

View File

@@ -1,5 +1,6 @@
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:em2rp/services/equipment_status_calculator.dart';
import '../models/event_model.dart'; import '../models/event_model.dart';
class EventProvider with ChangeNotifier { class EventProvider with ChangeNotifier {
@@ -120,6 +121,10 @@ class EventProvider with ChangeNotifier {
try { try {
await _firestore.collection('events').doc(eventId).delete(); await _firestore.collection('events').doc(eventId).delete();
_events.removeWhere((event) => event.id == eventId); _events.removeWhere((event) => event.id == eventId);
// Invalider le cache des statuts d'équipement
EquipmentStatusCalculator.invalidateGlobalCache();
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
print('Error deleting event: $e'); print('Error deleting event: $e');

View File

@@ -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<EventModel>? _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<EquipmentStatus> 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<Map<String, EquipmentStatus>> calculateMultipleStatuses(
List<EquipmentModel> equipments,
) async {
await _loadEventsIfNeeded();
final statuses = <String, EquipmentStatus>{};
// Trouver tous les équipements en cours d'utilisation
final equipmentIdsInUse = <String>{};
final containerIdsInUse = <String>{};
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<String>.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<bool> _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<String>.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<void> _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<EventModel>()
.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();
}
}

View File

@@ -2,6 +2,7 @@ import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/services/equipment_service.dart'; import 'package:em2rp/services/equipment_service.dart';
import 'package:em2rp/services/equipment_status_calculator.dart';
class EventPreparationService { class EventPreparationService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance;
@@ -68,6 +69,9 @@ class EventPreparationService {
'preparationStatus': preparationStatusToString(PreparationStatus.completed), '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) // Mettre à jour le statut des équipements à "inUse" (seulement pour les équipements qui existent)
for (var equipment in event.assignedEquipment) { for (var equipment in event.assignedEquipment) {
// Vérifier si l'équipement existe avant de mettre à jour son statut // 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) { } catch (e) {
print('Error validating all return: $e'); print('Error validating all return: $e');
rethrow; rethrow;

View File

@@ -1,6 +1,7 @@
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/models/equipment_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 /// Service étendu pour gérer les 4 étapes : Préparation, Chargement, Déchargement, Retour
class EventPreparationServiceExtended { class EventPreparationServiceExtended {
@@ -59,6 +60,9 @@ class EventPreparationServiceExtended {
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(), 'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
'loadingStatus': loadingStatusToString(LoadingStatus.completed), 'loadingStatus': loadingStatusToString(LoadingStatus.completed),
}); });
// Invalider le cache des statuts d'équipement
EquipmentStatusCalculator.invalidateGlobalCache();
} catch (e) { } catch (e) {
print('Error validating all loading: $e'); print('Error validating all loading: $e');
rethrow; rethrow;
@@ -115,6 +119,9 @@ class EventPreparationServiceExtended {
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(), 'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
'unloadingStatus': unloadingStatusToString(UnloadingStatus.completed), 'unloadingStatus': unloadingStatusToString(UnloadingStatus.completed),
}); });
// Invalider le cache des statuts d'équipement
EquipmentStatusCalculator.invalidateGlobalCache();
} catch (e) { } catch (e) {
print('Error validating all unloading: $e'); print('Error validating all unloading: $e');
rethrow; rethrow;

View File

@@ -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_main_info_section.dart';
import 'package:em2rp/views/widgets/equipment/equipment_notes_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_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_price_section.dart';
import 'package:em2rp/views/widgets/equipment/equipment_maintenance_history_section.dart'; import 'package:em2rp/views/widgets/equipment/equipment_maintenance_history_section.dart';
import 'package:em2rp/views/widgets/equipment/equipment_dates_section.dart'; import 'package:em2rp/views/widgets/equipment/equipment_dates_section.dart';
@@ -107,11 +108,15 @@ class _EquipmentDetailPageState extends State<EquipmentDetailPage> {
const SizedBox(height: 24), 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), EquipmentAssociatedEventsSection(equipment: widget.equipment),
const SizedBox(height: 24), 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) if (isDesktop)
_buildDesktopTwoColumnLayout(hasManagePermission) _buildDesktopTwoColumnLayout(hasManagePermission)
else else

View File

@@ -10,6 +10,7 @@ import 'package:em2rp/views/equipment_form_page.dart';
import 'package:em2rp/views/equipment_detail_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_dialog.dart';
import 'package:em2rp/views/widgets/common/qr_code_format_selector_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/mixins/selection_mode_mixin.dart';
import 'package:em2rp/views/widgets/management/management_list.dart'; import 'package:em2rp/views/widgets/management/management_list.dart';
@@ -428,9 +429,6 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
_cachedEquipment = items; _cachedEquipment = items;
}, },
itemBuilder: (equipment) { itemBuilder: (equipment) {
// Trier les équipements par nom
final sortedEquipment = List<EquipmentModel>.from(_cachedEquipment ?? [equipment]);
sortedEquipment.sort((a, b) => a.name.compareTo(b.name));
return _buildEquipmentCard(equipment); return _buildEquipmentCard(equipment);
}, },
); );
@@ -444,7 +442,7 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
return Card( return Card(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
color: isSelectionMode && isSelected color: isSelectionMode && isSelected
? AppColors.rouge.withOpacity(0.1) ? AppColors.rouge.withValues(alpha: 0.1)
: null, : null,
child: ListTile( child: ListTile(
leading: isSelectionMode leading: isSelectionMode
@@ -454,11 +452,10 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
activeColor: AppColors.rouge, activeColor: AppColors.rouge,
) )
: CircleAvatar( : CircleAvatar(
backgroundColor: backgroundColor: equipment.category.color.withValues(alpha: 0.2),
equipment.status.color.withOpacity(0.2),
child: equipment.category.getIcon( child: equipment.category.getIcon(
size: 20, size: 20,
color: Colors.black, color: equipment.category.color,
), ),
), ),
title: Row( title: Row(
@@ -469,10 +466,10 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
style: const TextStyle(fontWeight: FontWeight.bold), 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 && if (equipment.category != EquipmentCategory.consumable &&
equipment.category != EquipmentCategory.cable) equipment.category != EquipmentCategory.cable)
_buildStatusBadge(equipment.status), EquipmentStatusBadge(equipment: equipment),
], ],
), ),
subtitle: Column( subtitle: Column(
@@ -607,24 +604,6 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
); );
} }
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 // Actions
void _createNewEquipment() { void _createNewEquipment() {

View File

@@ -87,7 +87,10 @@ class _EventPreparationButtonsState extends State<EventPreparationButtons> {
children: [ children: [
// Bouton de l'étape actuelle // Bouton de l'étape actuelle
if (!isCompleted) if (!isCompleted)
ElevatedButton.icon( Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: ElevatedButton.icon(
onPressed: () async { onPressed: () async {
final result = await Navigator.push( final result = await Navigator.push(
context, context,
@@ -118,12 +121,14 @@ class _EventPreparationButtonsState extends State<EventPreparationButtons> {
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.bleuFonce, backgroundColor: AppColors.bleuFonce,
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
), ),
), ),
),
),
// Indicateur de completion // Indicateur de completion
if (isCompleted) if (isCompleted)

View File

@@ -6,7 +6,6 @@ import 'package:em2rp/utils/colors.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
enum EventFilter { enum EventFilter {
current, // Événements en cours (préparés mais pas encore retournés)
upcoming, // Événements à venir upcoming, // Événements à venir
past, // Événements passés past, // Événements passés
} }
@@ -27,7 +26,7 @@ class EquipmentAssociatedEventsSection extends StatefulWidget {
class _EquipmentAssociatedEventsSectionState class _EquipmentAssociatedEventsSectionState
extends State<EquipmentAssociatedEventsSection> { extends State<EquipmentAssociatedEventsSection> {
EventFilter _selectedFilter = EventFilter.current; EventFilter _selectedFilter = EventFilter.upcoming;
List<EventModel> _events = []; List<EventModel> _events = [];
bool _isLoading = true; bool _isLoading = true;
@@ -41,38 +40,80 @@ class _EquipmentAssociatedEventsSectionState
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
// Récupérer TOUS les événements car on ne peut pas faire arrayContains sur un objet
final eventsSnapshot = await FirebaseFirestore.instance final eventsSnapshot = await FirebaseFirestore.instance
.collection('events') .collection('events')
.where('assignedEquipment',
arrayContains: {'equipmentId': widget.equipment.id})
.get(); .get();
final events = eventsSnapshot.docs final events = <EventModel>[];
.map((doc) => EventModel.fromMap(doc.data(), doc.id))
.toList(); // Récupérer toutes les boîtes pour vérifier leur contenu
final containersSnapshot = await FirebaseFirestore.instance
.collection('containers')
.get();
final containersWithEquipment = <String>[];
for (var containerDoc in containersSnapshot.docs) {
try {
final data = containerDoc.data();
final equipmentIds = List<String>.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 // Filtrer selon le statut
final now = DateTime.now(); final now = DateTime.now();
final filteredEvents = events.where((event) { final filteredEvents = events.where((event) {
switch (_selectedFilter) { // Un événement est EN COURS dès que la préparation est complétée
case EventFilter.current: // et jusqu'à ce que le retour soit complété
// Événement en cours = préparation complétée ET retour pas encore complété final isPrepared = event.preparationStatus == PreparationStatus.completed ||
return (event.preparationStatus == PreparationStatus.completed || event.preparationStatus == PreparationStatus.completedWithMissing;
event.preparationStatus ==
PreparationStatus.completedWithMissing) &&
(event.returnStatus == null ||
event.returnStatus == ReturnStatus.notStarted ||
event.returnStatus == ReturnStatus.inProgress);
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: case EventFilter.upcoming:
// Événements à venir = date de début dans le futur OU préparation pas encore faite // Événements à venir = date de début dans le futur
return event.startDateTime.isAfter(now) || return event.startDateTime.isAfter(now);
event.preparationStatus == PreparationStatus.notStarted;
case EventFilter.past: case EventFilter.past:
// Événements passés = retour complété // Événements passés = date de fin passée
return event.returnStatus == ReturnStatus.completed || return event.endDateTime.isBefore(now);
event.returnStatus == ReturnStatus.completedWithMissing;
} }
}).toList(); }).toList();
@@ -98,8 +139,6 @@ class _EquipmentAssociatedEventsSectionState
String _getFilterLabel(EventFilter filter) { String _getFilterLabel(EventFilter filter) {
switch (filter) { switch (filter) {
case EventFilter.current:
return 'En cours';
case EventFilter.upcoming: case EventFilter.upcoming:
return 'À venir'; return 'À venir';
case EventFilter.past: case EventFilter.past:
@@ -109,8 +148,6 @@ class _EquipmentAssociatedEventsSectionState
IconData _getFilterIcon(EventFilter filter) { IconData _getFilterIcon(EventFilter filter) {
switch (filter) { switch (filter) {
case EventFilter.current:
return Icons.play_circle;
case EventFilter.upcoming: case EventFilter.upcoming:
return Icons.upcoming; return Icons.upcoming;
case EventFilter.past: case EventFilter.past:
@@ -134,7 +171,7 @@ class _EquipmentAssociatedEventsSectionState
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
'Événements associés', 'Événements passés / à venir',
style: Theme.of(context).textTheme.titleLarge?.copyWith( style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -225,11 +262,28 @@ class _EquipmentAssociatedEventsSectionState
Widget _buildEventCard(EventModel event) { Widget _buildEventCard(EventModel event) {
final dateFormat = DateFormat('dd/MM/yyyy HH:mm'); final dateFormat = DateFormat('dd/MM/yyyy HH:mm');
final isInProgress = (event.preparationStatus == PreparationStatus.completed ||
event.preparationStatus == PreparationStatus.completedWithMissing) && // Un événement est en cours dès que la préparation est complétée
(event.returnStatus == null || // et jusqu'à ce que le retour soit complété
event.returnStatus == ReturnStatus.notStarted || final isPrepared = event.preparationStatus == PreparationStatus.completed ||
event.returnStatus == ReturnStatus.inProgress); 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( return Card(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
@@ -290,16 +344,46 @@ class _EquipmentAssociatedEventsSectionState
children: [ children: [
const Icon(Icons.calendar_today, size: 14, color: Colors.grey), const Icon(Icons.calendar_today, size: 14, color: Colors.grey),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Expanded(
child: Text(
'${dateFormat.format(event.startDateTime)}${dateFormat.format(event.endDateTime)}', '${dateFormat.format(event.startDateTime)}${dateFormat.format(event.endDateTime)}',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: Colors.grey.shade700, 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), const SizedBox(height: 8),
// Statuts de préparation et retour // 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,
),
),
),
],
),
],
], ],
), ),
), ),

View File

@@ -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<EquipmentCurrentEventsSection> createState() =>
_EquipmentCurrentEventsSectionState();
}
class _EquipmentCurrentEventsSectionState
extends State<EquipmentCurrentEventsSection> {
List<EventModel> _events = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadCurrentEvents();
}
Future<void> _loadCurrentEvents() async {
setState(() => _isLoading = true);
try {
// Récupérer TOUS les événements
final eventsSnapshot = await FirebaseFirestore.instance
.collection('events')
.get();
final events = <EventModel>[];
// Récupérer toutes les boîtes pour vérifier leur contenu
final containersSnapshot = await FirebaseFirestore.instance
.collection('containers')
.get();
final containersWithEquipment = <String>[];
for (var containerDoc in containersSnapshot.docs) {
try {
final data = containerDoc.data();
final equipmentIds = List<String>.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,
),
),
],
),
),
],
],
),
),
),
);
}
}

View File

@@ -40,7 +40,7 @@ class EquipmentHeaderSection extends StatelessWidget {
radius: 30, radius: 30,
child: equipment.category.getIcon( child: equipment.category.getIcon(
size: 32, size: 32,
color: AppColors.rouge, color: equipment.category.color,
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),

View File

@@ -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<EquipmentProvider>(context, listen: false);
print('[EquipmentStatusBadge] Building badge for ${equipment.id}');
return FutureBuilder<EquipmentStatus>(
// 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,
),
),
);
},
);
}
}