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:
185
em2rp/lib/services/event_availability_service.dart
Normal file
185
em2rp/lib/services/event_availability_service.dart
Normal file
@@ -0,0 +1,185 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/models/equipment_model.dart';
|
||||
|
||||
/// Informations sur un conflit de disponibilité
|
||||
class AvailabilityConflict {
|
||||
final String equipmentId;
|
||||
final String equipmentName;
|
||||
final EventModel conflictingEvent;
|
||||
final int overlapDays;
|
||||
|
||||
AvailabilityConflict({
|
||||
required this.equipmentId,
|
||||
required this.equipmentName,
|
||||
required this.conflictingEvent,
|
||||
required this.overlapDays,
|
||||
});
|
||||
}
|
||||
|
||||
/// Service pour vérifier la disponibilité du matériel
|
||||
class EventAvailabilityService {
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
|
||||
/// Vérifie si un équipement est disponible pour une plage de dates
|
||||
Future<List<AvailabilityConflict>> checkEquipmentAvailability({
|
||||
required String equipmentId,
|
||||
required String equipmentName,
|
||||
required DateTime startDate,
|
||||
required DateTime endDate,
|
||||
String? excludeEventId, // Pour exclure l'événement en cours d'édition
|
||||
}) async {
|
||||
final conflicts = <AvailabilityConflict>[];
|
||||
|
||||
try {
|
||||
print('[EventAvailabilityService] Checking availability for equipment: $equipmentId');
|
||||
print('[EventAvailabilityService] Date range: $startDate - $endDate');
|
||||
|
||||
// Récupérer TOUS les événements (on filtre côté client car arrayContains avec objet ne marche pas)
|
||||
final eventsSnapshot = await _firestore.collection('events').get();
|
||||
|
||||
print('[EventAvailabilityService] Found ${eventsSnapshot.docs.length} total events');
|
||||
|
||||
for (var doc in eventsSnapshot.docs) {
|
||||
if (excludeEventId != null && doc.id == excludeEventId) {
|
||||
continue; // Ignorer l'événement en cours d'édition
|
||||
}
|
||||
|
||||
try {
|
||||
final event = EventModel.fromMap(doc.data(), doc.id);
|
||||
|
||||
// Vérifier si cet événement contient l'équipement recherché
|
||||
final assignedEquipment = event.assignedEquipment.firstWhere(
|
||||
(eq) => eq.equipmentId == equipmentId,
|
||||
orElse: () => EventEquipment(equipmentId: ''),
|
||||
);
|
||||
|
||||
// Si l'équipement est assigné et non retourné
|
||||
if (assignedEquipment.equipmentId.isNotEmpty && !assignedEquipment.isReturned) {
|
||||
print('[EventAvailabilityService] Equipment $equipmentId found in event: ${event.name}');
|
||||
|
||||
// Vérifier le chevauchement des dates
|
||||
if (_datesOverlap(startDate, endDate, event.startDateTime, event.endDateTime)) {
|
||||
final overlapDays = _calculateOverlapDays(
|
||||
startDate,
|
||||
endDate,
|
||||
event.startDateTime,
|
||||
event.endDateTime,
|
||||
);
|
||||
|
||||
print('[EventAvailabilityService] CONFLICT detected! Overlap: $overlapDays days');
|
||||
|
||||
conflicts.add(AvailabilityConflict(
|
||||
equipmentId: equipmentId,
|
||||
equipmentName: equipmentName,
|
||||
conflictingEvent: event,
|
||||
overlapDays: overlapDays,
|
||||
));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('[EventAvailabilityService] Error processing event ${doc.id}: $e');
|
||||
}
|
||||
}
|
||||
|
||||
print('[EventAvailabilityService] Total conflicts found for $equipmentId: ${conflicts.length}');
|
||||
} catch (e) {
|
||||
print('[EventAvailabilityService] Error checking equipment availability: $e');
|
||||
}
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
/// Vérifie la disponibilité pour une liste d'équipements
|
||||
Future<Map<String, List<AvailabilityConflict>>> checkMultipleEquipmentAvailability({
|
||||
required List<String> equipmentIds,
|
||||
required Map<String, String> equipmentNames,
|
||||
required DateTime startDate,
|
||||
required DateTime endDate,
|
||||
String? excludeEventId,
|
||||
}) async {
|
||||
final allConflicts = <String, List<AvailabilityConflict>>{};
|
||||
|
||||
for (var equipmentId in equipmentIds) {
|
||||
final conflicts = await checkEquipmentAvailability(
|
||||
equipmentId: equipmentId,
|
||||
equipmentName: equipmentNames[equipmentId] ?? equipmentId,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
excludeEventId: excludeEventId,
|
||||
);
|
||||
|
||||
if (conflicts.isNotEmpty) {
|
||||
allConflicts[equipmentId] = conflicts;
|
||||
}
|
||||
}
|
||||
|
||||
return allConflicts;
|
||||
}
|
||||
|
||||
/// Vérifie si deux plages de dates se chevauchent
|
||||
bool _datesOverlap(DateTime start1, DateTime end1, DateTime start2, DateTime end2) {
|
||||
// Deux plages se chevauchent si elles ne sont PAS complètement séparées
|
||||
// Elles sont séparées si : end1 < start2 OU end2 < start1
|
||||
// Donc elles se chevauchent si : NOT (end1 < start2 OU end2 < start1)
|
||||
// Équivalent à : end1 >= start2 ET end2 >= start1
|
||||
return !end1.isBefore(start2) && !end2.isBefore(start1);
|
||||
}
|
||||
|
||||
/// Calcule le nombre de jours de chevauchement
|
||||
int _calculateOverlapDays(DateTime start1, DateTime end1, DateTime start2, DateTime end2) {
|
||||
final overlapStart = start1.isAfter(start2) ? start1 : start2;
|
||||
final overlapEnd = end1.isBefore(end2) ? end1 : end2;
|
||||
|
||||
return overlapEnd.difference(overlapStart).inDays + 1;
|
||||
}
|
||||
|
||||
/// Récupère la quantité disponible pour un consommable/câble
|
||||
Future<int> getAvailableQuantity({
|
||||
required EquipmentModel equipment,
|
||||
required DateTime startDate,
|
||||
required DateTime endDate,
|
||||
String? excludeEventId,
|
||||
}) async {
|
||||
if (!equipment.hasQuantity) {
|
||||
return 1; // Équipement non consommable
|
||||
}
|
||||
|
||||
final totalQuantity = equipment.totalQuantity ?? 0;
|
||||
int reservedQuantity = 0;
|
||||
|
||||
try {
|
||||
// Récupérer tous les événements (on filtre côté client)
|
||||
final eventsSnapshot = await _firestore.collection('events').get();
|
||||
|
||||
for (var doc in eventsSnapshot.docs) {
|
||||
if (excludeEventId != null && doc.id == excludeEventId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
final event = EventModel.fromMap(doc.data(), doc.id);
|
||||
|
||||
// Vérifier le chevauchement des dates
|
||||
if (_datesOverlap(startDate, endDate, event.startDateTime, event.endDateTime)) {
|
||||
final assignedEquipment = event.assignedEquipment.firstWhere(
|
||||
(eq) => eq.equipmentId == equipment.id,
|
||||
orElse: () => EventEquipment(equipmentId: ''),
|
||||
);
|
||||
|
||||
if (assignedEquipment.equipmentId.isNotEmpty && !assignedEquipment.isReturned) {
|
||||
reservedQuantity += assignedEquipment.quantity;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('[EventAvailabilityService] Error processing event ${doc.id} for quantity: $e');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('[EventAvailabilityService] Error getting available quantity: $e');
|
||||
}
|
||||
|
||||
return totalQuantity - reservedQuantity;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user