feat: ajout de la gestion de la préparation d'un événement avec page permettant de le gérer

This commit is contained in:
ElPoyo
2026-01-06 10:53:23 +01:00
parent fa1d6a4295
commit 25d395b41a
18 changed files with 2121 additions and 500 deletions

View File

@@ -7,6 +7,7 @@ import 'package:em2rp/providers/event_provider.dart';
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_navigation.dart';
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_header.dart';
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_status_button.dart';
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart';
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_info.dart';
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_description.dart';
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_documents.dart';
@@ -60,6 +61,8 @@ class EventDetails extends StatelessWidget {
onSelectEvent: onSelectEvent,
),
),
// Boutons de préparation et retour
EventPreparationButtons(event: event),
const SizedBox(height: 16),
Expanded(
child: SingleChildScrollView(

View File

@@ -98,8 +98,8 @@ class _EventDetailsHeaderState extends State<EventDetailsHeader> {
_buildStatusIcon(widget.event.status),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.calendar_today, color: AppColors.rouge),
tooltip: 'Exporter vers Google Calendar',
icon: const Icon(Icons.add_to_home_screen, color: AppColors.rouge),
tooltip: 'Ajouter a mon application de calendrier',
onPressed: _exportToCalendar,
),
if (Provider.of<LocalUserProvider>(context, listen: false)

View File

@@ -0,0 +1,160 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/views/event_preparation_page.dart';
import 'package:em2rp/utils/colors.dart';
/// Boutons de préparation et retour d'événement
class EventPreparationButtons extends StatefulWidget {
final EventModel event;
const EventPreparationButtons({
super.key,
required this.event,
});
@override
State<EventPreparationButtons> createState() => _EventPreparationButtonsState();
}
class _EventPreparationButtonsState extends State<EventPreparationButtons> {
@override
Widget build(BuildContext context) {
// Écouter les changements de l'événement en temps réel
return StreamBuilder<DocumentSnapshot>(
stream: FirebaseFirestore.instance
.collection('events')
.doc(widget.event.id)
.snapshots(),
initialData: null,
builder: (context, snapshot) {
// Utiliser l'événement du stream si disponible, sinon l'événement initial
final EventModel currentEvent;
if (snapshot.hasData && snapshot.data != null && snapshot.data!.exists) {
currentEvent = EventModel.fromMap(
snapshot.data!.data() as Map<String, dynamic>,
snapshot.data!.id,
);
} else {
currentEvent = widget.event;
}
return _buildButtons(context, currentEvent);
},
);
}
Widget _buildButtons(BuildContext context, EventModel event) {
// Vérifier s'il y a du matériel assigné
final hasMaterial = event.assignedEquipment.isNotEmpty || event.assignedContainers.isNotEmpty;
if (!hasMaterial) {
return const SizedBox.shrink();
}
// Déterminer l'étape actuelle
final prep = event.preparationStatus ?? PreparationStatus.notStarted;
final loading = event.loadingStatus ?? LoadingStatus.notStarted;
final unloading = event.unloadingStatus ?? UnloadingStatus.notStarted;
final returnStatus = event.returnStatus ?? ReturnStatus.notStarted;
String buttonText;
IconData buttonIcon;
bool isCompleted = false;
if (prep != PreparationStatus.completed) {
buttonText = 'Préparation dépôt';
buttonIcon = Icons.inventory_2;
} else if (loading != LoadingStatus.completed) {
buttonText = 'Chargement aller';
buttonIcon = Icons.local_shipping;
} else if (unloading != UnloadingStatus.completed) {
buttonText = 'Chargement retour';
buttonIcon = Icons.unarchive;
} else if (returnStatus != ReturnStatus.completed) {
buttonText = 'Retour dépôt';
buttonIcon = Icons.assignment_return;
} else {
buttonText = 'Terminé';
buttonIcon = Icons.check_circle;
isCompleted = true;
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Bouton de l'étape actuelle
if (!isCompleted)
ElevatedButton.icon(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EventPreparationPage(
initialEvent: event,
),
),
);
// Si la validation a réussi, le StreamBuilder se rechargera automatiquement
if (result == true && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Étape validée avec succès'),
backgroundColor: Colors.green,
),
);
}
},
icon: Icon(buttonIcon),
label: Text(
buttonText,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.bleuFonce,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
// Indicateur de completion
if (isCompleted)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green, width: 1),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.check_circle, color: Colors.green, size: 20),
SizedBox(width: 8),
Text(
'Toutes les étapes sont terminées',
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,161 @@
import 'package:flutter/material.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/utils/colors.dart';
/// Type d'étape pour le checklist
enum ChecklistStep {
preparation,
loading,
unloading,
return_,
}
/// Widget pour afficher un équipement dans une checklist de préparation/retour
class EquipmentChecklistItem extends StatelessWidget {
final EquipmentModel equipment;
final EventEquipment eventEquipment;
final ChecklistStep step;
final bool isValidated; // État de validation (passé depuis le parent)
final VoidCallback onToggle;
final ValueChanged<int>? onReturnedQuantityChanged;
const EquipmentChecklistItem({
super.key,
required this.equipment,
required this.eventEquipment,
this.step = ChecklistStep.preparation,
required this.isValidated,
required this.onToggle,
this.onReturnedQuantityChanged,
});
@override
Widget build(BuildContext context) {
final hasQuantity = equipment.hasQuantity;
final showQuantityInput = step == ChecklistStep.return_ && hasQuantity;
return Card(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 0),
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: isValidated ? Colors.green : Colors.grey.shade300,
width: isValidated ? 2 : 1,
),
),
child: ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isValidated ? Colors.green.shade100 : Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: IconButton(
icon: Icon(
isValidated ? Icons.check_circle : Icons.radio_button_unchecked,
color: isValidated ? Colors.green : Colors.grey,
),
onPressed: onToggle,
padding: EdgeInsets.zero,
),
),
title: Text(
equipment.name,
style: TextStyle(
fontWeight: FontWeight.w600,
decoration: isValidated ? TextDecoration.lineThrough : null,
color: isValidated ? Colors.grey : null,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (equipment.model != null)
Text(
equipment.model!,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
if (hasQuantity) ...[
const SizedBox(height: 4),
Row(
children: [
Text(
'Quantité : ${eventEquipment.quantity}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.bleuFonce,
),
),
if (showQuantityInput && onReturnedQuantityChanged != null) ...[
const SizedBox(width: 16),
const Icon(Icons.arrow_forward, size: 12, color: Colors.grey),
const SizedBox(width: 8),
Text(
'Retourné : ',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
SizedBox(
width: 80,
child: TextField(
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
),
hintText: '${eventEquipment.quantity}',
),
keyboardType: TextInputType.number,
onChanged: (value) {
final qty = int.tryParse(value) ?? eventEquipment.quantity;
onReturnedQuantityChanged!(qty);
},
style: const TextStyle(fontSize: 12),
),
),
],
],
),
],
],
),
trailing: isValidated
? Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.check, size: 16, color: Colors.green),
SizedBox(width: 4),
Text(
'Validé',
style: TextStyle(
color: Colors.green,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
)
: null,
),
);
}
}

View File

@@ -0,0 +1,192 @@
import 'package:flutter/material.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/utils/colors.dart';
/// Dialog affichant les équipements manquants lors de la préparation/retour
class MissingEquipmentDialog extends StatelessWidget {
final List<EquipmentModel> missingEquipment;
final List<EventEquipment> eventEquipment;
final bool isReturnMode;
const MissingEquipmentDialog({
super.key,
required this.missingEquipment,
required this.eventEquipment,
this.isReturnMode = false,
});
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
constraints: const BoxConstraints(maxWidth: 600, maxHeight: 600),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// En-tête avec icône warning
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.shade100,
shape: BoxShape.circle,
),
child: Icon(
Icons.warning_amber_rounded,
size: 32,
color: Colors.orange.shade700,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isReturnMode
? 'Équipements manquants au retour'
: 'Équipements manquants',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'${missingEquipment.length} équipement(s) non validé(s)',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade700,
),
),
],
),
),
],
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 8),
// Liste des équipements manquants
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: missingEquipment.length,
itemBuilder: (context, index) {
final equipment = missingEquipment[index];
final eventEq = eventEquipment.firstWhere(
(eq) => eq.equipmentId == equipment.id,
orElse: () => EventEquipment(equipmentId: equipment.id),
);
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.orange.shade100,
child: equipment.category.getIcon(
size: 20,
color: Colors.orange.shade700,
),
),
title: Text(
equipment.name,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: equipment.hasQuantity
? Text('Quantité : ${eventEq.quantity}')
: Text(equipment.model ?? equipment.category.label),
trailing: Icon(
Icons.error_outline,
color: Colors.orange.shade700,
),
),
);
},
),
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 8),
// Actions
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Bouton principal : Confirmer malgré les manquants
ElevatedButton(
onPressed: () => Navigator.of(context).pop('confirm_anyway'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.rouge,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
isReturnMode
? 'Confirmer le retour malgré les manquants'
: 'Confirmer malgré les manquants',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 12),
// Bouton secondaire : Marquer comme validés
OutlinedButton(
onPressed: () => Navigator.of(context).pop('mark_as_validated'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
side: BorderSide(color: AppColors.bleuFonce, width: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'Indiquer les manquants comme validés',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColors.bleuFonce,
),
),
),
const SizedBox(height: 12),
// Bouton tertiaire : Retourner à la liste
TextButton(
onPressed: () => Navigator.of(context).pop('return_to_list'),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text(
'Retourner à la liste',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
),
],
),
],
),
),
);
}
}

View File

@@ -24,8 +24,22 @@ class _EquipmentConflictDialogState extends State<EquipmentConflictDialog> {
.where((entry) => !_removedEquipmentIds.contains(entry.key))
.fold(0, (sum, entry) => sum + entry.value.length);
/// Retourne l'icône appropriée selon le type de conflit
IconData _getIconForConflict(AvailabilityConflict conflict) {
switch (conflict.type) {
case ConflictType.containerFullyUsed:
case ConflictType.containerPartiallyUsed:
return Icons.inventory_2;
case ConflictType.insufficientQuantity:
return Icons.production_quantity_limits;
case ConflictType.equipmentUnavailable:
return Icons.block;
}
}
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat('dd/MM/yyyy');
return Dialog(
@@ -117,19 +131,37 @@ class _EquipmentConflictDialogState extends State<EquipmentConflictDialog> {
Row(
children: [
Icon(
Icons.inventory_2,
_getIconForConflict(firstConflict),
color: isRemoved ? Colors.grey : AppColors.rouge,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
firstConflict.equipmentName,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
decoration: isRemoved ? TextDecoration.lineThrough : null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
firstConflict.equipmentName,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
decoration: isRemoved ? TextDecoration.lineThrough : null,
),
),
// Message de conflit spécifique
if (firstConflict.conflictMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
firstConflict.conflictMessage,
style: TextStyle(
fontSize: 13,
color: Colors.orange.shade700,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
if (isRemoved)

View File

@@ -213,28 +213,54 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
setState(() => _isLoadingConflicts = true);
try {
print('[EquipmentSelectionDialog] Loading equipment conflicts...');
final equipmentProvider = context.read<EquipmentProvider>();
final equipment = await equipmentProvider.equipmentStream.first;
print('[EquipmentSelectionDialog] Checking conflicts for ${equipment.length} equipments');
for (var eq in equipment) {
final conflicts = await _availabilityService.checkEquipmentAvailability(
equipmentId: eq.id,
equipmentName: eq.id,
startDate: widget.startDate,
endDate: widget.endDate,
excludeEventId: widget.excludeEventId,
);
// Pour les consommables/câbles, vérifier avec gestion de quantité
if (eq.hasQuantity) {
// Récupérer la quantité disponible
final availableQty = await _availabilityService.getAvailableQuantity(
equipment: eq,
startDate: widget.startDate,
endDate: widget.endDate,
excludeEventId: widget.excludeEventId,
);
if (conflicts.isNotEmpty) {
print('[EquipmentSelectionDialog] Found ${conflicts.length} conflict(s) for ${eq.id}');
_equipmentConflicts[eq.id] = conflicts;
// Vérifier si un item de cet équipement est déjà sélectionné
final selectedItem = _selectedItems[eq.id];
final requestedQty = selectedItem?.quantity ?? 1;
// ✅ Ne créer un conflit QUE si la quantité demandée dépasse la quantité disponible
if (requestedQty > availableQty) {
final conflicts = await _availabilityService.checkEquipmentAvailabilityWithQuantity(
equipment: eq,
requestedQuantity: requestedQty,
startDate: widget.startDate,
endDate: widget.endDate,
excludeEventId: widget.excludeEventId,
);
if (conflicts.isNotEmpty) {
_equipmentConflicts[eq.id] = conflicts;
}
}
// Sinon, pas de conflit à afficher dans la liste
} else {
// Pour les équipements non quantifiables, vérification classique
final conflicts = await _availabilityService.checkEquipmentAvailability(
equipmentId: eq.id,
equipmentName: eq.id,
startDate: widget.startDate,
endDate: widget.endDate,
excludeEventId: widget.excludeEventId,
);
if (conflicts.isNotEmpty) {
_equipmentConflicts[eq.id] = conflicts;
}
}
}
print('[EquipmentSelectionDialog] Total equipments with conflicts: ${_equipmentConflicts.length}');
} catch (e) {
print('[EquipmentSelectionDialog] Error loading conflicts: $e');
} finally {
@@ -247,32 +273,76 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
try {
print('[EquipmentSelectionDialog] Loading container conflicts...');
final containerProvider = context.read<ContainerProvider>();
final equipmentProvider = context.read<EquipmentProvider>();
final containers = await containerProvider.containersStream.first;
final allEquipment = await equipmentProvider.equipmentStream.first;
print('[EquipmentSelectionDialog] Checking conflicts for ${containers.length} containers');
for (var container in containers) {
final conflictingChildren = <String>[];
// Vérifier d'abord si la boîte complète est utilisée ailleurs
final containerEquipment = allEquipment
.where((eq) => container.equipmentIds.contains(eq.id))
.toList();
// Vérifier chaque équipement enfant
for (var equipmentId in container.equipmentIds) {
if (_equipmentConflicts.containsKey(equipmentId)) {
conflictingChildren.add(equipmentId);
}
}
final containerConflicts = await _availabilityService.checkContainerAvailability(
container: container,
containerEquipment: containerEquipment,
startDate: widget.startDate,
endDate: widget.endDate,
excludeEventId: widget.excludeEventId,
);
if (conflictingChildren.isNotEmpty) {
final status = conflictingChildren.length == container.equipmentIds.length
? ContainerConflictStatus.complete
: ContainerConflictStatus.partial;
_containerConflicts[container.id] = ContainerConflictInfo(
status: status,
conflictingEquipmentIds: conflictingChildren,
totalChildren: container.equipmentIds.length,
if (containerConflicts.isNotEmpty) {
// Déterminer le statut en fonction du type de conflit
final hasFullConflict = containerConflicts.any(
(c) => c.type == ConflictType.containerFullyUsed,
);
print('[EquipmentSelectionDialog] Container ${container.id}: ${status.name} conflict (${conflictingChildren.length}/${container.equipmentIds.length} children)');
final conflictingChildren = containerConflicts
.where((c) => c.type != ConflictType.containerFullyUsed &&
c.type != ConflictType.containerPartiallyUsed)
.map((c) => c.equipmentId)
.toList();
final status = hasFullConflict
? ContainerConflictStatus.complete
: (conflictingChildren.isNotEmpty
? ContainerConflictStatus.partial
: ContainerConflictStatus.none);
if (status != ContainerConflictStatus.none) {
_containerConflicts[container.id] = ContainerConflictInfo(
status: status,
conflictingEquipmentIds: conflictingChildren,
totalChildren: container.equipmentIds.length,
);
print('[EquipmentSelectionDialog] Container ${container.id}: ${status.name} conflict');
}
} else {
// Vérifier chaque équipement enfant individuellement
final conflictingChildren = <String>[];
for (var equipmentId in container.equipmentIds) {
if (_equipmentConflicts.containsKey(equipmentId)) {
conflictingChildren.add(equipmentId);
}
}
if (conflictingChildren.isNotEmpty) {
final status = conflictingChildren.length == container.equipmentIds.length
? ContainerConflictStatus.complete
: ContainerConflictStatus.partial;
_containerConflicts[container.id] = ContainerConflictInfo(
status: status,
conflictingEquipmentIds: conflictingChildren,
totalChildren: container.equipmentIds.length,
);
print('[EquipmentSelectionDialog] Container ${container.id}: ${status.name} conflict (${conflictingChildren.length}/${container.equipmentIds.length} children)');
}
}
}

View File

@@ -62,10 +62,6 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
setState(() => _isLoading = true);
try {
print('[EventAssignedEquipmentSection] Loading equipment and containers...');
print('[EventAssignedEquipmentSection] assignedEquipment: ${widget.assignedEquipment.map((e) => e.equipmentId).toList()}');
print('[EventAssignedEquipmentSection] assignedContainers: ${widget.assignedContainers}');
final equipmentProvider = context.read<EquipmentProvider>();
final containerProvider = context.read<ContainerProvider>();
@@ -73,62 +69,40 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
final equipment = await equipmentProvider.equipmentStream.first;
final containers = await containerProvider.containersStream.first;
print('[EventAssignedEquipmentSection] Available equipment count: ${equipment.length}');
print('[EventAssignedEquipmentSection] Available containers count: ${containers.length}');
// Créer le cache des équipements
for (var eq in widget.assignedEquipment) {
print('[EventAssignedEquipmentSection] Looking for equipment: ${eq.equipmentId}');
final equipmentItem = equipment.firstWhere(
(e) {
print('[EventAssignedEquipmentSection] Comparing "${e.id}" with "${eq.equipmentId}"');
return e.id == eq.equipmentId;
},
orElse: () {
print('[EventAssignedEquipmentSection] Equipment NOT FOUND: ${eq.equipmentId}');
return EquipmentModel(
id: eq.equipmentId,
name: 'Équipement inconnu',
category: EquipmentCategory.other,
status: EquipmentStatus.available,
parentBoxIds: [],
maintenanceIds: [],
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
},
(e) => e.id == eq.equipmentId,
orElse: () => EquipmentModel(
id: eq.equipmentId,
name: 'Équipement inconnu',
category: EquipmentCategory.other,
status: EquipmentStatus.available,
parentBoxIds: [],
maintenanceIds: [],
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
);
_equipmentCache[eq.equipmentId] = equipmentItem;
print('[EventAssignedEquipmentSection] Cached equipment: ${equipmentItem.id} (${equipmentItem.name})');
}
// Créer le cache des conteneurs
for (var containerId in widget.assignedContainers) {
print('[EventAssignedEquipmentSection] Looking for container: $containerId');
final container = containers.firstWhere(
(c) {
print('[EventAssignedEquipmentSection] Comparing "${c.id}" with "$containerId"');
return c.id == containerId;
},
orElse: () {
print('[EventAssignedEquipmentSection] Container NOT FOUND: $containerId');
return ContainerModel(
id: containerId,
name: 'Conteneur inconnu',
type: ContainerType.flightCase,
status: EquipmentStatus.available,
equipmentIds: [],
updatedAt: DateTime.now(),
createdAt: DateTime.now(),
);
},
(c) => c.id == containerId,
orElse: () => ContainerModel(
id: containerId,
name: 'Conteneur inconnu',
type: ContainerType.flightCase,
status: EquipmentStatus.available,
equipmentIds: [],
updatedAt: DateTime.now(),
createdAt: DateTime.now(),
),
);
_containerCache[containerId] = container;
print('[EventAssignedEquipmentSection] Cached container: ${container.id} (${container.name})');
}
print('[EventAssignedEquipmentSection] Equipment cache: ${_equipmentCache.keys.toList()}');
print('[EventAssignedEquipmentSection] Container cache: ${_containerCache.keys.toList()}');
} catch (e) {
print('[EventAssignedEquipmentSection] Error loading equipment/containers: $e');
} finally {
@@ -173,18 +147,16 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
}
}
// Charger les équipements des conteneurs pour vérifier les conflits
// Charger les équipements et conteneurs
final containerProvider = context.read<ContainerProvider>();
final equipmentProvider = context.read<EquipmentProvider>();
final allContainers = await containerProvider.containersStream.first;
final allEquipment = await equipmentProvider.equipmentStream.first;
// Collecter TOUS les équipements à vérifier (directs + enfants des boîtes)
final equipmentIds = newEquipment.map((e) => e.equipmentId).toList();
final equipmentNames = <String, String>{};
final allConflicts = <String, List<AvailabilityConflict>>{};
// Ajouter les équipements directs
// 1. Vérifier les conflits pour les équipements directs
for (var eq in newEquipment) {
final equipment = allEquipment.firstWhere(
(e) => e.id == eq.equipmentId,
@@ -199,10 +171,51 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
updatedAt: DateTime.now(),
),
);
equipmentNames[eq.equipmentId] = equipment.id;
// Pour les équipements quantifiables (consommables/câbles)
if (equipment.hasQuantity) {
// Vérifier la quantité disponible
final availableQty = await _availabilityService.getAvailableQuantity(
equipment: equipment,
startDate: widget.startDate!,
endDate: widget.endDate!,
excludeEventId: widget.eventId,
);
// ⚠️ Ne créer un conflit QUE si la quantité demandée est supérieure à la quantité disponible
if (eq.quantity > availableQty) {
// Il y a vraiment un conflit de quantité
final conflicts = await _availabilityService.checkEquipmentAvailabilityWithQuantity(
equipment: equipment,
requestedQuantity: eq.quantity,
startDate: widget.startDate!,
endDate: widget.endDate!,
excludeEventId: widget.eventId,
);
// Ne garder que les conflits réels (quand il n'y a pas assez de stock)
if (conflicts.isNotEmpty) {
allConflicts[eq.equipmentId] = conflicts;
}
}
// ✅ Sinon, pas de conflit : il y a assez de stock disponible
} else {
// Pour les équipements non quantifiables (vérification classique)
final conflicts = await _availabilityService.checkEquipmentAvailability(
equipmentId: equipment.id,
equipmentName: equipment.name,
startDate: widget.startDate!,
endDate: widget.endDate!,
excludeEventId: widget.eventId,
);
if (conflicts.isNotEmpty) {
allConflicts[eq.equipmentId] = conflicts;
}
}
}
// Ajouter les équipements des conteneurs (par composition)
// 2. Vérifier les conflits pour les boîtes et leur contenu
for (var containerId in newContainers) {
final container = allContainers.firstWhere(
(c) => c.id == containerId,
@@ -217,76 +230,105 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
),
);
// Ajouter tous les équipements enfants pour vérification
for (var childEquipmentId in container.equipmentIds) {
if (!equipmentIds.contains(childEquipmentId)) {
equipmentIds.add(childEquipmentId);
// Récupérer les équipements de la boîte
final containerEquipment = container.equipmentIds
.map((eqId) => allEquipment.firstWhere(
(e) => e.id == eqId,
orElse: () => EquipmentModel(
id: eqId,
name: 'Inconnu',
category: EquipmentCategory.other,
status: EquipmentStatus.available,
parentBoxIds: [],
maintenanceIds: [],
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
))
.toList();
final equipment = allEquipment.firstWhere(
(e) => e.id == childEquipmentId,
orElse: () => EquipmentModel(
id: childEquipmentId,
name: 'Inconnu',
category: EquipmentCategory.other,
status: EquipmentStatus.available,
parentBoxIds: [],
maintenanceIds: [],
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
// Vérifier chaque équipement de la boîte individuellement
final containerConflicts = <AvailabilityConflict>[];
for (var equipment in containerEquipment) {
if (equipment.hasQuantity) {
// Pour les consommables/câbles, vérifier la quantité disponible
final availableQty = await _availabilityService.getAvailableQuantity(
equipment: equipment,
startDate: widget.startDate!,
endDate: widget.endDate!,
excludeEventId: widget.eventId,
);
equipmentNames[childEquipmentId] = '${equipment.id} (dans ${container.name})';
// La boîte contient 1 unité de cet équipement
// Si la quantité disponible est insuffisante, créer un conflit
if (availableQty < 1) {
final conflicts = await _availabilityService.checkEquipmentAvailability(
equipmentId: equipment.id,
equipmentName: equipment.name,
startDate: widget.startDate!,
endDate: widget.endDate!,
excludeEventId: widget.eventId,
);
containerConflicts.addAll(conflicts);
}
} else {
// Pour les équipements non quantifiables
final conflicts = await _availabilityService.checkEquipmentAvailability(
equipmentId: equipment.id,
equipmentName: equipment.name,
startDate: widget.startDate!,
endDate: widget.endDate!,
excludeEventId: widget.eventId,
);
containerConflicts.addAll(conflicts);
}
}
if (containerConflicts.isNotEmpty) {
allConflicts[containerId] = containerConflicts;
}
}
// Vérifier les conflits pour TOUS les équipements (directs + enfants)
final conflicts = await _availabilityService.checkMultipleEquipmentAvailability(
equipmentIds: equipmentIds,
equipmentNames: equipmentNames,
startDate: widget.startDate!,
endDate: widget.endDate!,
excludeEventId: widget.eventId,
);
if (conflicts.isNotEmpty) {
if (allConflicts.isNotEmpty) {
// Afficher le dialog de conflits
final action = await showDialog<String>(
context: context,
builder: (context) => EquipmentConflictDialog(conflicts: conflicts),
builder: (context) => EquipmentConflictDialog(conflicts: allConflicts),
);
if (action == 'cancel') {
return; // Annuler l'ajout
} else if (action == 'force_removed') {
// Identifier quels équipements retirer
final removedIds = conflicts.keys.toSet();
// Identifier quels équipements/conteneurs retirer
final removedIds = allConflicts.keys.toSet();
// Retirer les équipements directs en conflit
newEquipment.removeWhere((eq) => removedIds.contains(eq.equipmentId));
// Retirer les boîtes dont au moins un équipement enfant est en conflit
final containersToRemove = <String>[];
for (var containerId in newContainers) {
final container = allContainers.firstWhere((c) => c.id == containerId);
final hasConflict = container.equipmentIds.any((eqId) => removedIds.contains(eqId));
// Retirer les boîtes en conflit
newContainers.removeWhere((containerId) => removedIds.contains(containerId));
if (hasConflict) {
containersToRemove.add(containerId);
}
}
for (var containerId in containersToRemove) {
newContainers.remove(containerId);
// Informer l'utilisateur
// Informer l'utilisateur des boîtes retirées
for (var containerId in removedIds.where((id) => newContainers.contains(id))) {
if (mounted) {
final containerName = allContainers.firstWhere((c) => c.id == containerId).name;
final container = allContainers.firstWhere(
(c) => c.id == containerId,
orElse: () => ContainerModel(
id: containerId,
name: 'Inconnu',
type: ContainerType.flightCase,
status: EquipmentStatus.available,
equipmentIds: [],
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('La boîte "$containerName" a été retirée car elle contient du matériel en conflit.'),
content: Text('La boîte "${container.name}" a été retirée en raison de conflits.'),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 4),
duration: const Duration(seconds: 3),
),
);
}
@@ -299,8 +341,21 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
final updatedEquipment = [...widget.assignedEquipment];
final updatedContainers = [...widget.assignedContainers];
// Pour chaque nouvel équipement
for (var eq in newEquipment) {
if (!updatedEquipment.any((e) => e.equipmentId == eq.equipmentId)) {
final existingIndex = updatedEquipment.indexWhere((e) => e.equipmentId == eq.equipmentId);
if (existingIndex != -1) {
// L'équipement existe déjà : mettre à jour la quantité
updatedEquipment[existingIndex] = EventEquipment(
equipmentId: eq.equipmentId,
quantity: eq.quantity, // Utiliser la nouvelle quantité
isPrepared: updatedEquipment[existingIndex].isPrepared,
isReturned: updatedEquipment[existingIndex].isReturned,
returnedQuantity: updatedEquipment[existingIndex].returnedQuantity,
);
} else {
// L'équipement n'existe pas : l'ajouter
updatedEquipment.add(eq);
}
}
@@ -347,11 +402,6 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
return true;
}).toList();
print('[EventAssignedEquipmentSection] Removing container $containerId');
if (container != null) {
print('[EventAssignedEquipmentSection] Removing ${container.equipmentIds.length} children: ${container.equipmentIds}');
}
print('[EventAssignedEquipmentSection] Equipment before: ${widget.assignedEquipment.length}, after: ${updatedEquipment.length}');
// Notifier le changement avec les deux listes mises à jour
widget.onChanged(updatedEquipment, updatedContainers);

View File

@@ -22,7 +22,6 @@ class UserCard extends StatefulWidget {
class _UserCardState extends State<UserCard> {
ImageProvider? _profileImage;
String? _lastUrl;
bool _isLoadingImage = false;
@override
@@ -44,7 +43,6 @@ class _UserCardState extends State<UserCard> {
if (url.isNotEmpty) {
setState(() {
_isLoadingImage = true;
_lastUrl = url;
});
final image = NetworkImage(url);
image.resolve(const ImageConfiguration()).addListener(
@@ -71,7 +69,6 @@ class _UserCardState extends State<UserCard> {
setState(() {
_profileImage = null;
_isLoadingImage = false;
_lastUrl = null;
});
}
}