feat: Refactor event equipment management with advanced selection and conflict detection
This commit introduces a complete overhaul of how equipment is assigned to events, focusing on an enhanced user experience, advanced selection capabilities, and robust conflict detection.
**Key Features & Enhancements:**
- **Advanced Equipment Selection UI (`EquipmentSelectionDialog`):**
- New full-screen dialog to select equipment and containers ("boîtes") for an event.
- Hierarchical view showing containers and a flat list of all individual equipment.
- Real-time search and filtering by equipment category.
- Side panel summarizing the current selection and providing recommendations for containers based on selected equipment.
- Supports quantity selection for consumables and cables.
- **Conflict Detection & Management (`EventAvailabilityService`):**
- A new service (`EventAvailabilityService`) checks for equipment availability against other events based on the selected date range.
- The selection dialog visually highlights equipment and containers with scheduling conflicts (e.g., already used, partially unavailable).
- A dedicated conflict resolution dialog (`EquipmentConflictDialog`) appears if conflicting items are selected, allowing the user to either remove them or force the assignment.
- **Integrated Event Form (`EventAssignedEquipmentSection`):**
- The event creation/editing form now includes a new section for managing assigned equipment.
- It clearly displays assigned containers and standalone equipment, showing the composition of each container.
- Integrates the new selection dialog, ensuring all assignments are checked for conflicts before being saved.
- **Event Preparation & Return Workflow (`EventPreparationPage`):**
- New page (`EventPreparationPage`) for managing the check-out (preparation) and check-in (return) of equipment for an event.
- Provides a checklist of all assigned equipment.
- Users can validate each item, with options to "validate all" or finalize with missing items.
- Includes a dialog (`MissingEquipmentDialog`) to handle discrepancies.
- Supports tracking returned quantities for consumables.
**Data Model and Other Changes:**
- The `EventModel` now includes `assignedContainers` to explicitly link containers to an event.
- `EquipmentAssociatedEventsSection` on the equipment detail page is now functional, displaying current, upcoming, and past events for that item.
- Added deployment and versioning scripts (`scripts/deploy.js`, `scripts/increment_version.js`, `scripts/toggle_env.js`) to automate the release process.
- Introduced an application version display in the main drawer (`AppVersion`).
This commit is contained in:
@@ -0,0 +1,703 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/models/equipment_model.dart';
|
||||
import 'package:em2rp/models/container_model.dart';
|
||||
import 'package:em2rp/providers/equipment_provider.dart';
|
||||
import 'package:em2rp/providers/container_provider.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
import 'package:em2rp/views/widgets/event/equipment_selection_dialog.dart';
|
||||
import 'package:em2rp/views/widgets/event/equipment_conflict_dialog.dart';
|
||||
import 'package:em2rp/services/event_availability_service.dart';
|
||||
|
||||
/// Section pour afficher et gérer le matériel assigné à un événement
|
||||
class EventAssignedEquipmentSection extends StatefulWidget {
|
||||
final List<EventEquipment> assignedEquipment;
|
||||
final List<String> assignedContainers; // IDs des conteneurs
|
||||
final DateTime? startDate;
|
||||
final DateTime? endDate;
|
||||
final Function(List<EventEquipment>, List<String>) onChanged;
|
||||
final String? eventId; // Pour exclure l'événement actuel de la vérification
|
||||
|
||||
const EventAssignedEquipmentSection({
|
||||
super.key,
|
||||
required this.assignedEquipment,
|
||||
required this.assignedContainers,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.onChanged,
|
||||
this.eventId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EventAssignedEquipmentSection> createState() => _EventAssignedEquipmentSectionState();
|
||||
}
|
||||
|
||||
class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSection> {
|
||||
// ...existing code...
|
||||
|
||||
bool get _canAddMaterial => widget.startDate != null && widget.endDate != null;
|
||||
final EventAvailabilityService _availabilityService = EventAvailabilityService();
|
||||
Map<String, EquipmentModel> _equipmentCache = {};
|
||||
Map<String, ContainerModel> _containerCache = {};
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadEquipmentAndContainers();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(EventAssignedEquipmentSection oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// Recharger si les équipements ou conteneurs ont changé
|
||||
if (oldWidget.assignedEquipment != widget.assignedEquipment ||
|
||||
oldWidget.assignedContainers != widget.assignedContainers) {
|
||||
_loadEquipmentAndContainers();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadEquipmentAndContainers() async {
|
||||
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>();
|
||||
|
||||
// Charger depuis les streams
|
||||
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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
_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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
_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 {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openSelectionDialog() async {
|
||||
if (widget.startDate == null || widget.endDate == null) {
|
||||
return; // Ne devrait jamais arriver car le bouton est désactivé
|
||||
}
|
||||
|
||||
final result = await showDialog<Map<String, SelectedItem>>(
|
||||
context: context,
|
||||
builder: (context) => EquipmentSelectionDialog(
|
||||
startDate: widget.startDate!,
|
||||
endDate: widget.endDate!,
|
||||
alreadyAssigned: widget.assignedEquipment,
|
||||
alreadyAssignedContainers: widget.assignedContainers,
|
||||
excludeEventId: widget.eventId,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null && result.isNotEmpty) {
|
||||
await _processSelection(result);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _processSelection(Map<String, SelectedItem> selection) async {
|
||||
// Séparer équipements et conteneurs
|
||||
final newEquipment = <EventEquipment>[];
|
||||
final newContainers = <String>[];
|
||||
|
||||
for (var item in selection.values) {
|
||||
if (item.type == SelectionType.equipment) {
|
||||
newEquipment.add(EventEquipment(
|
||||
equipmentId: item.id,
|
||||
quantity: item.quantity,
|
||||
));
|
||||
} else {
|
||||
newContainers.add(item.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Charger les équipements des conteneurs pour vérifier les conflits
|
||||
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>{};
|
||||
|
||||
// Ajouter les équipements directs
|
||||
for (var eq in newEquipment) {
|
||||
final equipment = allEquipment.firstWhere(
|
||||
(e) => e.id == eq.equipmentId,
|
||||
orElse: () => EquipmentModel(
|
||||
id: eq.equipmentId,
|
||||
name: 'Inconnu',
|
||||
category: EquipmentCategory.other,
|
||||
status: EquipmentStatus.available,
|
||||
parentBoxIds: [],
|
||||
maintenanceIds: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
equipmentNames[eq.equipmentId] = equipment.id;
|
||||
}
|
||||
|
||||
// Ajouter les équipements des conteneurs (par composition)
|
||||
for (var containerId in newContainers) {
|
||||
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(),
|
||||
),
|
||||
);
|
||||
|
||||
// Ajouter tous les équipements enfants pour vérification
|
||||
for (var childEquipmentId in container.equipmentIds) {
|
||||
if (!equipmentIds.contains(childEquipmentId)) {
|
||||
equipmentIds.add(childEquipmentId);
|
||||
|
||||
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(),
|
||||
),
|
||||
);
|
||||
equipmentNames[childEquipmentId] = '${equipment.id} (dans ${container.name})';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Afficher le dialog de conflits
|
||||
final action = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => EquipmentConflictDialog(conflicts: conflicts),
|
||||
);
|
||||
|
||||
if (action == 'cancel') {
|
||||
return; // Annuler l'ajout
|
||||
} else if (action == 'force_removed') {
|
||||
// Identifier quels équipements retirer
|
||||
final removedIds = conflicts.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));
|
||||
|
||||
if (hasConflict) {
|
||||
containersToRemove.add(containerId);
|
||||
}
|
||||
}
|
||||
|
||||
for (var containerId in containersToRemove) {
|
||||
newContainers.remove(containerId);
|
||||
|
||||
// Informer l'utilisateur
|
||||
if (mounted) {
|
||||
final containerName = allContainers.firstWhere((c) => c.id == containerId).name;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('La boîte "$containerName" a été retirée car elle contient du matériel en conflit.'),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: const Duration(seconds: 4),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Si 'force_all', on garde tout
|
||||
}
|
||||
|
||||
// Fusionner avec l'existant
|
||||
final updatedEquipment = [...widget.assignedEquipment];
|
||||
final updatedContainers = [...widget.assignedContainers];
|
||||
|
||||
for (var eq in newEquipment) {
|
||||
if (!updatedEquipment.any((e) => e.equipmentId == eq.equipmentId)) {
|
||||
updatedEquipment.add(eq);
|
||||
}
|
||||
}
|
||||
|
||||
for (var containerId in newContainers) {
|
||||
if (!updatedContainers.contains(containerId)) {
|
||||
updatedContainers.add(containerId);
|
||||
}
|
||||
}
|
||||
|
||||
// Notifier le changement
|
||||
widget.onChanged(updatedEquipment, updatedContainers);
|
||||
|
||||
// Recharger le cache
|
||||
await _loadEquipmentAndContainers();
|
||||
}
|
||||
|
||||
void _removeEquipment(String equipmentId) {
|
||||
final updated = widget.assignedEquipment
|
||||
.where((eq) => eq.equipmentId != equipmentId)
|
||||
.toList();
|
||||
|
||||
widget.onChanged(updated, widget.assignedContainers);
|
||||
setState(() {
|
||||
_equipmentCache.remove(equipmentId);
|
||||
});
|
||||
}
|
||||
|
||||
void _removeContainer(String containerId) {
|
||||
// Récupérer le conteneur pour obtenir la liste de ses enfants
|
||||
final container = _containerCache[containerId];
|
||||
|
||||
// Retirer le conteneur de la liste
|
||||
final updatedContainers = widget.assignedContainers
|
||||
.where((id) => id != containerId)
|
||||
.toList();
|
||||
|
||||
// Retirer les équipements enfants de la liste des équipements assignés
|
||||
final updatedEquipment = widget.assignedEquipment.where((eq) {
|
||||
if (container != null) {
|
||||
// Garder uniquement les équipements qui ne sont PAS dans ce conteneur
|
||||
return !container.equipmentIds.contains(eq.equipmentId);
|
||||
}
|
||||
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);
|
||||
|
||||
setState(() {
|
||||
_containerCache.remove(containerId);
|
||||
// Retirer aussi les équipements enfants du cache
|
||||
if (container != null) {
|
||||
for (var equipmentId in container.equipmentIds) {
|
||||
_equipmentCache.remove(equipmentId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Récupère les équipements qui ne sont PAS dans un conteneur assigné
|
||||
List<EventEquipment> _getStandaloneEquipment() {
|
||||
// Collecter tous les IDs des équipements qui sont dans des conteneurs assignés
|
||||
final Set<String> equipmentIdsInContainers = {};
|
||||
|
||||
for (var containerId in widget.assignedContainers) {
|
||||
final container = _containerCache[containerId];
|
||||
if (container != null) {
|
||||
equipmentIdsInContainers.addAll(container.equipmentIds);
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer les équipements assignés pour garder uniquement ceux qui ne sont pas dans un conteneur
|
||||
return widget.assignedEquipment
|
||||
.where((eq) => !equipmentIdsInContainers.contains(eq.equipmentId))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final totalItems = widget.assignedEquipment.length + widget.assignedContainers.length;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.rouge.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.inventory_2,
|
||||
color: AppColors.rouge,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Matériel assigné',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$totalItems élément(s)',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _canAddMaterial ? _openSelectionDialog : null,
|
||||
icon: Icon(Icons.add, color: _canAddMaterial ? Colors.white : Colors.grey),
|
||||
label: Text(
|
||||
'Ajouter',
|
||||
style: TextStyle(color: _canAddMaterial ? Colors.white : Colors.grey),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _canAddMaterial ? AppColors.rouge : Colors.grey.shade300,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Message si dates non sélectionnées
|
||||
if (!_canAddMaterial)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
'Veuillez sélectionner les dates de début et de fin pour ajouter du matériel',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.orange.shade700,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Contenu
|
||||
if (_isLoading)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else if (totalItems == 0)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 64,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Aucun matériel assigné',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Cliquez sur "Ajouter" pour sélectionner du matériel',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Conteneurs
|
||||
if (widget.assignedContainers.isNotEmpty) ...[
|
||||
Text(
|
||||
'Boîtes (${widget.assignedContainers.length})',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...widget.assignedContainers.map((containerId) {
|
||||
final container = _containerCache[containerId];
|
||||
return _buildContainerItem(container);
|
||||
}).toList(),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Équipements directs (qui ne sont PAS dans un conteneur assigné)
|
||||
if (_getStandaloneEquipment().isNotEmpty) ...[
|
||||
Text(
|
||||
'Équipements (${_getStandaloneEquipment().length})',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
..._getStandaloneEquipment().map((eq) {
|
||||
final equipment = _equipmentCache[eq.equipmentId];
|
||||
return _buildEquipmentItem(equipment, eq);
|
||||
}).toList(),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContainerItem(ContainerModel? container) {
|
||||
if (container == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ExpansionTile(
|
||||
leading: container.type.getIcon(size: 24, color: AppColors.rouge),
|
||||
title: Text(
|
||||
container.id,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text(container.name),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: () => _removeContainer(container.id),
|
||||
),
|
||||
children: [
|
||||
// Afficher les équipements enfants (par composition)
|
||||
Consumer<EquipmentProvider>(
|
||||
builder: (context, provider, child) {
|
||||
return StreamBuilder<List<EquipmentModel>>(
|
||||
stream: provider.equipmentStream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final allEquipment = snapshot.data ?? [];
|
||||
final childEquipments = allEquipment
|
||||
.where((eq) => container.equipmentIds.contains(eq.id))
|
||||
.toList();
|
||||
|
||||
if (childEquipments.isEmpty) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'Aucun équipement dans ce conteneur',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Contenu (${childEquipments.length} équipement(s))',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...childEquipments.map((eq) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
Icon(
|
||||
Icons.subdirectory_arrow_right,
|
||||
size: 16,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
eq.category.getIcon(size: 16, color: eq.category.color),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
eq.id,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEquipmentItem(EquipmentModel? equipment, EventEquipment eventEq) {
|
||||
if (equipment == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final isConsumable = equipment.category == EquipmentCategory.consumable ||
|
||||
equipment.category == EquipmentCategory.cable;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
leading: equipment.category.getIcon(size: 24, color: equipment.category.color),
|
||||
title: Text(
|
||||
equipment.id,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: equipment.brand != null || equipment.model != null
|
||||
? Text('${equipment.brand ?? ''} ${equipment.model ?? ''}'.trim())
|
||||
: null,
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isConsumable && eventEq.quantity > 1)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'Qté: ${eventEq.quantity}',
|
||||
style: TextStyle(
|
||||
color: Colors.blue.shade900,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: () => _removeEquipment(equipment.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user