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:
@@ -53,12 +53,25 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
// Méthode publique pour mettre à jour les options depuis l'extérieur
|
||||
void _updateOptions(List<EventOption> options) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_allOptions = options;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _showOptionPicker() async {
|
||||
final selected = await showDialog<Map<String, dynamic>>(
|
||||
context: context,
|
||||
builder: (ctx) => _OptionPickerDialog(
|
||||
allOptions: _allOptions,
|
||||
eventType: widget.eventType, // Ajout du paramètre manquant
|
||||
eventType: widget.eventType,
|
||||
onOptionsUpdated: (updatedOptions) {
|
||||
// Callback pour mettre à jour les options après création
|
||||
_updateOptions(updatedOptions);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (selected != null) {
|
||||
@@ -242,10 +255,12 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
||||
class _OptionPickerDialog extends StatefulWidget {
|
||||
final List<EventOption> allOptions;
|
||||
final String? eventType;
|
||||
final Function(List<EventOption>)? onOptionsUpdated;
|
||||
|
||||
const _OptionPickerDialog({
|
||||
required this.allOptions,
|
||||
this.eventType,
|
||||
this.onOptionsUpdated,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -256,15 +271,36 @@ class _OptionPickerDialog extends StatefulWidget {
|
||||
class _OptionPickerDialogState extends State<_OptionPickerDialog> {
|
||||
String _search = '';
|
||||
bool _creating = false;
|
||||
late List<EventOption> _currentOptions;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentOptions = widget.allOptions;
|
||||
}
|
||||
|
||||
Future<void> _reloadOptions() async {
|
||||
final snapshot = await FirebaseFirestore.instance.collection('options').get();
|
||||
final updatedOptions = snapshot.docs
|
||||
.map((doc) => EventOption.fromMap(doc.data(), doc.id))
|
||||
.toList();
|
||||
|
||||
setState(() {
|
||||
_currentOptions = updatedOptions;
|
||||
});
|
||||
|
||||
// Appeler le callback pour mettre à jour aussi le parent
|
||||
widget.onOptionsUpdated?.call(updatedOptions);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Debug: Afficher les informations de filtrage
|
||||
print('=== DEBUG OptionPickerDialog ===');
|
||||
print('widget.eventType: ${widget.eventType}');
|
||||
print('widget.allOptions.length: ${widget.allOptions.length}');
|
||||
print('_currentOptions.length: ${_currentOptions.length}');
|
||||
|
||||
final filtered = widget.allOptions.where((opt) {
|
||||
final filtered = _currentOptions.where((opt) {
|
||||
print('Option: ${opt.name}');
|
||||
print(' opt.eventTypes: ${opt.eventTypes}');
|
||||
print(' widget.eventType: ${widget.eventType}');
|
||||
@@ -381,8 +417,9 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> {
|
||||
builder: (ctx) => _CreateOptionDialog(),
|
||||
);
|
||||
setState(() => _creating = false);
|
||||
if (created == true) {
|
||||
Navigator.pop(context);
|
||||
if (created == true && mounted) {
|
||||
// Recharger les options depuis Firestore
|
||||
await _reloadOptions();
|
||||
}
|
||||
},
|
||||
child: _creating
|
||||
@@ -473,7 +510,7 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return 'Champ requis';
|
||||
if (v.length > 16) return 'Maximum 16 caractères';
|
||||
if (!RegExp(r'^[A-Z0-9_-]+$').hasMatch(v)) {
|
||||
if (!RegExp(r'^[A-Za-z0-9_-]+$').hasMatch(v)) {
|
||||
return 'Seuls les lettres, chiffres, _ et - sont autorisés';
|
||||
}
|
||||
return null;
|
||||
@@ -526,24 +563,29 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Types d\'événement associés :'),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: _allEventTypes
|
||||
.map((type) => FilterChip(
|
||||
label: Text(type['name']),
|
||||
selected: _selectedTypes.contains(type['id']),
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_selectedTypes.add(type['id']);
|
||||
} else {
|
||||
_selectedTypes.remove(type['id']);
|
||||
}
|
||||
});
|
||||
},
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
_loading
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
: Wrap(
|
||||
spacing: 8,
|
||||
children: _allEventTypes
|
||||
.map((type) => FilterChip(
|
||||
label: Text(type['name']),
|
||||
selected: _selectedTypes.contains(type['id']),
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_selectedTypes.add(type['id']);
|
||||
} else {
|
||||
_selectedTypes.remove(type['id']);
|
||||
}
|
||||
});
|
||||
},
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_error != null)
|
||||
|
||||
Reference in New Issue
Block a user