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

@@ -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;
}
}

View File

@@ -148,7 +148,10 @@ class EventPreparationService {
}
/// Valider tous les retours
Future<void> validateAllReturn(String eventId) async {
Future<void> validateAllReturn(
String eventId, [
Map<String, int>? returnedQuantities,
]) async {
try {
final event = await _getEvent(eventId);
if (event == null) {
@@ -157,9 +160,12 @@ class EventPreparationService {
// Marquer tous les équipements comme retournés
final updatedEquipment = event.assignedEquipment.map((eq) {
final returnedQty = returnedQuantities?[eq.equipmentId] ??
eq.returnedQuantity ??
eq.quantity;
return eq.copyWith(
isReturned: true,
returnedQuantity: eq.returnedQuantity ?? eq.quantity,
returnedQuantity: returnedQty,
);
}).toList();