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: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<EquipmentModel> _equipment = [];
|
||||
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 ===
|
||||
|
||||
/// Définir la catégorie sélectionnée
|
||||
|
||||
@@ -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');
|
||||
|
||||
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/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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<EquipmentDetailPage> {
|
||||
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
|
||||
|
||||
@@ -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<EquipmentManagementPage>
|
||||
_cachedEquipment = items;
|
||||
},
|
||||
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);
|
||||
},
|
||||
);
|
||||
@@ -444,7 +442,7 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
||||
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<EquipmentManagementPage>
|
||||
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<EquipmentManagementPage>
|
||||
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<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
|
||||
void _createNewEquipment() {
|
||||
|
||||
@@ -87,7 +87,10 @@ class _EventPreparationButtonsState extends State<EventPreparationButtons> {
|
||||
children: [
|
||||
// Bouton de l'étape actuelle
|
||||
if (!isCompleted)
|
||||
ElevatedButton.icon(
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final result = await Navigator.push(
|
||||
context,
|
||||
@@ -118,12 +121,14 @@ class _EventPreparationButtonsState extends State<EventPreparationButtons> {
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.bleuFonce,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Indicateur de completion
|
||||
if (isCompleted)
|
||||
|
||||
@@ -6,7 +6,6 @@ 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
|
||||
}
|
||||
@@ -27,7 +26,7 @@ class EquipmentAssociatedEventsSection extends StatefulWidget {
|
||||
|
||||
class _EquipmentAssociatedEventsSectionState
|
||||
extends State<EquipmentAssociatedEventsSection> {
|
||||
EventFilter _selectedFilter = EventFilter.current;
|
||||
EventFilter _selectedFilter = EventFilter.upcoming;
|
||||
List<EventModel> _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 = <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('[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(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
child: equipment.category.getIcon(
|
||||
size: 32,
|
||||
color: AppColors.rouge,
|
||||
color: equipment.category.color,
|
||||
),
|
||||
),
|
||||
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