Files
EM2_ERP/em2rp/lib/services/event_preparation_service.dart
ElPoyo 08f046c89c 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`).
2025-11-30 20:33:03 +01:00

370 lines
12 KiB
Dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/services/equipment_service.dart';
class EventPreparationService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final EquipmentService _equipmentService = EquipmentService();
// Collection references
CollectionReference get _eventsCollection => _firestore.collection('events');
CollectionReference get _equipmentCollection => _firestore.collection('equipment');
// === PRÉPARATION ===
/// Valider un équipement individuel en préparation
Future<void> validateEquipmentPreparation(String eventId, String equipmentId) async {
try {
final event = await _getEvent(eventId);
if (event == null) {
throw Exception('Event not found');
}
// Mettre à jour le statut de l'équipement dans la liste
final updatedEquipment = event.assignedEquipment.map((eq) {
if (eq.equipmentId == equipmentId) {
return eq.copyWith(isPrepared: true);
}
return eq;
}).toList();
await _eventsCollection.doc(eventId).update({
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
});
} catch (e) {
print('Error validating equipment preparation: $e');
rethrow;
}
}
/// Valider tous les équipements en préparation
Future<void> validateAllPreparation(String eventId) async {
try {
final event = await _getEvent(eventId);
if (event == null) {
throw Exception('Event not found');
}
// Marquer tous les équipements comme préparés
final updatedEquipment = event.assignedEquipment.map((eq) {
return eq.copyWith(isPrepared: true);
}).toList();
await _eventsCollection.doc(eventId).update({
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
'preparationStatus': preparationStatusToString(PreparationStatus.completed),
});
// Mettre à jour le statut des équipements à "inUse"
for (var equipment in event.assignedEquipment) {
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse);
}
} catch (e) {
print('Error validating all preparation: $e');
rethrow;
}
}
/// Finaliser la préparation avec des équipements manquants
Future<void> completePreparationWithMissing(
String eventId,
List<String> missingEquipmentIds,
) async {
try {
final event = await _getEvent(eventId);
if (event == null) {
throw Exception('Event not found');
}
// Marquer comme complété avec manquants
await _eventsCollection.doc(eventId).update({
'preparationStatus': preparationStatusToString(PreparationStatus.completedWithMissing),
});
// Mettre à jour le statut des équipements préparés à "inUse"
for (var equipment in event.assignedEquipment) {
if (equipment.isPrepared && !missingEquipmentIds.contains(equipment.equipmentId)) {
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse);
}
}
} catch (e) {
print('Error completing preparation with missing: $e');
rethrow;
}
}
// === RETOUR ===
/// Valider le retour d'un équipement individuel
Future<void> validateEquipmentReturn(
String eventId,
String equipmentId, {
int? returnedQuantity,
}) async {
try {
final event = await _getEvent(eventId);
if (event == null) {
throw Exception('Event not found');
}
// Mettre à jour le statut de l'équipement dans la liste
final updatedEquipment = event.assignedEquipment.map((eq) {
if (eq.equipmentId == equipmentId) {
return eq.copyWith(
isReturned: true,
returnedQuantity: returnedQuantity,
);
}
return eq;
}).toList();
await _eventsCollection.doc(eventId).update({
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
});
// Mettre à jour le stock si c'est un consommable
if (returnedQuantity != null) {
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
if (equipmentDoc.exists) {
final equipment = EquipmentModel.fromMap(
equipmentDoc.data() as Map<String, dynamic>,
equipmentDoc.id,
);
if (equipment.hasQuantity) {
final currentAvailable = equipment.availableQuantity ?? 0;
await _equipmentCollection.doc(equipmentId).update({
'availableQuantity': currentAvailable + returnedQuantity,
});
}
}
}
} catch (e) {
print('Error validating equipment return: $e');
rethrow;
}
}
/// Valider tous les retours
Future<void> validateAllReturn(
String eventId, [
Map<String, int>? returnedQuantities,
]) async {
try {
final event = await _getEvent(eventId);
if (event == null) {
throw Exception('Event not found');
}
// 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: returnedQty,
);
}).toList();
await _eventsCollection.doc(eventId).update({
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
'returnStatus': returnStatusToString(ReturnStatus.completed),
});
// Mettre à jour le statut des équipements à "available" et gérer les stocks
for (var equipment in updatedEquipment) {
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available);
// Restaurer le stock pour les consommables
final equipmentDoc = await _equipmentCollection.doc(equipment.equipmentId).get();
if (equipmentDoc.exists) {
final equipmentData = EquipmentModel.fromMap(
equipmentDoc.data() as Map<String, dynamic>,
equipmentDoc.id,
);
if (equipmentData.hasQuantity && equipment.returnedQuantity != null) {
final currentAvailable = equipmentData.availableQuantity ?? 0;
await _equipmentCollection.doc(equipment.equipmentId).update({
'availableQuantity': currentAvailable + equipment.returnedQuantity!,
});
}
}
}
} catch (e) {
print('Error validating all return: $e');
rethrow;
}
}
/// Finaliser le retour avec des équipements manquants
Future<void> completeReturnWithMissing(
String eventId,
List<String> missingEquipmentIds,
) async {
try {
final event = await _getEvent(eventId);
if (event == null) {
throw Exception('Event not found');
}
// Marquer comme complété avec manquants
await _eventsCollection.doc(eventId).update({
'returnStatus': returnStatusToString(ReturnStatus.completedWithMissing),
});
// Mettre à jour le statut des équipements retournés à "available"
for (var equipment in event.assignedEquipment) {
if (equipment.isReturned && !missingEquipmentIds.contains(equipment.equipmentId)) {
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available);
// Restaurer le stock pour les consommables
final equipmentDoc = await _equipmentCollection.doc(equipment.equipmentId).get();
if (equipmentDoc.exists) {
final equipmentData = EquipmentModel.fromMap(
equipmentDoc.data() as Map<String, dynamic>,
equipmentDoc.id,
);
if (equipmentData.hasQuantity && equipment.returnedQuantity != null) {
final currentAvailable = equipmentData.availableQuantity ?? 0;
await _equipmentCollection.doc(equipment.equipmentId).update({
'availableQuantity': currentAvailable + equipment.returnedQuantity!,
});
}
}
} else if (missingEquipmentIds.contains(equipment.equipmentId)) {
// Marquer comme perdu
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.lost);
}
}
} catch (e) {
print('Error completing return with missing: $e');
rethrow;
}
}
// === HELPERS ===
/// Mettre à jour le statut d'un équipement
Future<void> updateEquipmentStatus(String equipmentId, EquipmentStatus status) async {
try {
await _equipmentCollection.doc(equipmentId).update({
'status': equipmentStatusToString(status),
'updatedAt': Timestamp.fromDate(DateTime.now()),
});
} catch (e) {
print('Error updating equipment status: $e');
rethrow;
}
}
/// Récupérer un événement
Future<EventModel?> _getEvent(String eventId) async {
try {
final doc = await _eventsCollection.doc(eventId).get();
if (doc.exists) {
return EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
}
return null;
} catch (e) {
print('Error getting event: $e');
rethrow;
}
}
/// Ajouter un équipement à un événement
Future<void> addEquipmentToEvent(
String eventId,
String equipmentId, {
int quantity = 1,
}) async {
try {
final event = await _getEvent(eventId);
if (event == null) {
throw Exception('Event not found');
}
// Vérifier que l'équipement n'est pas déjà ajouté
final alreadyAdded = event.assignedEquipment.any((eq) => eq.equipmentId == equipmentId);
if (alreadyAdded) {
throw Exception('Equipment already added to event');
}
final newEquipment = EventEquipment(
equipmentId: equipmentId,
quantity: quantity,
);
final updatedEquipment = [...event.assignedEquipment, newEquipment];
await _eventsCollection.doc(eventId).update({
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
});
// Décrémenter le stock pour les consommables
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
if (equipmentDoc.exists) {
final equipmentData = EquipmentModel.fromMap(
equipmentDoc.data() as Map<String, dynamic>,
equipmentDoc.id,
);
if (equipmentData.hasQuantity) {
final currentAvailable = equipmentData.availableQuantity ?? 0;
await _equipmentCollection.doc(equipmentId).update({
'availableQuantity': currentAvailable - quantity,
});
}
}
} catch (e) {
print('Error adding equipment to event: $e');
rethrow;
}
}
/// Retirer un équipement d'un événement
Future<void> removeEquipmentFromEvent(String eventId, String equipmentId) async {
try {
final event = await _getEvent(eventId);
if (event == null) {
throw Exception('Event not found');
}
final equipmentToRemove = event.assignedEquipment.firstWhere(
(eq) => eq.equipmentId == equipmentId,
);
final updatedEquipment = event.assignedEquipment
.where((eq) => eq.equipmentId != equipmentId)
.toList();
await _eventsCollection.doc(eventId).update({
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
});
// Restaurer le stock pour les consommables
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
if (equipmentDoc.exists) {
final equipmentData = EquipmentModel.fromMap(
equipmentDoc.data() as Map<String, dynamic>,
equipmentDoc.id,
);
if (equipmentData.hasQuantity) {
final currentAvailable = equipmentData.availableQuantity ?? 0;
await _equipmentCollection.doc(equipmentId).update({
'availableQuantity': currentAvailable + equipmentToRemove.quantity,
});
}
}
} catch (e) {
print('Error removing equipment from event: $e');
rethrow;
}
}
}