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`).
186 lines
6.5 KiB
Dart
186 lines
6.5 KiB
Dart
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;
|
|
}
|
|
}
|
|
|