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:
ElPoyo
2025-11-30 20:33:03 +01:00
parent e59e3e6316
commit 08f046c89c
31 changed files with 4955 additions and 46 deletions

View File

@@ -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)