Files
EM2_ERP/em2rp/lib/views/widgets/event/equipment_conflict_dialog.dart
ElPoyo 08f046c89c 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`).
2025-11-30 20:33:03 +01:00

289 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:em2rp/services/event_availability_service.dart';
import 'package:em2rp/utils/colors.dart';
import 'package:intl/intl.dart';
/// Dialog affichant les conflits de disponibilité du matériel
class EquipmentConflictDialog extends StatefulWidget {
final Map<String, List<AvailabilityConflict>> conflicts;
const EquipmentConflictDialog({
super.key,
required this.conflicts,
});
@override
State<EquipmentConflictDialog> createState() => _EquipmentConflictDialogState();
}
class _EquipmentConflictDialogState extends State<EquipmentConflictDialog> {
final Set<String> _removedEquipmentIds = {};
int get totalConflicts => widget.conflicts.values.fold(0, (sum, list) => sum + list.length);
int get remainingConflicts => widget.conflicts.entries
.where((entry) => !_removedEquipmentIds.contains(entry.key))
.fold(0, (sum, entry) => sum + entry.value.length);
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat('dd/MM/yyyy');
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
constraints: const BoxConstraints(maxWidth: 700, maxHeight: 700),
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(
'Conflits de disponibilité détectés',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'$remainingConflicts conflit(s) sur $totalConflicts équipement(s)',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade700,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop('cancel'),
),
],
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 8),
// Liste des conflits
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: widget.conflicts.length,
itemBuilder: (context, index) {
final entry = widget.conflicts.entries.elementAt(index);
final equipmentId = entry.key;
final conflictsList = entry.value;
final isRemoved = _removedEquipmentIds.contains(equipmentId);
if (conflictsList.isEmpty) return const SizedBox.shrink();
final firstConflict = conflictsList.first;
return Opacity(
opacity: isRemoved ? 0.4 : 1.0,
child: Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: isRemoved ? 0 : 2,
color: isRemoved ? Colors.grey.shade200 : null,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Nom de l'équipement
Row(
children: [
Icon(
Icons.inventory_2,
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,
),
),
),
if (isRemoved)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'RETIRÉ',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
// Liste des événements en conflit
...conflictsList.map((conflict) {
return Padding(
padding: const EdgeInsets.only(bottom: 8, left: 28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.event,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 6),
Expanded(
child: Text(
conflict.conflictingEvent.name,
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey.shade800,
),
),
),
],
),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.only(left: 22),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${dateFormat.format(conflict.conflictingEvent.startDateTime)}${dateFormat.format(conflict.conflictingEvent.endDateTime)}',
style: TextStyle(
fontSize: 13,
color: Colors.grey.shade600,
),
),
Text(
'Chevauchement : ${conflict.overlapDays} jour(s)',
style: TextStyle(
fontSize: 12,
color: Colors.orange.shade700,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
);
}).toList(),
// Boutons d'action par équipement
if (!isRemoved)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
children: [
const Spacer(),
OutlinedButton.icon(
onPressed: () {
setState(() {
_removedEquipmentIds.add(equipmentId);
});
},
icon: const Icon(Icons.remove_circle_outline, size: 16),
label: const Text('Retirer'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
side: const BorderSide(color: Colors.red),
),
),
],
),
),
],
),
),
),
);
},
),
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
// Boutons d'action globaux
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop('cancel'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: const Text('Annuler tout'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: remainingConflicts == 0
? () => Navigator.of(context).pop('force_removed')
: () => Navigator.of(context).pop('force_all'),
style: ElevatedButton.styleFrom(
backgroundColor: remainingConflicts == 0 ? Colors.green : Colors.orange,
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: Text(
remainingConflicts == 0
? 'Valider sans les retirés'
: 'Forcer malgré les conflits',
style: const TextStyle(color: Colors.white),
),
),
),
],
),
],
),
),
);
}
}