feat: add current events section for equipment with dynamic status calculation
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
235
em2rp/lib/services/equipment_status_calculator.dart
Normal file
235
em2rp/lib/services/equipment_status_calculator.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user