feat: ajout de la gestion de la préparation d'un événement avec page permettant de le gérer
This commit is contained in:
@@ -41,6 +41,12 @@
|
|||||||
"firebase.json",
|
"firebase.json",
|
||||||
"**/.*",
|
"**/.*",
|
||||||
"**/node_modules/**"
|
"**/node_modules/**"
|
||||||
|
],
|
||||||
|
"rewrites": [
|
||||||
|
{
|
||||||
|
"source": "**",
|
||||||
|
"destination": "/index.html"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/// Configuration de la version de l'application
|
/// Configuration de la version de l'application
|
||||||
class AppVersion {
|
class AppVersion {
|
||||||
static const String version = '0.3.5';
|
static const String version = '0.3.7';
|
||||||
|
|
||||||
/// Retourne la version complète de l'application
|
/// Retourne la version complète de l'application
|
||||||
static String get fullVersion => 'v$version';
|
static String get fullVersion => 'v$version';
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:em2rp/views/container_form_page.dart';
|
|||||||
import 'package:em2rp/views/container_detail_page.dart';
|
import 'package:em2rp/views/container_detail_page.dart';
|
||||||
import 'package:em2rp/views/event_preparation_page.dart';
|
import 'package:em2rp/views/event_preparation_page.dart';
|
||||||
import 'package:em2rp/models/container_model.dart';
|
import 'package:em2rp/models/container_model.dart';
|
||||||
|
import 'package:em2rp/models/event_model.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
@@ -152,9 +153,12 @@ class MyApp extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
'/event_preparation': (context) {
|
'/event_preparation': (context) {
|
||||||
final eventId = ModalRoute.of(context)!.settings.arguments as String;
|
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
|
||||||
|
final event = args['event'] as EventModel;
|
||||||
return AuthGuard(
|
return AuthGuard(
|
||||||
child: EventPreparationPage(eventId: eventId),
|
child: EventPreparationPage(
|
||||||
|
initialEvent: event,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ String eventStatusToString(EventStatus status) {
|
|||||||
case EventStatus.canceled:
|
case EventStatus.canceled:
|
||||||
return 'CANCELED';
|
return 'CANCELED';
|
||||||
case EventStatus.waitingForApproval:
|
case EventStatus.waitingForApproval:
|
||||||
default:
|
return 'WAITING_FOR_APPROVAL';
|
||||||
return 'WAITING_FOR_APPROVAL';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +64,78 @@ PreparationStatus preparationStatusFromString(String? status) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Statut de chargement (loading)
|
||||||
|
enum LoadingStatus {
|
||||||
|
notStarted,
|
||||||
|
inProgress,
|
||||||
|
completed,
|
||||||
|
completedWithMissing
|
||||||
|
}
|
||||||
|
|
||||||
|
String loadingStatusToString(LoadingStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
case LoadingStatus.notStarted:
|
||||||
|
return 'NOT_STARTED';
|
||||||
|
case LoadingStatus.inProgress:
|
||||||
|
return 'IN_PROGRESS';
|
||||||
|
case LoadingStatus.completed:
|
||||||
|
return 'COMPLETED';
|
||||||
|
case LoadingStatus.completedWithMissing:
|
||||||
|
return 'COMPLETED_WITH_MISSING';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingStatus loadingStatusFromString(String? status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'NOT_STARTED':
|
||||||
|
return LoadingStatus.notStarted;
|
||||||
|
case 'IN_PROGRESS':
|
||||||
|
return LoadingStatus.inProgress;
|
||||||
|
case 'COMPLETED':
|
||||||
|
return LoadingStatus.completed;
|
||||||
|
case 'COMPLETED_WITH_MISSING':
|
||||||
|
return LoadingStatus.completedWithMissing;
|
||||||
|
default:
|
||||||
|
return LoadingStatus.notStarted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statut de déchargement (unloading)
|
||||||
|
enum UnloadingStatus {
|
||||||
|
notStarted,
|
||||||
|
inProgress,
|
||||||
|
completed,
|
||||||
|
completedWithMissing
|
||||||
|
}
|
||||||
|
|
||||||
|
String unloadingStatusToString(UnloadingStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
case UnloadingStatus.notStarted:
|
||||||
|
return 'NOT_STARTED';
|
||||||
|
case UnloadingStatus.inProgress:
|
||||||
|
return 'IN_PROGRESS';
|
||||||
|
case UnloadingStatus.completed:
|
||||||
|
return 'COMPLETED';
|
||||||
|
case UnloadingStatus.completedWithMissing:
|
||||||
|
return 'COMPLETED_WITH_MISSING';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UnloadingStatus unloadingStatusFromString(String? status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'NOT_STARTED':
|
||||||
|
return UnloadingStatus.notStarted;
|
||||||
|
case 'IN_PROGRESS':
|
||||||
|
return UnloadingStatus.inProgress;
|
||||||
|
case 'COMPLETED':
|
||||||
|
return UnloadingStatus.completed;
|
||||||
|
case 'COMPLETED_WITH_MISSING':
|
||||||
|
return UnloadingStatus.completedWithMissing;
|
||||||
|
default:
|
||||||
|
return UnloadingStatus.notStarted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum ReturnStatus {
|
enum ReturnStatus {
|
||||||
notStarted,
|
notStarted,
|
||||||
inProgress,
|
inProgress,
|
||||||
@@ -104,6 +175,8 @@ class EventEquipment {
|
|||||||
final String equipmentId; // ID de l'équipement
|
final String equipmentId; // ID de l'équipement
|
||||||
final int quantity; // Quantité (pour consommables)
|
final int quantity; // Quantité (pour consommables)
|
||||||
final bool isPrepared; // Validé en préparation
|
final bool isPrepared; // Validé en préparation
|
||||||
|
final bool isLoaded; // Validé au chargement
|
||||||
|
final bool isUnloaded; // Validé au déchargement
|
||||||
final bool isReturned; // Validé au retour
|
final bool isReturned; // Validé au retour
|
||||||
final int? returnedQuantity; // Quantité retournée (pour consommables)
|
final int? returnedQuantity; // Quantité retournée (pour consommables)
|
||||||
|
|
||||||
@@ -111,6 +184,8 @@ class EventEquipment {
|
|||||||
required this.equipmentId,
|
required this.equipmentId,
|
||||||
this.quantity = 1,
|
this.quantity = 1,
|
||||||
this.isPrepared = false,
|
this.isPrepared = false,
|
||||||
|
this.isLoaded = false,
|
||||||
|
this.isUnloaded = false,
|
||||||
this.isReturned = false,
|
this.isReturned = false,
|
||||||
this.returnedQuantity,
|
this.returnedQuantity,
|
||||||
});
|
});
|
||||||
@@ -120,6 +195,8 @@ class EventEquipment {
|
|||||||
equipmentId: map['equipmentId'] ?? '',
|
equipmentId: map['equipmentId'] ?? '',
|
||||||
quantity: map['quantity'] ?? 1,
|
quantity: map['quantity'] ?? 1,
|
||||||
isPrepared: map['isPrepared'] ?? false,
|
isPrepared: map['isPrepared'] ?? false,
|
||||||
|
isLoaded: map['isLoaded'] ?? false,
|
||||||
|
isUnloaded: map['isUnloaded'] ?? false,
|
||||||
isReturned: map['isReturned'] ?? false,
|
isReturned: map['isReturned'] ?? false,
|
||||||
returnedQuantity: map['returnedQuantity'],
|
returnedQuantity: map['returnedQuantity'],
|
||||||
);
|
);
|
||||||
@@ -130,6 +207,8 @@ class EventEquipment {
|
|||||||
'equipmentId': equipmentId,
|
'equipmentId': equipmentId,
|
||||||
'quantity': quantity,
|
'quantity': quantity,
|
||||||
'isPrepared': isPrepared,
|
'isPrepared': isPrepared,
|
||||||
|
'isLoaded': isLoaded,
|
||||||
|
'isUnloaded': isUnloaded,
|
||||||
'isReturned': isReturned,
|
'isReturned': isReturned,
|
||||||
'returnedQuantity': returnedQuantity,
|
'returnedQuantity': returnedQuantity,
|
||||||
};
|
};
|
||||||
@@ -139,6 +218,8 @@ class EventEquipment {
|
|||||||
String? equipmentId,
|
String? equipmentId,
|
||||||
int? quantity,
|
int? quantity,
|
||||||
bool? isPrepared,
|
bool? isPrepared,
|
||||||
|
bool? isLoaded,
|
||||||
|
bool? isUnloaded,
|
||||||
bool? isReturned,
|
bool? isReturned,
|
||||||
int? returnedQuantity,
|
int? returnedQuantity,
|
||||||
}) {
|
}) {
|
||||||
@@ -146,6 +227,8 @@ class EventEquipment {
|
|||||||
equipmentId: equipmentId ?? this.equipmentId,
|
equipmentId: equipmentId ?? this.equipmentId,
|
||||||
quantity: quantity ?? this.quantity,
|
quantity: quantity ?? this.quantity,
|
||||||
isPrepared: isPrepared ?? this.isPrepared,
|
isPrepared: isPrepared ?? this.isPrepared,
|
||||||
|
isLoaded: isLoaded ?? this.isLoaded,
|
||||||
|
isUnloaded: isUnloaded ?? this.isUnloaded,
|
||||||
isReturned: isReturned ?? this.isReturned,
|
isReturned: isReturned ?? this.isReturned,
|
||||||
returnedQuantity: returnedQuantity ?? this.returnedQuantity,
|
returnedQuantity: returnedQuantity ?? this.returnedQuantity,
|
||||||
);
|
);
|
||||||
@@ -181,6 +264,8 @@ class EventModel {
|
|||||||
final List<EventEquipment> assignedEquipment;
|
final List<EventEquipment> assignedEquipment;
|
||||||
final List<String> assignedContainers; // IDs des conteneurs assignés
|
final List<String> assignedContainers; // IDs des conteneurs assignés
|
||||||
final PreparationStatus? preparationStatus;
|
final PreparationStatus? preparationStatus;
|
||||||
|
final LoadingStatus? loadingStatus;
|
||||||
|
final UnloadingStatus? unloadingStatus;
|
||||||
final ReturnStatus? returnStatus;
|
final ReturnStatus? returnStatus;
|
||||||
|
|
||||||
EventModel({
|
EventModel({
|
||||||
@@ -208,6 +293,8 @@ class EventModel {
|
|||||||
this.assignedEquipment = const [],
|
this.assignedEquipment = const [],
|
||||||
this.assignedContainers = const [],
|
this.assignedContainers = const [],
|
||||||
this.preparationStatus,
|
this.preparationStatus,
|
||||||
|
this.loadingStatus,
|
||||||
|
this.unloadingStatus,
|
||||||
this.returnStatus,
|
this.returnStatus,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -342,6 +429,8 @@ class EventModel {
|
|||||||
contactPhone: map['contactPhone']?.toString(),
|
contactPhone: map['contactPhone']?.toString(),
|
||||||
assignedEquipment: assignedEquipment,
|
assignedEquipment: assignedEquipment,
|
||||||
preparationStatus: preparationStatusFromString(map['preparationStatus'] as String?),
|
preparationStatus: preparationStatusFromString(map['preparationStatus'] as String?),
|
||||||
|
loadingStatus: loadingStatusFromString(map['loadingStatus'] as String?),
|
||||||
|
unloadingStatus: unloadingStatusFromString(map['unloadingStatus'] as String?),
|
||||||
returnStatus: returnStatusFromString(map['returnStatus'] as String?),
|
returnStatus: returnStatusFromString(map['returnStatus'] as String?),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -401,6 +490,8 @@ class EventModel {
|
|||||||
'assignedEquipment': assignedEquipment.map((e) => e.toMap()).toList(),
|
'assignedEquipment': assignedEquipment.map((e) => e.toMap()).toList(),
|
||||||
'assignedContainers': assignedContainers,
|
'assignedContainers': assignedContainers,
|
||||||
'preparationStatus': preparationStatus != null ? preparationStatusToString(preparationStatus!) : null,
|
'preparationStatus': preparationStatus != null ? preparationStatusToString(preparationStatus!) : null,
|
||||||
|
'loadingStatus': loadingStatus != null ? loadingStatusToString(loadingStatus!) : null,
|
||||||
|
'unloadingStatus': unloadingStatus != null ? unloadingStatusToString(unloadingStatus!) : null,
|
||||||
'returnStatus': returnStatus != null ? returnStatusToString(returnStatus!) : null,
|
'returnStatus': returnStatus != null ? returnStatusToString(returnStatus!) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:em2rp/models/event_model.dart';
|
import 'package:em2rp/models/event_model.dart';
|
||||||
import 'package:em2rp/models/equipment_model.dart';
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
|
import 'package:em2rp/models/container_model.dart';
|
||||||
|
|
||||||
|
/// Type de conflit
|
||||||
|
enum ConflictType {
|
||||||
|
equipmentUnavailable, // Équipement non quantifiable utilisé
|
||||||
|
insufficientQuantity, // Quantité insuffisante pour consommable/câble
|
||||||
|
containerFullyUsed, // Boîte complète utilisée
|
||||||
|
containerPartiallyUsed, // Certains équipements de la boîte utilisés
|
||||||
|
}
|
||||||
|
|
||||||
/// Informations sur un conflit de disponibilité
|
/// Informations sur un conflit de disponibilité
|
||||||
class AvailabilityConflict {
|
class AvailabilityConflict {
|
||||||
@@ -8,13 +17,48 @@ class AvailabilityConflict {
|
|||||||
final String equipmentName;
|
final String equipmentName;
|
||||||
final EventModel conflictingEvent;
|
final EventModel conflictingEvent;
|
||||||
final int overlapDays;
|
final int overlapDays;
|
||||||
|
final ConflictType type;
|
||||||
|
|
||||||
|
// Pour les quantités (consommables/câbles)
|
||||||
|
final int? totalQuantity;
|
||||||
|
final int? availableQuantity;
|
||||||
|
final int? requestedQuantity;
|
||||||
|
final int? reservedQuantity;
|
||||||
|
|
||||||
|
// Pour les boîtes
|
||||||
|
final String? containerId;
|
||||||
|
final String? containerName;
|
||||||
|
final List<String>? conflictingChildrenIds;
|
||||||
|
|
||||||
AvailabilityConflict({
|
AvailabilityConflict({
|
||||||
required this.equipmentId,
|
required this.equipmentId,
|
||||||
required this.equipmentName,
|
required this.equipmentName,
|
||||||
required this.conflictingEvent,
|
required this.conflictingEvent,
|
||||||
required this.overlapDays,
|
required this.overlapDays,
|
||||||
|
this.type = ConflictType.equipmentUnavailable,
|
||||||
|
this.totalQuantity,
|
||||||
|
this.availableQuantity,
|
||||||
|
this.requestedQuantity,
|
||||||
|
this.reservedQuantity,
|
||||||
|
this.containerId,
|
||||||
|
this.containerName,
|
||||||
|
this.conflictingChildrenIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Message descriptif du conflit
|
||||||
|
String get conflictMessage {
|
||||||
|
switch (type) {
|
||||||
|
case ConflictType.equipmentUnavailable:
|
||||||
|
return 'Équipement déjà utilisé';
|
||||||
|
case ConflictType.insufficientQuantity:
|
||||||
|
return 'Stock insuffisant : $availableQuantity/$totalQuantity disponible (demandé: $requestedQuantity)';
|
||||||
|
case ConflictType.containerFullyUsed:
|
||||||
|
return 'Boîte complète déjà utilisée';
|
||||||
|
case ConflictType.containerPartiallyUsed:
|
||||||
|
final count = conflictingChildrenIds?.length ?? 0;
|
||||||
|
return '$count équipement(s) de la boîte déjà utilisé(s)';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Service pour vérifier la disponibilité du matériel
|
/// Service pour vérifier la disponibilité du matériel
|
||||||
@@ -32,21 +76,22 @@ class EventAvailabilityService {
|
|||||||
final conflicts = <AvailabilityConflict>[];
|
final conflicts = <AvailabilityConflict>[];
|
||||||
|
|
||||||
try {
|
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)
|
// 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();
|
final eventsSnapshot = await _firestore.collection('events').get();
|
||||||
|
|
||||||
print('[EventAvailabilityService] Found ${eventsSnapshot.docs.length} total events');
|
|
||||||
|
|
||||||
for (var doc in eventsSnapshot.docs) {
|
for (var doc in eventsSnapshot.docs) {
|
||||||
if (excludeEventId != null && doc.id == excludeEventId) {
|
if (excludeEventId != null && doc.id == excludeEventId) {
|
||||||
continue; // Ignorer l'événement en cours d'édition
|
continue; // Ignorer l'événement en cours d'édition
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final event = EventModel.fromMap(doc.data(), doc.id);
|
final data = doc.data();
|
||||||
|
final event = EventModel.fromMap(data, doc.id);
|
||||||
|
|
||||||
|
// Ignorer les événements annulés
|
||||||
|
if (event.status == EventStatus.canceled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Vérifier si cet événement contient l'équipement recherché
|
// Vérifier si cet événement contient l'équipement recherché
|
||||||
final assignedEquipment = event.assignedEquipment.firstWhere(
|
final assignedEquipment = event.assignedEquipment.firstWhere(
|
||||||
@@ -54,21 +99,26 @@ class EventAvailabilityService {
|
|||||||
orElse: () => EventEquipment(equipmentId: ''),
|
orElse: () => EventEquipment(equipmentId: ''),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Si l'équipement est assigné et non retourné
|
// Si l'équipement est assigné à cet événement, il est indisponible
|
||||||
if (assignedEquipment.equipmentId.isNotEmpty && !assignedEquipment.isReturned) {
|
// (peu importe le statut de préparation/chargement/retour)
|
||||||
print('[EventAvailabilityService] Equipment $equipmentId found in event: ${event.name}');
|
if (assignedEquipment.equipmentId.isNotEmpty) {
|
||||||
|
// Calculer les dates réelles avec temps d'installation et démontage
|
||||||
|
final eventRealStartDate = event.startDateTime.subtract(
|
||||||
|
Duration(hours: event.installationTime),
|
||||||
|
);
|
||||||
|
final eventRealEndDate = event.endDateTime.add(
|
||||||
|
Duration(hours: event.disassemblyTime),
|
||||||
|
);
|
||||||
|
|
||||||
// Vérifier le chevauchement des dates
|
// Vérifier le chevauchement des dates
|
||||||
if (_datesOverlap(startDate, endDate, event.startDateTime, event.endDateTime)) {
|
if (_datesOverlap(startDate, endDate, eventRealStartDate, eventRealEndDate)) {
|
||||||
final overlapDays = _calculateOverlapDays(
|
final overlapDays = _calculateOverlapDays(
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
event.startDateTime,
|
eventRealStartDate,
|
||||||
event.endDateTime,
|
eventRealEndDate,
|
||||||
);
|
);
|
||||||
|
|
||||||
print('[EventAvailabilityService] CONFLICT detected! Overlap: $overlapDays days');
|
|
||||||
|
|
||||||
conflicts.add(AvailabilityConflict(
|
conflicts.add(AvailabilityConflict(
|
||||||
equipmentId: equipmentId,
|
equipmentId: equipmentId,
|
||||||
equipmentName: equipmentName,
|
equipmentName: equipmentName,
|
||||||
@@ -81,15 +131,18 @@ class EventAvailabilityService {
|
|||||||
print('[EventAvailabilityService] Error processing event ${doc.id}: $e');
|
print('[EventAvailabilityService] Error processing event ${doc.id}: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print('[EventAvailabilityService] Total conflicts found for $equipmentId: ${conflicts.length}');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[EventAvailabilityService] Error checking equipment availability: $e');
|
print('[EventAvailabilityService] Error checking availability: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
return conflicts;
|
return conflicts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper pour formater les dates dans les logs
|
||||||
|
String _formatDate(DateTime date) {
|
||||||
|
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
/// Vérifie la disponibilité pour une liste d'équipements
|
/// Vérifie la disponibilité pour une liste d'équipements
|
||||||
Future<Map<String, List<AvailabilityConflict>>> checkMultipleEquipmentAvailability({
|
Future<Map<String, List<AvailabilityConflict>>> checkMultipleEquipmentAvailability({
|
||||||
required List<String> equipmentIds,
|
required List<String> equipmentIds,
|
||||||
@@ -114,6 +167,7 @@ class EventAvailabilityService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return allConflicts;
|
return allConflicts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,14 +214,29 @@ class EventAvailabilityService {
|
|||||||
try {
|
try {
|
||||||
final event = EventModel.fromMap(doc.data(), doc.id);
|
final event = EventModel.fromMap(doc.data(), doc.id);
|
||||||
|
|
||||||
|
// Ignorer les événements annulés
|
||||||
|
if (event.status == EventStatus.canceled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculer les dates réelles avec temps d'installation et démontage
|
||||||
|
final eventRealStartDate = event.startDateTime.subtract(
|
||||||
|
Duration(hours: event.installationTime),
|
||||||
|
);
|
||||||
|
final eventRealEndDate = event.endDateTime.add(
|
||||||
|
Duration(hours: event.disassemblyTime),
|
||||||
|
);
|
||||||
|
|
||||||
// Vérifier le chevauchement des dates
|
// Vérifier le chevauchement des dates
|
||||||
if (_datesOverlap(startDate, endDate, event.startDateTime, event.endDateTime)) {
|
if (_datesOverlap(startDate, endDate, eventRealStartDate, eventRealEndDate)) {
|
||||||
final assignedEquipment = event.assignedEquipment.firstWhere(
|
final assignedEquipment = event.assignedEquipment.firstWhere(
|
||||||
(eq) => eq.equipmentId == equipment.id,
|
(eq) => eq.equipmentId == equipment.id,
|
||||||
orElse: () => EventEquipment(equipmentId: ''),
|
orElse: () => EventEquipment(equipmentId: ''),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (assignedEquipment.equipmentId.isNotEmpty && !assignedEquipment.isReturned) {
|
// Si l'équipement est assigné, réserver la quantité
|
||||||
|
// (peu importe le statut de préparation/retour)
|
||||||
|
if (assignedEquipment.equipmentId.isNotEmpty) {
|
||||||
reservedQuantity += assignedEquipment.quantity;
|
reservedQuantity += assignedEquipment.quantity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,5 +250,179 @@ class EventAvailabilityService {
|
|||||||
|
|
||||||
return totalQuantity - reservedQuantity;
|
return totalQuantity - reservedQuantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Vérifie la disponibilité d'un équipement avec gestion des quantités
|
||||||
|
Future<List<AvailabilityConflict>> checkEquipmentAvailabilityWithQuantity({
|
||||||
|
required EquipmentModel equipment,
|
||||||
|
required int requestedQuantity,
|
||||||
|
required DateTime startDate,
|
||||||
|
required DateTime endDate,
|
||||||
|
String? excludeEventId,
|
||||||
|
}) async {
|
||||||
|
final conflicts = <AvailabilityConflict>[];
|
||||||
|
|
||||||
|
// Si équipement quantifiable (consommable/câble)
|
||||||
|
if (equipment.hasQuantity) {
|
||||||
|
final totalQuantity = equipment.totalQuantity ?? 0;
|
||||||
|
final availableQty = await getAvailableQuantity(
|
||||||
|
equipment: equipment,
|
||||||
|
startDate: startDate,
|
||||||
|
endDate: endDate,
|
||||||
|
excludeEventId: excludeEventId,
|
||||||
|
);
|
||||||
|
final reservedQty = totalQuantity - availableQty;
|
||||||
|
|
||||||
|
// ✅ Ne créer un conflit que si la quantité est VRAIMENT insuffisante
|
||||||
|
if (availableQty < requestedQuantity) {
|
||||||
|
// Trouver les événements qui réservent cette quantité
|
||||||
|
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);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
conflicts.add(AvailabilityConflict(
|
||||||
|
equipmentId: equipment.id,
|
||||||
|
equipmentName: equipment.name,
|
||||||
|
conflictingEvent: event,
|
||||||
|
overlapDays: _calculateOverlapDays(startDate, endDate, event.startDateTime, event.endDateTime),
|
||||||
|
type: ConflictType.insufficientQuantity,
|
||||||
|
totalQuantity: totalQuantity,
|
||||||
|
availableQuantity: availableQty,
|
||||||
|
requestedQuantity: requestedQuantity,
|
||||||
|
reservedQuantity: reservedQty,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('[EventAvailabilityService] Error processing event ${doc.id}: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Équipement non quantifiable : vérification classique
|
||||||
|
return await checkEquipmentAvailability(
|
||||||
|
equipmentId: equipment.id,
|
||||||
|
equipmentName: equipment.name,
|
||||||
|
startDate: startDate,
|
||||||
|
endDate: endDate,
|
||||||
|
excludeEventId: excludeEventId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return conflicts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifie la disponibilité d'une boîte et de son contenu
|
||||||
|
Future<List<AvailabilityConflict>> checkContainerAvailability({
|
||||||
|
required ContainerModel container,
|
||||||
|
required List<EquipmentModel> containerEquipment,
|
||||||
|
required DateTime startDate,
|
||||||
|
required DateTime endDate,
|
||||||
|
String? excludeEventId,
|
||||||
|
}) async {
|
||||||
|
final conflicts = <AvailabilityConflict>[];
|
||||||
|
final conflictingChildrenIds = <String>[];
|
||||||
|
|
||||||
|
// Vérifier d'abord si la boîte complète est utilisée
|
||||||
|
final eventsSnapshot = await _firestore.collection('events').get();
|
||||||
|
bool isContainerFullyUsed = false;
|
||||||
|
EventModel? containerConflictingEvent;
|
||||||
|
|
||||||
|
for (var doc in eventsSnapshot.docs) {
|
||||||
|
if (excludeEventId != null && doc.id == excludeEventId) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final event = EventModel.fromMap(doc.data(), doc.id);
|
||||||
|
|
||||||
|
// Ignorer les événements annulés
|
||||||
|
if (event.status == EventStatus.canceled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculer les dates réelles avec temps d'installation et démontage
|
||||||
|
final eventRealStartDate = event.startDateTime.subtract(
|
||||||
|
Duration(hours: event.installationTime),
|
||||||
|
);
|
||||||
|
final eventRealEndDate = event.endDateTime.add(
|
||||||
|
Duration(hours: event.disassemblyTime),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Vérifier si cette boîte est assignée
|
||||||
|
if (event.assignedContainers.contains(container.id)) {
|
||||||
|
if (_datesOverlap(startDate, endDate, eventRealStartDate, eventRealEndDate)) {
|
||||||
|
isContainerFullyUsed = true;
|
||||||
|
containerConflictingEvent = event;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('[EventAvailabilityService] Error processing event ${doc.id}: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isContainerFullyUsed && containerConflictingEvent != null) {
|
||||||
|
// Boîte complète utilisée
|
||||||
|
conflicts.add(AvailabilityConflict(
|
||||||
|
equipmentId: container.id,
|
||||||
|
equipmentName: container.name,
|
||||||
|
conflictingEvent: containerConflictingEvent,
|
||||||
|
overlapDays: _calculateOverlapDays(
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
containerConflictingEvent.startDateTime,
|
||||||
|
containerConflictingEvent.endDateTime,
|
||||||
|
),
|
||||||
|
type: ConflictType.containerFullyUsed,
|
||||||
|
containerId: container.id,
|
||||||
|
containerName: container.name,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// Vérifier chaque équipement enfant individuellement
|
||||||
|
for (var equipment in containerEquipment) {
|
||||||
|
final equipmentConflicts = await checkEquipmentAvailability(
|
||||||
|
equipmentId: equipment.id,
|
||||||
|
equipmentName: equipment.name,
|
||||||
|
startDate: startDate,
|
||||||
|
endDate: endDate,
|
||||||
|
excludeEventId: excludeEventId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (equipmentConflicts.isNotEmpty) {
|
||||||
|
conflictingChildrenIds.add(equipment.id);
|
||||||
|
conflicts.addAll(equipmentConflicts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si au moins un enfant en conflit, ajouter un conflit pour la boîte
|
||||||
|
if (conflictingChildrenIds.isNotEmpty && conflicts.isNotEmpty) {
|
||||||
|
conflicts.insert(
|
||||||
|
0,
|
||||||
|
AvailabilityConflict(
|
||||||
|
equipmentId: container.id,
|
||||||
|
equipmentName: container.name,
|
||||||
|
conflictingEvent: conflicts.first.conflictingEvent,
|
||||||
|
overlapDays: conflicts.first.overlapDays,
|
||||||
|
type: ConflictType.containerPartiallyUsed,
|
||||||
|
containerId: container.id,
|
||||||
|
containerName: container.name,
|
||||||
|
conflictingChildrenIds: conflictingChildrenIds,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conflicts;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class EventPreparationService {
|
|||||||
|
|
||||||
// Collection references
|
// Collection references
|
||||||
CollectionReference get _eventsCollection => _firestore.collection('events');
|
CollectionReference get _eventsCollection => _firestore.collection('events');
|
||||||
CollectionReference get _equipmentCollection => _firestore.collection('equipment');
|
CollectionReference get _equipmentCollection => _firestore.collection('equipments');
|
||||||
|
|
||||||
// === PRÉPARATION ===
|
// === PRÉPARATION ===
|
||||||
|
|
||||||
@@ -29,9 +29,21 @@ class EventPreparationService {
|
|||||||
return eq;
|
return eq;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
await _eventsCollection.doc(eventId).update({
|
// Vérifier si tous les équipements sont préparés
|
||||||
|
final allPrepared = updatedEquipment.every((eq) => eq.isPrepared);
|
||||||
|
|
||||||
|
final updateData = <String, dynamic>{
|
||||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Mettre à jour le statut selon la complétion
|
||||||
|
if (allPrepared) {
|
||||||
|
updateData['preparationStatus'] = preparationStatusToString(PreparationStatus.completed);
|
||||||
|
} else {
|
||||||
|
updateData['preparationStatus'] = preparationStatusToString(PreparationStatus.inProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _eventsCollection.doc(eventId).update(updateData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error validating equipment preparation: $e');
|
print('Error validating equipment preparation: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -56,9 +68,13 @@ class EventPreparationService {
|
|||||||
'preparationStatus': preparationStatusToString(PreparationStatus.completed),
|
'preparationStatus': preparationStatusToString(PreparationStatus.completed),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mettre à jour le statut des équipements à "inUse"
|
// Mettre à jour le statut des équipements à "inUse" (seulement pour les équipements qui existent)
|
||||||
for (var equipment in event.assignedEquipment) {
|
for (var equipment in event.assignedEquipment) {
|
||||||
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse);
|
// Vérifier si l'équipement existe avant de mettre à jour son statut
|
||||||
|
final doc = await _equipmentCollection.doc(equipment.equipmentId).get();
|
||||||
|
if (doc.exists) {
|
||||||
|
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error validating all preparation: $e');
|
print('Error validating all preparation: $e');
|
||||||
@@ -85,7 +101,11 @@ class EventPreparationService {
|
|||||||
// Mettre à jour le statut des équipements préparés à "inUse"
|
// Mettre à jour le statut des équipements préparés à "inUse"
|
||||||
for (var equipment in event.assignedEquipment) {
|
for (var equipment in event.assignedEquipment) {
|
||||||
if (equipment.isPrepared && !missingEquipmentIds.contains(equipment.equipmentId)) {
|
if (equipment.isPrepared && !missingEquipmentIds.contains(equipment.equipmentId)) {
|
||||||
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse);
|
// Vérifier si l'équipement existe avant de mettre à jour son statut
|
||||||
|
final doc = await _equipmentCollection.doc(equipment.equipmentId).get();
|
||||||
|
if (doc.exists) {
|
||||||
|
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -120,9 +140,21 @@ class EventPreparationService {
|
|||||||
return eq;
|
return eq;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
await _eventsCollection.doc(eventId).update({
|
// Vérifier si tous les équipements sont retournés
|
||||||
|
final allReturned = updatedEquipment.every((eq) => eq.isReturned);
|
||||||
|
|
||||||
|
final updateData = <String, dynamic>{
|
||||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Mettre à jour le statut selon la complétion
|
||||||
|
if (allReturned) {
|
||||||
|
updateData['returnStatus'] = returnStatusToString(ReturnStatus.completed);
|
||||||
|
} else {
|
||||||
|
updateData['returnStatus'] = returnStatusToString(ReturnStatus.inProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _eventsCollection.doc(eventId).update(updateData);
|
||||||
|
|
||||||
// Mettre à jour le stock si c'est un consommable
|
// Mettre à jour le stock si c'est un consommable
|
||||||
if (returnedQuantity != null) {
|
if (returnedQuantity != null) {
|
||||||
@@ -176,9 +208,7 @@ class EventPreparationService {
|
|||||||
|
|
||||||
// Mettre à jour le statut des équipements à "available" et gérer les stocks
|
// Mettre à jour le statut des équipements à "available" et gérer les stocks
|
||||||
for (var equipment in updatedEquipment) {
|
for (var equipment in updatedEquipment) {
|
||||||
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available);
|
// Vérifier si le document existe
|
||||||
|
|
||||||
// Restaurer le stock pour les consommables
|
|
||||||
final equipmentDoc = await _equipmentCollection.doc(equipment.equipmentId).get();
|
final equipmentDoc = await _equipmentCollection.doc(equipment.equipmentId).get();
|
||||||
if (equipmentDoc.exists) {
|
if (equipmentDoc.exists) {
|
||||||
final equipmentData = EquipmentModel.fromMap(
|
final equipmentData = EquipmentModel.fromMap(
|
||||||
@@ -186,10 +216,17 @@ class EventPreparationService {
|
|||||||
equipmentDoc.id,
|
equipmentDoc.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Mettre à jour le statut uniquement pour les équipements non quantifiables
|
||||||
|
if (!equipmentData.hasQuantity) {
|
||||||
|
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restaurer le stock pour les consommables
|
||||||
if (equipmentData.hasQuantity && equipment.returnedQuantity != null) {
|
if (equipmentData.hasQuantity && equipment.returnedQuantity != null) {
|
||||||
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
||||||
await _equipmentCollection.doc(equipment.equipmentId).update({
|
await _equipmentCollection.doc(equipment.equipmentId).update({
|
||||||
'availableQuantity': currentAvailable + equipment.returnedQuantity!,
|
'availableQuantity': currentAvailable + equipment.returnedQuantity!,
|
||||||
|
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,27 +255,36 @@ class EventPreparationService {
|
|||||||
|
|
||||||
// Mettre à jour le statut des équipements retournés à "available"
|
// Mettre à jour le statut des équipements retournés à "available"
|
||||||
for (var equipment in event.assignedEquipment) {
|
for (var equipment in event.assignedEquipment) {
|
||||||
|
// Vérifier si le document existe
|
||||||
|
final equipmentDoc = await _equipmentCollection.doc(equipment.equipmentId).get();
|
||||||
|
if (!equipmentDoc.exists) {
|
||||||
|
continue; // Passer cet équipement s'il n'existe pas
|
||||||
|
}
|
||||||
|
|
||||||
|
final equipmentData = EquipmentModel.fromMap(
|
||||||
|
equipmentDoc.data() as Map<String, dynamic>,
|
||||||
|
equipmentDoc.id,
|
||||||
|
);
|
||||||
|
|
||||||
if (equipment.isReturned && !missingEquipmentIds.contains(equipment.equipmentId)) {
|
if (equipment.isReturned && !missingEquipmentIds.contains(equipment.equipmentId)) {
|
||||||
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available);
|
// Mettre à jour le statut uniquement pour les équipements non quantifiables
|
||||||
|
if (!equipmentData.hasQuantity) {
|
||||||
|
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available);
|
||||||
|
}
|
||||||
|
|
||||||
// Restaurer le stock pour les consommables
|
// Restaurer le stock pour les consommables
|
||||||
final equipmentDoc = await _equipmentCollection.doc(equipment.equipmentId).get();
|
if (equipmentData.hasQuantity && equipment.returnedQuantity != null) {
|
||||||
if (equipmentDoc.exists) {
|
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
||||||
final equipmentData = EquipmentModel.fromMap(
|
await _equipmentCollection.doc(equipment.equipmentId).update({
|
||||||
equipmentDoc.data() as Map<String, dynamic>,
|
'availableQuantity': currentAvailable + equipment.returnedQuantity!,
|
||||||
equipmentDoc.id,
|
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||||
);
|
});
|
||||||
|
|
||||||
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)) {
|
} else if (missingEquipmentIds.contains(equipment.equipmentId)) {
|
||||||
// Marquer comme perdu
|
// Marquer comme perdu uniquement pour les équipements non quantifiables
|
||||||
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.lost);
|
if (!equipmentData.hasQuantity) {
|
||||||
|
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.lost);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -252,13 +298,20 @@ class EventPreparationService {
|
|||||||
/// Mettre à jour le statut d'un équipement
|
/// Mettre à jour le statut d'un équipement
|
||||||
Future<void> updateEquipmentStatus(String equipmentId, EquipmentStatus status) async {
|
Future<void> updateEquipmentStatus(String equipmentId, EquipmentStatus status) async {
|
||||||
try {
|
try {
|
||||||
|
// Vérifier que le document existe avant de le mettre à jour
|
||||||
|
final doc = await _equipmentCollection.doc(equipmentId).get();
|
||||||
|
if (!doc.exists) {
|
||||||
|
print('Warning: Equipment document $equipmentId does not exist, skipping status update');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await _equipmentCollection.doc(equipmentId).update({
|
await _equipmentCollection.doc(equipmentId).update({
|
||||||
'status': equipmentStatusToString(status),
|
'status': equipmentStatusToString(status),
|
||||||
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error updating equipment status: $e');
|
print('Error updating equipment status for $equipmentId: $e');
|
||||||
rethrow;
|
// Ne pas rethrow pour ne pas bloquer le processus si un équipement n'existe pas
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
240
em2rp/lib/services/event_preparation_service_extended.dart
Normal file
240
em2rp/lib/services/event_preparation_service_extended.dart
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:em2rp/models/event_model.dart';
|
||||||
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
|
|
||||||
|
/// Service étendu pour gérer les 4 étapes : Préparation, Chargement, Déchargement, Retour
|
||||||
|
class EventPreparationServiceExtended {
|
||||||
|
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||||
|
|
||||||
|
CollectionReference get _eventsCollection => _firestore.collection('events');
|
||||||
|
CollectionReference get _equipmentCollection => _firestore.collection('equipments');
|
||||||
|
|
||||||
|
// === CHARGEMENT (LOADING) ===
|
||||||
|
|
||||||
|
/// Valider un équipement individuel pour le chargement
|
||||||
|
Future<void> validateEquipmentLoading(String eventId, String equipmentId) async {
|
||||||
|
try {
|
||||||
|
final event = await _getEvent(eventId);
|
||||||
|
if (event == null) throw Exception('Event not found');
|
||||||
|
|
||||||
|
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||||
|
if (eq.equipmentId == equipmentId) {
|
||||||
|
return eq.copyWith(isLoaded: true);
|
||||||
|
}
|
||||||
|
return eq;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Vérifier si tous les équipements sont chargés
|
||||||
|
final allLoaded = updatedEquipment.every((eq) => eq.isLoaded);
|
||||||
|
|
||||||
|
final updateData = <String, dynamic>{
|
||||||
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Si tous sont chargés, mettre à jour le statut
|
||||||
|
if (allLoaded) {
|
||||||
|
updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.completed);
|
||||||
|
} else {
|
||||||
|
updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.inProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _eventsCollection.doc(eventId).update(updateData);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error validating equipment loading: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valider tous les équipements pour le chargement
|
||||||
|
Future<void> validateAllLoading(String eventId) async {
|
||||||
|
try {
|
||||||
|
final event = await _getEvent(eventId);
|
||||||
|
if (event == null) throw Exception('Event not found');
|
||||||
|
|
||||||
|
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||||
|
return eq.copyWith(isLoaded: true);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
await _eventsCollection.doc(eventId).update({
|
||||||
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
|
'loadingStatus': loadingStatusToString(LoadingStatus.completed),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print('Error validating all loading: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === DÉCHARGEMENT (UNLOADING) ===
|
||||||
|
|
||||||
|
/// Valider un équipement individuel pour le déchargement
|
||||||
|
Future<void> validateEquipmentUnloading(String eventId, String equipmentId) async {
|
||||||
|
try {
|
||||||
|
final event = await _getEvent(eventId);
|
||||||
|
if (event == null) throw Exception('Event not found');
|
||||||
|
|
||||||
|
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||||
|
if (eq.equipmentId == equipmentId) {
|
||||||
|
return eq.copyWith(isUnloaded: true);
|
||||||
|
}
|
||||||
|
return eq;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Vérifier si tous les équipements sont déchargés
|
||||||
|
final allUnloaded = updatedEquipment.every((eq) => eq.isUnloaded);
|
||||||
|
|
||||||
|
final updateData = <String, dynamic>{
|
||||||
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Si tous sont déchargés, mettre à jour le statut
|
||||||
|
if (allUnloaded) {
|
||||||
|
updateData['unloadingStatus'] = unloadingStatusToString(UnloadingStatus.completed);
|
||||||
|
} else {
|
||||||
|
updateData['unloadingStatus'] = unloadingStatusToString(UnloadingStatus.inProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _eventsCollection.doc(eventId).update(updateData);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error validating equipment unloading: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valider tous les équipements pour le déchargement
|
||||||
|
Future<void> validateAllUnloading(String eventId) async {
|
||||||
|
try {
|
||||||
|
final event = await _getEvent(eventId);
|
||||||
|
if (event == null) throw Exception('Event not found');
|
||||||
|
|
||||||
|
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||||
|
return eq.copyWith(isUnloaded: true);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
await _eventsCollection.doc(eventId).update({
|
||||||
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
|
'unloadingStatus': unloadingStatusToString(UnloadingStatus.completed),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print('Error validating all unloading: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === PRÉPARATION + CHARGEMENT ===
|
||||||
|
|
||||||
|
/// Valider préparation ET chargement en même temps
|
||||||
|
Future<void> validateAllPreparationAndLoading(String eventId) async {
|
||||||
|
try {
|
||||||
|
final event = await _getEvent(eventId);
|
||||||
|
if (event == null) throw Exception('Event not found');
|
||||||
|
|
||||||
|
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||||
|
return eq.copyWith(isPrepared: true, isLoaded: true);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
await _eventsCollection.doc(eventId).update({
|
||||||
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
|
'preparationStatus': preparationStatusToString(PreparationStatus.completed),
|
||||||
|
'loadingStatus': loadingStatusToString(LoadingStatus.completed),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mettre à jour le statut des équipements
|
||||||
|
for (var equipment in event.assignedEquipment) {
|
||||||
|
final doc = await _equipmentCollection.doc(equipment.equipmentId).get();
|
||||||
|
if (doc.exists) {
|
||||||
|
await _updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error validating all preparation and loading: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === DÉCHARGEMENT + RETOUR ===
|
||||||
|
|
||||||
|
/// Valider déchargement ET retour en même temps
|
||||||
|
Future<void> validateAllUnloadingAndReturn(
|
||||||
|
String eventId,
|
||||||
|
Map<String, int>? returnedQuantities,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final event = await _getEvent(eventId);
|
||||||
|
if (event == null) throw Exception('Event not found');
|
||||||
|
|
||||||
|
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||||
|
final returnedQty = returnedQuantities?[eq.equipmentId] ??
|
||||||
|
eq.returnedQuantity ??
|
||||||
|
eq.quantity;
|
||||||
|
return eq.copyWith(
|
||||||
|
isUnloaded: true,
|
||||||
|
isReturned: true,
|
||||||
|
returnedQuantity: returnedQty,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
await _eventsCollection.doc(eventId).update({
|
||||||
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
|
'unloadingStatus': unloadingStatusToString(UnloadingStatus.completed),
|
||||||
|
'returnStatus': returnStatusToString(ReturnStatus.completed),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mettre à jour les statuts et stocks
|
||||||
|
for (var equipment in updatedEquipment) {
|
||||||
|
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) {
|
||||||
|
await _updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (equipmentData.hasQuantity && equipment.returnedQuantity != null) {
|
||||||
|
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
||||||
|
await _equipmentCollection.doc(equipment.equipmentId).update({
|
||||||
|
'availableQuantity': currentAvailable + equipment.returnedQuantity!,
|
||||||
|
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error validating all unloading and return: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === HELPERS ===
|
||||||
|
|
||||||
|
Future<void> _updateEquipmentStatus(String equipmentId, EquipmentStatus status) async {
|
||||||
|
try {
|
||||||
|
final doc = await _equipmentCollection.doc(equipmentId).get();
|
||||||
|
if (!doc.exists) return;
|
||||||
|
|
||||||
|
await _equipmentCollection.doc(equipmentId).update({
|
||||||
|
'status': equipmentStatusToString(status),
|
||||||
|
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating equipment status: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -5,4 +5,5 @@ class AppColors {
|
|||||||
static const Color blanc = Color(0xFFFFFFFF); // Blanc
|
static const Color blanc = Color(0xFFFFFFFF); // Blanc
|
||||||
static const Color rouge = Color.fromARGB(255, 159, 0, 0); // Rouge
|
static const Color rouge = Color.fromARGB(255, 159, 0, 0); // Rouge
|
||||||
static const Color gris = Color(0xFF808080); // Gris (gris moyen)
|
static const Color gris = Color(0xFF808080); // Gris (gris moyen)
|
||||||
|
static const Color bleuFonce = Color(0xFF1565C0); // Bleu foncé
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,383 +1,701 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:em2rp/models/event_model.dart';
|
import 'package:em2rp/models/event_model.dart';
|
||||||
import 'package:em2rp/models/equipment_model.dart';
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
import 'package:em2rp/providers/event_provider.dart';
|
import 'package:em2rp/models/container_model.dart';
|
||||||
import 'package:em2rp/providers/equipment_provider.dart';
|
import 'package:em2rp/providers/equipment_provider.dart';
|
||||||
import 'package:em2rp/providers/local_user_provider.dart';
|
import 'package:em2rp/providers/container_provider.dart';
|
||||||
import 'package:em2rp/services/event_preparation_service.dart';
|
import 'package:em2rp/services/event_preparation_service.dart';
|
||||||
|
import 'package:em2rp/services/event_preparation_service_extended.dart';
|
||||||
|
import 'package:em2rp/views/widgets/equipment/equipment_checklist_item.dart' show EquipmentChecklistItem, ChecklistStep;
|
||||||
|
import 'package:em2rp/views/widgets/equipment/missing_equipment_dialog.dart';
|
||||||
import 'package:em2rp/utils/colors.dart';
|
import 'package:em2rp/utils/colors.dart';
|
||||||
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
|
||||||
import 'package:em2rp/views/widgets/event/equipment_checklist_item.dart';
|
|
||||||
import 'package:em2rp/views/widgets/event/missing_equipment_dialog.dart';
|
|
||||||
import 'package:em2rp/views/widgets/event/preparation_success_dialog.dart';
|
|
||||||
|
|
||||||
|
/// Type d'étape de préparation
|
||||||
|
enum PreparationStep {
|
||||||
|
preparation, // Préparation dépôt
|
||||||
|
loadingOutbound, // Chargement aller
|
||||||
|
unloadingReturn, // Chargement retour (déchargement)
|
||||||
|
return_, // Retour dépôt
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Page de préparation ou de retour d'un événement
|
||||||
class EventPreparationPage extends StatefulWidget {
|
class EventPreparationPage extends StatefulWidget {
|
||||||
final String eventId;
|
final EventModel initialEvent;
|
||||||
|
|
||||||
const EventPreparationPage({
|
const EventPreparationPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.eventId,
|
required this.initialEvent,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<EventPreparationPage> createState() => _EventPreparationPageState();
|
State<EventPreparationPage> createState() => _EventPreparationPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EventPreparationPageState extends State<EventPreparationPage> {
|
class _EventPreparationPageState extends State<EventPreparationPage> with SingleTickerProviderStateMixin {
|
||||||
final EventPreparationService _preparationService = EventPreparationService();
|
final EventPreparationService _preparationService = EventPreparationService();
|
||||||
EventModel? _event;
|
final EventPreparationServiceExtended _extendedService = EventPreparationServiceExtended();
|
||||||
Map<String, EquipmentModel> _equipmentMap = {};
|
late AnimationController _animationController;
|
||||||
Map<String, int> _returnedQuantities = {}; // Pour les quantités retournées (consommables)
|
|
||||||
|
Map<String, EquipmentModel> _equipmentCache = {};
|
||||||
|
Map<String, ContainerModel> _containerCache = {};
|
||||||
|
Map<String, int> _returnedQuantities = {};
|
||||||
|
|
||||||
|
// État local des validations (non sauvegardé jusqu'à la validation finale)
|
||||||
|
Map<String, bool> _localValidationState = {};
|
||||||
|
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
bool _isSaving = false;
|
bool _isValidating = false;
|
||||||
|
bool _showSuccessAnimation = false;
|
||||||
|
bool _loadSimultaneously = false; // Checkbox "charger en même temps"
|
||||||
|
|
||||||
// Mode déterminé automatiquement
|
// Stockage de l'événement actuel
|
||||||
bool get _isReturnMode {
|
late EventModel _currentEvent;
|
||||||
if (_event == null) return false;
|
|
||||||
// Mode retour si préparation complétée et retour pas encore complété
|
// Détermine l'étape actuelle selon le statut de l'événement
|
||||||
return _event!.preparationStatus == PreparationStatus.completed ||
|
PreparationStep get _currentStep {
|
||||||
_event!.preparationStatus == PreparationStatus.completedWithMissing;
|
final prep = _currentEvent.preparationStatus ?? PreparationStatus.notStarted;
|
||||||
|
final loading = _currentEvent.loadingStatus ?? LoadingStatus.notStarted;
|
||||||
|
final unloading = _currentEvent.unloadingStatus ?? UnloadingStatus.notStarted;
|
||||||
|
final returnStatus = _currentEvent.returnStatus ?? ReturnStatus.notStarted;
|
||||||
|
|
||||||
|
// Logique stricte : on avance étape par étape
|
||||||
|
// 1. Préparation dépôt
|
||||||
|
if (prep != PreparationStatus.completed) {
|
||||||
|
return PreparationStep.preparation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Chargement aller (après préparation complète)
|
||||||
|
if (loading != LoadingStatus.completed) {
|
||||||
|
return PreparationStep.loadingOutbound;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Chargement retour (après chargement aller complet)
|
||||||
|
if (unloading != UnloadingStatus.completed) {
|
||||||
|
return PreparationStep.unloadingReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Retour dépôt (après déchargement complet)
|
||||||
|
if (returnStatus != ReturnStatus.completed) {
|
||||||
|
return PreparationStep.return_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tout est terminé, par défaut on retourne à la préparation
|
||||||
|
return PreparationStep.preparation;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get _pageTitle => _isReturnMode ? 'Retour matériel' : 'Préparation matériel';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadEventAndEquipment();
|
_currentEvent = widget.initialEvent;
|
||||||
|
_animationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Vérification de sécurité : bloquer l'accès si toutes les étapes sont complétées
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (_isCurrentStepCompleted()) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Cette étape est déjà terminée'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_loadEquipmentAndContainers();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadEventAndEquipment() async {
|
/// Vérifie si l'étape actuelle est déjà complétée
|
||||||
|
bool _isCurrentStepCompleted() {
|
||||||
|
switch (_currentStep) {
|
||||||
|
case PreparationStep.preparation:
|
||||||
|
return (_currentEvent.preparationStatus ?? PreparationStatus.notStarted) == PreparationStatus.completed;
|
||||||
|
case PreparationStep.loadingOutbound:
|
||||||
|
return (_currentEvent.loadingStatus ?? LoadingStatus.notStarted) == LoadingStatus.completed;
|
||||||
|
case PreparationStep.unloadingReturn:
|
||||||
|
return (_currentEvent.unloadingStatus ?? UnloadingStatus.notStarted) == UnloadingStatus.completed;
|
||||||
|
case PreparationStep.return_:
|
||||||
|
return (_currentEvent.returnStatus ?? ReturnStatus.notStarted) == ReturnStatus.completed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_animationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recharger l'événement depuis Firestore
|
||||||
|
Future<void> _reloadEvent() async {
|
||||||
try {
|
try {
|
||||||
// Charger l'événement
|
final doc = await FirebaseFirestore.instance
|
||||||
final eventProvider = context.read<EventProvider>();
|
.collection('events')
|
||||||
final event = await eventProvider.getEvent(widget.eventId);
|
.doc(_currentEvent.id)
|
||||||
|
.get();
|
||||||
|
|
||||||
if (event == null) {
|
if (doc.exists) {
|
||||||
throw Exception('Événement non trouvé');
|
setState(() {
|
||||||
|
_currentEvent = EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('[EventPreparationPage] Error reloading event: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Charger tous les équipements assignés
|
Future<void> _loadEquipmentAndContainers() async {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
|
try {
|
||||||
final equipmentProvider = context.read<EquipmentProvider>();
|
final equipmentProvider = context.read<EquipmentProvider>();
|
||||||
final Map<String, EquipmentModel> equipmentMap = {};
|
final containerProvider = context.read<ContainerProvider>();
|
||||||
|
|
||||||
for (var assignedEq in event.assignedEquipment) {
|
final equipment = await equipmentProvider.equipmentStream.first;
|
||||||
final equipment = await equipmentProvider.getEquipmentById(assignedEq.equipmentId);
|
final containers = await containerProvider.containersStream.first;
|
||||||
if (equipment != null) {
|
|
||||||
equipmentMap[assignedEq.equipmentId] = equipment;
|
|
||||||
|
|
||||||
// Initialiser les quantités retournées pour les consommables
|
for (var eq in _currentEvent.assignedEquipment) {
|
||||||
if (_isReturnMode &&
|
final equipmentItem = equipment.firstWhere(
|
||||||
(equipment.category == EquipmentCategory.consumable ||
|
(e) => e.id == eq.equipmentId,
|
||||||
equipment.category == EquipmentCategory.cable)) {
|
orElse: () => EquipmentModel(
|
||||||
_returnedQuantities[assignedEq.equipmentId] = assignedEq.returnedQuantity ?? assignedEq.quantity;
|
id: eq.equipmentId,
|
||||||
}
|
name: 'Équipement inconnu',
|
||||||
|
category: EquipmentCategory.other,
|
||||||
|
status: EquipmentStatus.available,
|
||||||
|
parentBoxIds: [],
|
||||||
|
maintenanceIds: [],
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_equipmentCache[eq.equipmentId] = equipmentItem;
|
||||||
|
|
||||||
|
// Initialiser l'état local de validation depuis l'événement
|
||||||
|
switch (_currentStep) {
|
||||||
|
case PreparationStep.preparation:
|
||||||
|
_localValidationState[eq.equipmentId] = eq.isPrepared;
|
||||||
|
break;
|
||||||
|
case PreparationStep.loadingOutbound:
|
||||||
|
_localValidationState[eq.equipmentId] = eq.isLoaded;
|
||||||
|
break;
|
||||||
|
case PreparationStep.unloadingReturn:
|
||||||
|
_localValidationState[eq.equipmentId] = eq.isUnloaded;
|
||||||
|
break;
|
||||||
|
case PreparationStep.return_:
|
||||||
|
_localValidationState[eq.equipmentId] = eq.isReturned;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_currentStep == PreparationStep.return_ ||
|
||||||
|
_currentStep == PreparationStep.unloadingReturn) &&
|
||||||
|
equipmentItem.hasQuantity) {
|
||||||
|
_returnedQuantities[eq.equipmentId] = eq.returnedQuantity ?? eq.quantity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
for (var containerId in _currentEvent.assignedContainers) {
|
||||||
_event = event;
|
final container = containers.firstWhere(
|
||||||
_equipmentMap = equipmentMap;
|
(c) => c.id == containerId,
|
||||||
_isLoading = false;
|
orElse: () => ContainerModel(
|
||||||
});
|
id: containerId,
|
||||||
|
name: 'Conteneur inconnu',
|
||||||
|
type: ContainerType.flightCase,
|
||||||
|
status: EquipmentStatus.available,
|
||||||
|
equipmentIds: [],
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_containerCache[containerId] = container;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
print('[EventPreparationPage] Error: $e');
|
||||||
|
} finally {
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Basculer l'état de validation d'un équipement (état local uniquement)
|
||||||
|
void _toggleEquipmentValidation(String equipmentId) {
|
||||||
|
setState(() {
|
||||||
|
_localValidationState[equipmentId] = !(_localValidationState[equipmentId] ?? false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _validateAll() async {
|
||||||
|
setState(() => _isValidating = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Si "tout valider" est cliqué, marquer tout comme validé localement
|
||||||
|
for (var equipmentId in _localValidationState.keys) {
|
||||||
|
_localValidationState[equipmentId] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Préparer la liste des équipements avec leur nouvel état
|
||||||
|
final updatedEquipment = _currentEvent.assignedEquipment.map((eq) {
|
||||||
|
final isValidated = _localValidationState[eq.equipmentId] ?? false;
|
||||||
|
|
||||||
|
switch (_currentStep) {
|
||||||
|
case PreparationStep.preparation:
|
||||||
|
if (_loadSimultaneously) {
|
||||||
|
return eq.copyWith(isPrepared: isValidated, isLoaded: isValidated);
|
||||||
|
}
|
||||||
|
return eq.copyWith(isPrepared: isValidated);
|
||||||
|
|
||||||
|
case PreparationStep.loadingOutbound:
|
||||||
|
return eq.copyWith(isLoaded: isValidated);
|
||||||
|
|
||||||
|
case PreparationStep.unloadingReturn:
|
||||||
|
if (_loadSimultaneously) {
|
||||||
|
final returnedQty = _returnedQuantities[eq.equipmentId] ?? eq.quantity;
|
||||||
|
return eq.copyWith(isUnloaded: isValidated, isReturned: isValidated, returnedQuantity: returnedQty);
|
||||||
|
}
|
||||||
|
return eq.copyWith(isUnloaded: isValidated);
|
||||||
|
|
||||||
|
case PreparationStep.return_:
|
||||||
|
final returnedQty = _returnedQuantities[eq.equipmentId] ?? eq.quantity;
|
||||||
|
return eq.copyWith(isReturned: isValidated, returnedQuantity: returnedQty);
|
||||||
|
}
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Mettre à jour Firestore selon l'étape
|
||||||
|
final updateData = <String, dynamic>{
|
||||||
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ajouter les statuts selon l'étape et la checkbox
|
||||||
|
switch (_currentStep) {
|
||||||
|
case PreparationStep.preparation:
|
||||||
|
updateData['preparationStatus'] = preparationStatusToString(PreparationStatus.completed);
|
||||||
|
if (_loadSimultaneously) {
|
||||||
|
updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.completed);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PreparationStep.loadingOutbound:
|
||||||
|
updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.completed);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PreparationStep.unloadingReturn:
|
||||||
|
updateData['unloadingStatus'] = unloadingStatusToString(UnloadingStatus.completed);
|
||||||
|
if (_loadSimultaneously) {
|
||||||
|
updateData['returnStatus'] = returnStatusToString(ReturnStatus.completed);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PreparationStep.return_:
|
||||||
|
updateData['returnStatus'] = returnStatusToString(ReturnStatus.completed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sauvegarder dans Firestore
|
||||||
|
await FirebaseFirestore.instance
|
||||||
|
.collection('events')
|
||||||
|
.doc(_currentEvent.id)
|
||||||
|
.update(updateData);
|
||||||
|
|
||||||
|
// Mettre à jour les statuts des équipements si nécessaire
|
||||||
|
if (_currentStep == PreparationStep.preparation ||
|
||||||
|
(_currentStep == PreparationStep.unloadingReturn && _loadSimultaneously)) {
|
||||||
|
await _updateEquipmentStatuses(updatedEquipment);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _showSuccessAnimation = true);
|
||||||
|
_animationController.forward();
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Erreur: $e'),
|
content: Text(_getSuccessMessage()),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur : $e'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _toggleEquipmentValidation(String equipmentId, bool isValidated) async {
|
|
||||||
try {
|
|
||||||
if (_isReturnMode) {
|
|
||||||
if (isValidated) {
|
|
||||||
final returnedQty = _returnedQuantities[equipmentId];
|
|
||||||
await _preparationService.validateEquipmentReturn(
|
|
||||||
widget.eventId,
|
|
||||||
equipmentId,
|
|
||||||
returnedQuantity: returnedQty,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await _preparationService.validateEquipmentPreparation(widget.eventId, equipmentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recharger l'événement
|
|
||||||
await _loadEventAndEquipment();
|
|
||||||
} catch (e) {
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Erreur: $e'), backgroundColor: Colors.red),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _validateAllQuickly() async {
|
|
||||||
setState(() => _isSaving = true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (_isReturnMode) {
|
|
||||||
await _preparationService.validateAllReturn(widget.eventId, _returnedQuantities);
|
|
||||||
} else {
|
|
||||||
await _preparationService.validateAllPreparation(widget.eventId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Afficher le dialog de succès avec animation
|
|
||||||
if (mounted) {
|
|
||||||
await showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder: (context) => PreparationSuccessDialog(isReturnMode: _isReturnMode),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Retour à la page précédente
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Erreur: $e'), backgroundColor: Colors.red),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isSaving = false);
|
if (mounted) setState(() => _isValidating = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _validatePreparation() async {
|
Future<void> _updateEquipmentStatuses(List<EventEquipment> equipment) async {
|
||||||
if (_event == null) return;
|
for (var eq in equipment) {
|
||||||
|
try {
|
||||||
|
final doc = await FirebaseFirestore.instance
|
||||||
|
.collection('equipments')
|
||||||
|
.doc(eq.equipmentId)
|
||||||
|
.get();
|
||||||
|
|
||||||
// Vérifier quels équipements ne sont pas validés
|
if (doc.exists) {
|
||||||
final missingEquipment = <EquipmentModel>[];
|
final equipmentData = EquipmentModel.fromMap(
|
||||||
final missingIds = <String>[];
|
doc.data() as Map<String, dynamic>,
|
||||||
|
doc.id,
|
||||||
|
);
|
||||||
|
|
||||||
for (var assignedEq in _event!.assignedEquipment) {
|
// Déterminer le nouveau statut
|
||||||
final isValidated = _isReturnMode ? assignedEq.isReturned : assignedEq.isPrepared;
|
EquipmentStatus newStatus;
|
||||||
|
if (eq.isReturned) {
|
||||||
|
newStatus = EquipmentStatus.available;
|
||||||
|
} else if (eq.isPrepared || eq.isLoaded) {
|
||||||
|
newStatus = EquipmentStatus.inUse;
|
||||||
|
} else {
|
||||||
|
continue; // Pas de changement
|
||||||
|
}
|
||||||
|
|
||||||
if (!isValidated) {
|
// Ne mettre à jour que les équipements non quantifiables
|
||||||
final equipment = _equipmentMap[assignedEq.equipmentId];
|
if (!equipmentData.hasQuantity) {
|
||||||
if (equipment != null) {
|
await FirebaseFirestore.instance
|
||||||
missingEquipment.add(equipment);
|
.collection('equipments')
|
||||||
missingIds.add(assignedEq.equipmentId);
|
.doc(eq.equipmentId)
|
||||||
|
.update({
|
||||||
|
'status': equipmentStatusToString(newStatus),
|
||||||
|
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gérer les stocks pour les consommables
|
||||||
|
if (equipmentData.hasQuantity && eq.isReturned && eq.returnedQuantity != null) {
|
||||||
|
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
||||||
|
await FirebaseFirestore.instance
|
||||||
|
.collection('equipments')
|
||||||
|
.doc(eq.equipmentId)
|
||||||
|
.update({
|
||||||
|
'availableQuantity': currentAvailable + eq.returnedQuantity!,
|
||||||
|
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating equipment status for ${eq.equipmentId}: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Si tout est validé, on finalise directement
|
String _getSuccessMessage() {
|
||||||
if (missingEquipment.isEmpty) {
|
switch (_currentStep) {
|
||||||
await _validateAllQuickly();
|
case PreparationStep.preparation:
|
||||||
return;
|
return _loadSimultaneously
|
||||||
|
? 'Préparation dépôt et chargement aller validés !'
|
||||||
|
: 'Préparation dépôt validée !';
|
||||||
|
case PreparationStep.loadingOutbound:
|
||||||
|
return 'Chargement aller validé !';
|
||||||
|
case PreparationStep.unloadingReturn:
|
||||||
|
return _loadSimultaneously
|
||||||
|
? 'Chargement retour et retour dépôt validés !'
|
||||||
|
: 'Chargement retour validé !';
|
||||||
|
case PreparationStep.return_:
|
||||||
|
return 'Retour dépôt validé !';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getStepTitle() {
|
||||||
|
switch (_currentStep) {
|
||||||
|
case PreparationStep.preparation:
|
||||||
|
return 'Préparation dépôt';
|
||||||
|
case PreparationStep.loadingOutbound:
|
||||||
|
return 'Chargement aller';
|
||||||
|
case PreparationStep.unloadingReturn:
|
||||||
|
return 'Chargement retour';
|
||||||
|
case PreparationStep.return_:
|
||||||
|
return 'Retour dépôt';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getValidateAllButtonText() {
|
||||||
|
if (_loadSimultaneously) {
|
||||||
|
return _currentStep == PreparationStep.preparation
|
||||||
|
? 'Tout confirmer comme préparé et chargé'
|
||||||
|
: 'Tout confirmer comme déchargé et retourné';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sinon, afficher le dialog des manquants
|
switch (_currentStep) {
|
||||||
if (mounted) {
|
case PreparationStep.preparation:
|
||||||
final result = await showDialog<String>(
|
return 'Tout confirmer comme préparé';
|
||||||
|
case PreparationStep.loadingOutbound:
|
||||||
|
return 'Tout confirmer comme chargé';
|
||||||
|
case PreparationStep.unloadingReturn:
|
||||||
|
return 'Tout confirmer comme déchargé';
|
||||||
|
case PreparationStep.return_:
|
||||||
|
return 'Tout confirmer comme retourné';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isStepCompleted() {
|
||||||
|
return _currentEvent.assignedEquipment.every((eq) {
|
||||||
|
return _localValidationState[eq.equipmentId] ?? false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getProgress() {
|
||||||
|
if (_currentEvent.assignedEquipment.isEmpty) return 0;
|
||||||
|
return _getValidatedCount() / _currentEvent.assignedEquipment.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _getValidatedCount() {
|
||||||
|
return _currentEvent.assignedEquipment.where((eq) {
|
||||||
|
return _localValidationState[eq.equipmentId] ?? false;
|
||||||
|
}).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChecklistStep _getChecklistStep() {
|
||||||
|
switch (_currentStep) {
|
||||||
|
case PreparationStep.preparation:
|
||||||
|
return ChecklistStep.preparation;
|
||||||
|
case PreparationStep.loadingOutbound:
|
||||||
|
return ChecklistStep.loading;
|
||||||
|
case PreparationStep.unloadingReturn:
|
||||||
|
return ChecklistStep.unloading;
|
||||||
|
case PreparationStep.return_:
|
||||||
|
return ChecklistStep.return_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _confirm() async {
|
||||||
|
// Vérifier s'il y a des équipements manquants (non cochés localement)
|
||||||
|
final missingEquipmentIds = _currentEvent.assignedEquipment
|
||||||
|
.where((eq) => !(_localValidationState[eq.equipmentId] ?? false))
|
||||||
|
.map((eq) => eq.equipmentId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (missingEquipmentIds.isEmpty) {
|
||||||
|
// Tout est validé, confirmer directement
|
||||||
|
await _validateAll();
|
||||||
|
} else {
|
||||||
|
// Afficher le dialog des manquants
|
||||||
|
final missingEquipmentModels = missingEquipmentIds
|
||||||
|
.map((id) => _equipmentCache[id])
|
||||||
|
.whereType<EquipmentModel>()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final missingEventEquipment = _currentEvent.assignedEquipment
|
||||||
|
.where((eq) => missingEquipmentIds.contains(eq.equipmentId))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final action = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => MissingEquipmentDialog(
|
builder: (context) => MissingEquipmentDialog(
|
||||||
missingEquipments: missingEquipment,
|
missingEquipment: missingEquipmentModels,
|
||||||
eventId: widget.eventId,
|
eventEquipment: missingEventEquipment,
|
||||||
isReturnMode: _isReturnMode,
|
isReturnMode: _currentStep == PreparationStep.return_ ||
|
||||||
|
_currentStep == PreparationStep.unloadingReturn,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result == 'confirm_with_missing') {
|
if (action == 'confirm_anyway') {
|
||||||
setState(() => _isSaving = true);
|
// Confirmer malgré les manquants
|
||||||
try {
|
await _validateAll();
|
||||||
if (_isReturnMode) {
|
} else if (action == 'mark_as_validated') {
|
||||||
await _preparationService.completeReturnWithMissing(widget.eventId, missingIds);
|
// Marquer les manquants comme validés localement
|
||||||
} else {
|
for (var equipmentId in missingEquipmentIds) {
|
||||||
await _preparationService.completePreparationWithMissing(widget.eventId, missingIds);
|
_localValidationState[equipmentId] = true;
|
||||||
}
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
await showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder: (context) => PreparationSuccessDialog(isReturnMode: _isReturnMode),
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Erreur: $e'), backgroundColor: Colors.red),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setState(() => _isSaving = false);
|
|
||||||
}
|
|
||||||
} else if (result == 'validate_missing') {
|
|
||||||
// Valider tous les manquants
|
|
||||||
setState(() => _isSaving = true);
|
|
||||||
try {
|
|
||||||
for (var equipmentId in missingIds) {
|
|
||||||
await _toggleEquipmentValidation(equipmentId, true);
|
|
||||||
}
|
|
||||||
await _validateAllQuickly();
|
|
||||||
} finally {
|
|
||||||
setState(() => _isSaving = false);
|
|
||||||
}
|
}
|
||||||
|
setState(() {});
|
||||||
|
// Puis confirmer
|
||||||
|
await _validateAll();
|
||||||
}
|
}
|
||||||
|
// Si 'return_to_list', ne rien faire
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final userProvider = context.watch<LocalUserProvider>();
|
final allValidated = _isStepCompleted();
|
||||||
final userId = userProvider.uid;
|
final stepTitle = _getStepTitle();
|
||||||
final hasManagePermission = userProvider.hasPermission('manage_events');
|
|
||||||
|
|
||||||
// Vérifier si l'utilisateur fait partie de l'équipe
|
|
||||||
final isInWorkforce = _event?.workforce.any((ref) => ref.id == userId) ?? false;
|
|
||||||
final hasPermission = hasManagePermission || isInWorkforce;
|
|
||||||
|
|
||||||
if (!hasPermission) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: const CustomAppBar(title: 'Accès refusé'),
|
|
||||||
body: const Center(
|
|
||||||
child: Text('Vous n\'avez pas les permissions pour accéder à cette page.'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(title: _pageTitle),
|
appBar: AppBar(
|
||||||
body: _isLoading
|
title: Text(stepTitle),
|
||||||
? const Center(child: CircularProgressIndicator())
|
backgroundColor: AppColors.bleuFonce,
|
||||||
: _event == null
|
),
|
||||||
? const Center(child: Text('Événement introuvable'))
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
_isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
: Column(
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
// En-tête avec info de l'événement
|
Container(
|
||||||
_buildEventHeader(),
|
padding: const EdgeInsets.all(16),
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_currentEvent.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: _getProgress(),
|
||||||
|
backgroundColor: Colors.grey.shade300,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
allValidated ? Colors.green : AppColors.bleuFonce,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
'${_getValidatedCount()}/${_currentEvent.assignedEquipment.length}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// Bouton "Tout confirmer"
|
// Checkbox "charger en même temps" (uniquement pour préparation ou chargement retour)
|
||||||
_buildQuickValidateButton(),
|
if (_currentStep == PreparationStep.preparation ||
|
||||||
|
_currentStep == PreparationStep.unloadingReturn)
|
||||||
|
CheckboxListTile(
|
||||||
|
value: _loadSimultaneously,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_loadSimultaneously = value ?? false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
title: Text(
|
||||||
|
_currentStep == PreparationStep.preparation
|
||||||
|
? 'Charger en même temps (chargement aller)'
|
||||||
|
: 'Confirmer le retour en même temps (retour dépôt)',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
|
||||||
// Liste des équipements
|
const SizedBox(height: 8),
|
||||||
Expanded(child: _buildEquipmentList()),
|
ElevatedButton.icon(
|
||||||
|
onPressed: allValidated ? null : _validateAll,
|
||||||
|
icon: const Icon(Icons.check_circle_outline),
|
||||||
|
label: Text(
|
||||||
|
allValidated
|
||||||
|
? 'Tout est validé !'
|
||||||
|
: _getValidateAllButtonText(),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: allValidated ? Colors.green : AppColors.bleuFonce,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: _currentEvent.assignedEquipment.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final eventEquipment = _currentEvent.assignedEquipment[index];
|
||||||
|
final equipment = _equipmentCache[eventEquipment.equipmentId];
|
||||||
|
|
||||||
// Bouton de validation final
|
if (equipment == null) {
|
||||||
_buildValidateButton(),
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return EquipmentChecklistItem(
|
||||||
|
equipment: equipment,
|
||||||
|
eventEquipment: eventEquipment,
|
||||||
|
step: _getChecklistStep(),
|
||||||
|
isValidated: _localValidationState[equipment.id] ?? false,
|
||||||
|
onToggle: () => _toggleEquipmentValidation(equipment.id),
|
||||||
|
onReturnedQuantityChanged: _currentStep == PreparationStep.return_ && equipment.hasQuantity
|
||||||
|
? (qty) {
|
||||||
|
setState(() {
|
||||||
|
_returnedQuantities[equipment.id] = qty;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
if (_showSuccessAnimation)
|
||||||
}
|
Center(
|
||||||
|
child: ScaleTransition(
|
||||||
Widget _buildEventHeader() {
|
scale: _animationController,
|
||||||
return Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(32),
|
||||||
decoration: BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: AppColors.rouge.withOpacity(0.1),
|
color: Colors.green,
|
||||||
border: Border(
|
shape: BoxShape.circle,
|
||||||
bottom: BorderSide(color: Colors.grey.shade300),
|
),
|
||||||
),
|
child: const Icon(
|
||||||
|
Icons.check,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
bottomNavigationBar: Container(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
decoration: BoxDecoration(
|
||||||
Text(
|
color: Colors.white,
|
||||||
_event!.name,
|
boxShadow: [
|
||||||
style: const TextStyle(
|
BoxShadow(
|
||||||
fontSize: 20,
|
color: Colors.black.withValues(alpha: 0.1),
|
||||||
fontWeight: FontWeight.bold,
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, -2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _isValidating ? null : _confirm,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: allValidated ? Colors.green : AppColors.rouge,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
child: _isValidating
|
||||||
Text(
|
? const SizedBox(
|
||||||
'${_event!.assignedEquipment.length} équipement(s) assigné(s)',
|
height: 20,
|
||||||
style: TextStyle(color: Colors.grey.shade700),
|
width: 20,
|
||||||
),
|
child: CircularProgressIndicator(
|
||||||
],
|
strokeWidth: 2,
|
||||||
),
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
);
|
),
|
||||||
}
|
)
|
||||||
|
: Text(
|
||||||
Widget _buildQuickValidateButton() {
|
'Confirmer ${_getStepTitle().toLowerCase()}',
|
||||||
return Container(
|
style: const TextStyle(
|
||||||
padding: const EdgeInsets.all(16),
|
fontSize: 18,
|
||||||
child: ElevatedButton.icon(
|
fontWeight: FontWeight.bold,
|
||||||
onPressed: _isSaving ? null : _validateAllQuickly,
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
),
|
||||||
backgroundColor: Colors.green,
|
|
||||||
minimumSize: const Size(double.infinity, 50),
|
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.check_circle, color: Colors.white),
|
|
||||||
label: Text(
|
|
||||||
_isReturnMode ? 'Tout confirmer comme retourné' : 'Tout confirmer comme préparé',
|
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEquipmentList() {
|
|
||||||
return ListView.builder(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
itemCount: _event!.assignedEquipment.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final assignedEq = _event!.assignedEquipment[index];
|
|
||||||
final equipment = _equipmentMap[assignedEq.equipmentId];
|
|
||||||
|
|
||||||
if (equipment == null) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
final isValidated = _isReturnMode ? assignedEq.isReturned : assignedEq.isPrepared;
|
|
||||||
|
|
||||||
return EquipmentChecklistItem(
|
|
||||||
equipment: equipment,
|
|
||||||
isValidated: isValidated,
|
|
||||||
onValidate: (value) => _toggleEquipmentValidation(assignedEq.equipmentId, value),
|
|
||||||
isReturnMode: _isReturnMode,
|
|
||||||
quantity: assignedEq.quantity,
|
|
||||||
returnedQuantity: _returnedQuantities[assignedEq.equipmentId],
|
|
||||||
onReturnedQuantityChanged: _isReturnMode
|
|
||||||
? (value) {
|
|
||||||
setState(() {
|
|
||||||
_returnedQuantities[assignedEq.equipmentId] = value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildValidateButton() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.3),
|
|
||||||
spreadRadius: 1,
|
|
||||||
blurRadius: 5,
|
|
||||||
offset: const Offset(0, -2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: _isSaving ? null : _validatePreparation,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppColors.rouge,
|
|
||||||
minimumSize: const Size(double.infinity, 50),
|
|
||||||
),
|
|
||||||
child: _isSaving
|
|
||||||
? const CircularProgressIndicator(color: Colors.white)
|
|
||||||
: Text(
|
|
||||||
_isReturnMode ? 'Finaliser le retour' : 'Finaliser la préparation',
|
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:em2rp/providers/event_provider.dart';
|
|||||||
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_navigation.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_navigation.dart';
|
||||||
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_header.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_header.dart';
|
||||||
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_status_button.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_status_button.dart';
|
||||||
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_preparation_buttons.dart';
|
||||||
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_info.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_info.dart';
|
||||||
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_description.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_description.dart';
|
||||||
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_documents.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details_components/event_details_documents.dart';
|
||||||
@@ -60,6 +61,8 @@ class EventDetails extends StatelessWidget {
|
|||||||
onSelectEvent: onSelectEvent,
|
onSelectEvent: onSelectEvent,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Boutons de préparation et retour
|
||||||
|
EventPreparationButtons(event: event),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ class _EventDetailsHeaderState extends State<EventDetailsHeader> {
|
|||||||
_buildStatusIcon(widget.event.status),
|
_buildStatusIcon(widget.event.status),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.calendar_today, color: AppColors.rouge),
|
icon: const Icon(Icons.add_to_home_screen, color: AppColors.rouge),
|
||||||
tooltip: 'Exporter vers Google Calendar',
|
tooltip: 'Ajouter a mon application de calendrier',
|
||||||
onPressed: _exportToCalendar,
|
onPressed: _exportToCalendar,
|
||||||
),
|
),
|
||||||
if (Provider.of<LocalUserProvider>(context, listen: false)
|
if (Provider.of<LocalUserProvider>(context, listen: false)
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:em2rp/models/event_model.dart';
|
||||||
|
import 'package:em2rp/views/event_preparation_page.dart';
|
||||||
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
|
||||||
|
/// Boutons de préparation et retour d'événement
|
||||||
|
class EventPreparationButtons extends StatefulWidget {
|
||||||
|
final EventModel event;
|
||||||
|
|
||||||
|
const EventPreparationButtons({
|
||||||
|
super.key,
|
||||||
|
required this.event,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EventPreparationButtons> createState() => _EventPreparationButtonsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EventPreparationButtonsState extends State<EventPreparationButtons> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Écouter les changements de l'événement en temps réel
|
||||||
|
return StreamBuilder<DocumentSnapshot>(
|
||||||
|
stream: FirebaseFirestore.instance
|
||||||
|
.collection('events')
|
||||||
|
.doc(widget.event.id)
|
||||||
|
.snapshots(),
|
||||||
|
initialData: null,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
// Utiliser l'événement du stream si disponible, sinon l'événement initial
|
||||||
|
final EventModel currentEvent;
|
||||||
|
if (snapshot.hasData && snapshot.data != null && snapshot.data!.exists) {
|
||||||
|
currentEvent = EventModel.fromMap(
|
||||||
|
snapshot.data!.data() as Map<String, dynamic>,
|
||||||
|
snapshot.data!.id,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
currentEvent = widget.event;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildButtons(context, currentEvent);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildButtons(BuildContext context, EventModel event) {
|
||||||
|
// Vérifier s'il y a du matériel assigné
|
||||||
|
final hasMaterial = event.assignedEquipment.isNotEmpty || event.assignedContainers.isNotEmpty;
|
||||||
|
|
||||||
|
if (!hasMaterial) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déterminer l'étape actuelle
|
||||||
|
final prep = event.preparationStatus ?? PreparationStatus.notStarted;
|
||||||
|
final loading = event.loadingStatus ?? LoadingStatus.notStarted;
|
||||||
|
final unloading = event.unloadingStatus ?? UnloadingStatus.notStarted;
|
||||||
|
final returnStatus = event.returnStatus ?? ReturnStatus.notStarted;
|
||||||
|
|
||||||
|
String buttonText;
|
||||||
|
IconData buttonIcon;
|
||||||
|
bool isCompleted = false;
|
||||||
|
|
||||||
|
if (prep != PreparationStatus.completed) {
|
||||||
|
buttonText = 'Préparation dépôt';
|
||||||
|
buttonIcon = Icons.inventory_2;
|
||||||
|
} else if (loading != LoadingStatus.completed) {
|
||||||
|
buttonText = 'Chargement aller';
|
||||||
|
buttonIcon = Icons.local_shipping;
|
||||||
|
} else if (unloading != UnloadingStatus.completed) {
|
||||||
|
buttonText = 'Chargement retour';
|
||||||
|
buttonIcon = Icons.unarchive;
|
||||||
|
} else if (returnStatus != ReturnStatus.completed) {
|
||||||
|
buttonText = 'Retour dépôt';
|
||||||
|
buttonIcon = Icons.assignment_return;
|
||||||
|
} else {
|
||||||
|
buttonText = 'Terminé';
|
||||||
|
buttonIcon = Icons.check_circle;
|
||||||
|
isCompleted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Bouton de l'étape actuelle
|
||||||
|
if (!isCompleted)
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
final result = await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => EventPreparationPage(
|
||||||
|
initialEvent: event,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Si la validation a réussi, le StreamBuilder se rechargera automatiquement
|
||||||
|
if (result == true && context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Étape validée avec succès'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: Icon(buttonIcon),
|
||||||
|
label: Text(
|
||||||
|
buttonText,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.bleuFonce,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Indicateur de completion
|
||||||
|
if (isCompleted)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.green, width: 1),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: const [
|
||||||
|
Icon(Icons.check_circle, color: Colors.green, size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Toutes les étapes sont terminées',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.green,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
161
em2rp/lib/views/widgets/equipment/equipment_checklist_item.dart
Normal file
161
em2rp/lib/views/widgets/equipment/equipment_checklist_item.dart
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
|
import 'package:em2rp/models/event_model.dart';
|
||||||
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
|
||||||
|
/// Type d'étape pour le checklist
|
||||||
|
enum ChecklistStep {
|
||||||
|
preparation,
|
||||||
|
loading,
|
||||||
|
unloading,
|
||||||
|
return_,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget pour afficher un équipement dans une checklist de préparation/retour
|
||||||
|
class EquipmentChecklistItem extends StatelessWidget {
|
||||||
|
final EquipmentModel equipment;
|
||||||
|
final EventEquipment eventEquipment;
|
||||||
|
final ChecklistStep step;
|
||||||
|
final bool isValidated; // État de validation (passé depuis le parent)
|
||||||
|
final VoidCallback onToggle;
|
||||||
|
final ValueChanged<int>? onReturnedQuantityChanged;
|
||||||
|
|
||||||
|
const EquipmentChecklistItem({
|
||||||
|
super.key,
|
||||||
|
required this.equipment,
|
||||||
|
required this.eventEquipment,
|
||||||
|
this.step = ChecklistStep.preparation,
|
||||||
|
required this.isValidated,
|
||||||
|
required this.onToggle,
|
||||||
|
this.onReturnedQuantityChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final hasQuantity = equipment.hasQuantity;
|
||||||
|
final showQuantityInput = step == ChecklistStep.return_ && hasQuantity;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 0),
|
||||||
|
elevation: 1,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
side: BorderSide(
|
||||||
|
color: isValidated ? Colors.green : Colors.grey.shade300,
|
||||||
|
width: isValidated ? 2 : 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
leading: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isValidated ? Colors.green.shade100 : Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
isValidated ? Icons.check_circle : Icons.radio_button_unchecked,
|
||||||
|
color: isValidated ? Colors.green : Colors.grey,
|
||||||
|
),
|
||||||
|
onPressed: onToggle,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
equipment.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
decoration: isValidated ? TextDecoration.lineThrough : null,
|
||||||
|
color: isValidated ? Colors.grey : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (equipment.model != null)
|
||||||
|
Text(
|
||||||
|
equipment.model!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasQuantity) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Quantité : ${eventEquipment.quantity}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppColors.bleuFonce,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (showQuantityInput && onReturnedQuantityChanged != null) ...[
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
const Icon(Icons.arrow_forward, size: 12, color: Colors.grey),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Retourné : ',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 80,
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
hintText: '${eventEquipment.quantity}',
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
onChanged: (value) {
|
||||||
|
final qty = int.tryParse(value) ?? eventEquipment.quantity;
|
||||||
|
onReturnedQuantityChanged!(qty);
|
||||||
|
},
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: isValidated
|
||||||
|
? Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: const [
|
||||||
|
Icon(Icons.check, size: 16, color: Colors.green),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Validé',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.green,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
192
em2rp/lib/views/widgets/equipment/missing_equipment_dialog.dart
Normal file
192
em2rp/lib/views/widgets/equipment/missing_equipment_dialog.dart
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
|
import 'package:em2rp/models/event_model.dart';
|
||||||
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
|
||||||
|
/// Dialog affichant les équipements manquants lors de la préparation/retour
|
||||||
|
class MissingEquipmentDialog extends StatelessWidget {
|
||||||
|
final List<EquipmentModel> missingEquipment;
|
||||||
|
final List<EventEquipment> eventEquipment;
|
||||||
|
final bool isReturnMode;
|
||||||
|
|
||||||
|
const MissingEquipmentDialog({
|
||||||
|
super.key,
|
||||||
|
required this.missingEquipment,
|
||||||
|
required this.eventEquipment,
|
||||||
|
this.isReturnMode = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 600, maxHeight: 600),
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// En-tête avec icône warning
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.shade100,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.warning_amber_rounded,
|
||||||
|
size: 32,
|
||||||
|
color: Colors.orange.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
isReturnMode
|
||||||
|
? 'Équipements manquants au retour'
|
||||||
|
: 'Équipements manquants',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${missingEquipment.length} équipement(s) non validé(s)',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Liste des équipements manquants
|
||||||
|
Flexible(
|
||||||
|
child: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: missingEquipment.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final equipment = missingEquipment[index];
|
||||||
|
final eventEq = eventEquipment.firstWhere(
|
||||||
|
(eq) => eq.equipmentId == equipment.id,
|
||||||
|
orElse: () => EventEquipment(equipmentId: equipment.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: Colors.orange.shade100,
|
||||||
|
child: equipment.category.getIcon(
|
||||||
|
size: 20,
|
||||||
|
color: Colors.orange.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
equipment.name,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
subtitle: equipment.hasQuantity
|
||||||
|
? Text('Quantité : ${eventEq.quantity}')
|
||||||
|
: Text(equipment.model ?? equipment.category.label),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
color: Colors.orange.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Bouton principal : Confirmer malgré les manquants
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop('confirm_anyway'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.rouge,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
isReturnMode
|
||||||
|
? 'Confirmer le retour malgré les manquants'
|
||||||
|
: 'Confirmer malgré les manquants',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Bouton secondaire : Marquer comme validés
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop('mark_as_validated'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
side: BorderSide(color: AppColors.bleuFonce, width: 2),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Indiquer les manquants comme validés',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.bleuFonce,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Bouton tertiaire : Retourner à la liste
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop('return_to_list'),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Retourner à la liste',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -24,8 +24,22 @@ class _EquipmentConflictDialogState extends State<EquipmentConflictDialog> {
|
|||||||
.where((entry) => !_removedEquipmentIds.contains(entry.key))
|
.where((entry) => !_removedEquipmentIds.contains(entry.key))
|
||||||
.fold(0, (sum, entry) => sum + entry.value.length);
|
.fold(0, (sum, entry) => sum + entry.value.length);
|
||||||
|
|
||||||
|
/// Retourne l'icône appropriée selon le type de conflit
|
||||||
|
IconData _getIconForConflict(AvailabilityConflict conflict) {
|
||||||
|
switch (conflict.type) {
|
||||||
|
case ConflictType.containerFullyUsed:
|
||||||
|
case ConflictType.containerPartiallyUsed:
|
||||||
|
return Icons.inventory_2;
|
||||||
|
case ConflictType.insufficientQuantity:
|
||||||
|
return Icons.production_quantity_limits;
|
||||||
|
case ConflictType.equipmentUnavailable:
|
||||||
|
return Icons.block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
final dateFormat = DateFormat('dd/MM/yyyy');
|
final dateFormat = DateFormat('dd/MM/yyyy');
|
||||||
|
|
||||||
return Dialog(
|
return Dialog(
|
||||||
@@ -117,19 +131,37 @@ class _EquipmentConflictDialogState extends State<EquipmentConflictDialog> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.inventory_2,
|
_getIconForConflict(firstConflict),
|
||||||
color: isRemoved ? Colors.grey : AppColors.rouge,
|
color: isRemoved ? Colors.grey : AppColors.rouge,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Column(
|
||||||
firstConflict.equipmentName,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: TextStyle(
|
children: [
|
||||||
fontWeight: FontWeight.bold,
|
Text(
|
||||||
fontSize: 16,
|
firstConflict.equipmentName,
|
||||||
decoration: isRemoved ? TextDecoration.lineThrough : null,
|
style: TextStyle(
|
||||||
),
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
decoration: isRemoved ? TextDecoration.lineThrough : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Message de conflit spécifique
|
||||||
|
if (firstConflict.conflictMessage.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Text(
|
||||||
|
firstConflict.conflictMessage,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.orange.shade700,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isRemoved)
|
if (isRemoved)
|
||||||
|
|||||||
@@ -213,28 +213,54 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
setState(() => _isLoadingConflicts = true);
|
setState(() => _isLoadingConflicts = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
print('[EquipmentSelectionDialog] Loading equipment conflicts...');
|
|
||||||
final equipmentProvider = context.read<EquipmentProvider>();
|
final equipmentProvider = context.read<EquipmentProvider>();
|
||||||
final equipment = await equipmentProvider.equipmentStream.first;
|
final equipment = await equipmentProvider.equipmentStream.first;
|
||||||
|
|
||||||
print('[EquipmentSelectionDialog] Checking conflicts for ${equipment.length} equipments');
|
|
||||||
|
|
||||||
for (var eq in equipment) {
|
for (var eq in equipment) {
|
||||||
final conflicts = await _availabilityService.checkEquipmentAvailability(
|
// Pour les consommables/câbles, vérifier avec gestion de quantité
|
||||||
equipmentId: eq.id,
|
if (eq.hasQuantity) {
|
||||||
equipmentName: eq.id,
|
// Récupérer la quantité disponible
|
||||||
startDate: widget.startDate,
|
final availableQty = await _availabilityService.getAvailableQuantity(
|
||||||
endDate: widget.endDate,
|
equipment: eq,
|
||||||
excludeEventId: widget.excludeEventId,
|
startDate: widget.startDate,
|
||||||
);
|
endDate: widget.endDate,
|
||||||
|
excludeEventId: widget.excludeEventId,
|
||||||
|
);
|
||||||
|
|
||||||
if (conflicts.isNotEmpty) {
|
// Vérifier si un item de cet équipement est déjà sélectionné
|
||||||
print('[EquipmentSelectionDialog] Found ${conflicts.length} conflict(s) for ${eq.id}');
|
final selectedItem = _selectedItems[eq.id];
|
||||||
_equipmentConflicts[eq.id] = conflicts;
|
final requestedQty = selectedItem?.quantity ?? 1;
|
||||||
|
|
||||||
|
// ✅ Ne créer un conflit QUE si la quantité demandée dépasse la quantité disponible
|
||||||
|
if (requestedQty > availableQty) {
|
||||||
|
final conflicts = await _availabilityService.checkEquipmentAvailabilityWithQuantity(
|
||||||
|
equipment: eq,
|
||||||
|
requestedQuantity: requestedQty,
|
||||||
|
startDate: widget.startDate,
|
||||||
|
endDate: widget.endDate,
|
||||||
|
excludeEventId: widget.excludeEventId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (conflicts.isNotEmpty) {
|
||||||
|
_equipmentConflicts[eq.id] = conflicts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sinon, pas de conflit à afficher dans la liste
|
||||||
|
} else {
|
||||||
|
// Pour les équipements non quantifiables, vérification classique
|
||||||
|
final conflicts = await _availabilityService.checkEquipmentAvailability(
|
||||||
|
equipmentId: eq.id,
|
||||||
|
equipmentName: eq.id,
|
||||||
|
startDate: widget.startDate,
|
||||||
|
endDate: widget.endDate,
|
||||||
|
excludeEventId: widget.excludeEventId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (conflicts.isNotEmpty) {
|
||||||
|
_equipmentConflicts[eq.id] = conflicts;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print('[EquipmentSelectionDialog] Total equipments with conflicts: ${_equipmentConflicts.length}');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[EquipmentSelectionDialog] Error loading conflicts: $e');
|
print('[EquipmentSelectionDialog] Error loading conflicts: $e');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -247,32 +273,76 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
try {
|
try {
|
||||||
print('[EquipmentSelectionDialog] Loading container conflicts...');
|
print('[EquipmentSelectionDialog] Loading container conflicts...');
|
||||||
final containerProvider = context.read<ContainerProvider>();
|
final containerProvider = context.read<ContainerProvider>();
|
||||||
|
final equipmentProvider = context.read<EquipmentProvider>();
|
||||||
final containers = await containerProvider.containersStream.first;
|
final containers = await containerProvider.containersStream.first;
|
||||||
|
final allEquipment = await equipmentProvider.equipmentStream.first;
|
||||||
|
|
||||||
print('[EquipmentSelectionDialog] Checking conflicts for ${containers.length} containers');
|
print('[EquipmentSelectionDialog] Checking conflicts for ${containers.length} containers');
|
||||||
|
|
||||||
for (var container in containers) {
|
for (var container in containers) {
|
||||||
final conflictingChildren = <String>[];
|
// Vérifier d'abord si la boîte complète est utilisée ailleurs
|
||||||
|
final containerEquipment = allEquipment
|
||||||
|
.where((eq) => container.equipmentIds.contains(eq.id))
|
||||||
|
.toList();
|
||||||
|
|
||||||
// Vérifier chaque équipement enfant
|
final containerConflicts = await _availabilityService.checkContainerAvailability(
|
||||||
for (var equipmentId in container.equipmentIds) {
|
container: container,
|
||||||
if (_equipmentConflicts.containsKey(equipmentId)) {
|
containerEquipment: containerEquipment,
|
||||||
conflictingChildren.add(equipmentId);
|
startDate: widget.startDate,
|
||||||
}
|
endDate: widget.endDate,
|
||||||
}
|
excludeEventId: widget.excludeEventId,
|
||||||
|
);
|
||||||
|
|
||||||
if (conflictingChildren.isNotEmpty) {
|
if (containerConflicts.isNotEmpty) {
|
||||||
final status = conflictingChildren.length == container.equipmentIds.length
|
// Déterminer le statut en fonction du type de conflit
|
||||||
? ContainerConflictStatus.complete
|
final hasFullConflict = containerConflicts.any(
|
||||||
: ContainerConflictStatus.partial;
|
(c) => c.type == ConflictType.containerFullyUsed,
|
||||||
|
|
||||||
_containerConflicts[container.id] = ContainerConflictInfo(
|
|
||||||
status: status,
|
|
||||||
conflictingEquipmentIds: conflictingChildren,
|
|
||||||
totalChildren: container.equipmentIds.length,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
print('[EquipmentSelectionDialog] Container ${container.id}: ${status.name} conflict (${conflictingChildren.length}/${container.equipmentIds.length} children)');
|
final conflictingChildren = containerConflicts
|
||||||
|
.where((c) => c.type != ConflictType.containerFullyUsed &&
|
||||||
|
c.type != ConflictType.containerPartiallyUsed)
|
||||||
|
.map((c) => c.equipmentId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final status = hasFullConflict
|
||||||
|
? ContainerConflictStatus.complete
|
||||||
|
: (conflictingChildren.isNotEmpty
|
||||||
|
? ContainerConflictStatus.partial
|
||||||
|
: ContainerConflictStatus.none);
|
||||||
|
|
||||||
|
if (status != ContainerConflictStatus.none) {
|
||||||
|
_containerConflicts[container.id] = ContainerConflictInfo(
|
||||||
|
status: status,
|
||||||
|
conflictingEquipmentIds: conflictingChildren,
|
||||||
|
totalChildren: container.equipmentIds.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
print('[EquipmentSelectionDialog] Container ${container.id}: ${status.name} conflict');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Vérifier chaque équipement enfant individuellement
|
||||||
|
final conflictingChildren = <String>[];
|
||||||
|
|
||||||
|
for (var equipmentId in container.equipmentIds) {
|
||||||
|
if (_equipmentConflicts.containsKey(equipmentId)) {
|
||||||
|
conflictingChildren.add(equipmentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conflictingChildren.isNotEmpty) {
|
||||||
|
final status = conflictingChildren.length == container.equipmentIds.length
|
||||||
|
? ContainerConflictStatus.complete
|
||||||
|
: ContainerConflictStatus.partial;
|
||||||
|
|
||||||
|
_containerConflicts[container.id] = ContainerConflictInfo(
|
||||||
|
status: status,
|
||||||
|
conflictingEquipmentIds: conflictingChildren,
|
||||||
|
totalChildren: container.equipmentIds.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
print('[EquipmentSelectionDialog] Container ${container.id}: ${status.name} conflict (${conflictingChildren.length}/${container.equipmentIds.length} children)');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,10 +62,6 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
|||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
print('[EventAssignedEquipmentSection] Loading equipment and containers...');
|
|
||||||
print('[EventAssignedEquipmentSection] assignedEquipment: ${widget.assignedEquipment.map((e) => e.equipmentId).toList()}');
|
|
||||||
print('[EventAssignedEquipmentSection] assignedContainers: ${widget.assignedContainers}');
|
|
||||||
|
|
||||||
final equipmentProvider = context.read<EquipmentProvider>();
|
final equipmentProvider = context.read<EquipmentProvider>();
|
||||||
final containerProvider = context.read<ContainerProvider>();
|
final containerProvider = context.read<ContainerProvider>();
|
||||||
|
|
||||||
@@ -73,62 +69,40 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
|||||||
final equipment = await equipmentProvider.equipmentStream.first;
|
final equipment = await equipmentProvider.equipmentStream.first;
|
||||||
final containers = await containerProvider.containersStream.first;
|
final containers = await containerProvider.containersStream.first;
|
||||||
|
|
||||||
print('[EventAssignedEquipmentSection] Available equipment count: ${equipment.length}');
|
|
||||||
print('[EventAssignedEquipmentSection] Available containers count: ${containers.length}');
|
|
||||||
|
|
||||||
// Créer le cache des équipements
|
// Créer le cache des équipements
|
||||||
for (var eq in widget.assignedEquipment) {
|
for (var eq in widget.assignedEquipment) {
|
||||||
print('[EventAssignedEquipmentSection] Looking for equipment: ${eq.equipmentId}');
|
|
||||||
final equipmentItem = equipment.firstWhere(
|
final equipmentItem = equipment.firstWhere(
|
||||||
(e) {
|
(e) => e.id == eq.equipmentId,
|
||||||
print('[EventAssignedEquipmentSection] Comparing "${e.id}" with "${eq.equipmentId}"');
|
orElse: () => EquipmentModel(
|
||||||
return e.id == eq.equipmentId;
|
id: eq.equipmentId,
|
||||||
},
|
name: 'Équipement inconnu',
|
||||||
orElse: () {
|
category: EquipmentCategory.other,
|
||||||
print('[EventAssignedEquipmentSection] Equipment NOT FOUND: ${eq.equipmentId}');
|
status: EquipmentStatus.available,
|
||||||
return EquipmentModel(
|
parentBoxIds: [],
|
||||||
id: eq.equipmentId,
|
maintenanceIds: [],
|
||||||
name: 'Équipement inconnu',
|
createdAt: DateTime.now(),
|
||||||
category: EquipmentCategory.other,
|
updatedAt: DateTime.now(),
|
||||||
status: EquipmentStatus.available,
|
),
|
||||||
parentBoxIds: [],
|
|
||||||
maintenanceIds: [],
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
updatedAt: DateTime.now(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
_equipmentCache[eq.equipmentId] = equipmentItem;
|
_equipmentCache[eq.equipmentId] = equipmentItem;
|
||||||
print('[EventAssignedEquipmentSection] Cached equipment: ${equipmentItem.id} (${equipmentItem.name})');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Créer le cache des conteneurs
|
// Créer le cache des conteneurs
|
||||||
for (var containerId in widget.assignedContainers) {
|
for (var containerId in widget.assignedContainers) {
|
||||||
print('[EventAssignedEquipmentSection] Looking for container: $containerId');
|
|
||||||
final container = containers.firstWhere(
|
final container = containers.firstWhere(
|
||||||
(c) {
|
(c) => c.id == containerId,
|
||||||
print('[EventAssignedEquipmentSection] Comparing "${c.id}" with "$containerId"');
|
orElse: () => ContainerModel(
|
||||||
return c.id == containerId;
|
id: containerId,
|
||||||
},
|
name: 'Conteneur inconnu',
|
||||||
orElse: () {
|
type: ContainerType.flightCase,
|
||||||
print('[EventAssignedEquipmentSection] Container NOT FOUND: $containerId');
|
status: EquipmentStatus.available,
|
||||||
return ContainerModel(
|
equipmentIds: [],
|
||||||
id: containerId,
|
updatedAt: DateTime.now(),
|
||||||
name: 'Conteneur inconnu',
|
createdAt: DateTime.now(),
|
||||||
type: ContainerType.flightCase,
|
),
|
||||||
status: EquipmentStatus.available,
|
|
||||||
equipmentIds: [],
|
|
||||||
updatedAt: DateTime.now(),
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
_containerCache[containerId] = container;
|
_containerCache[containerId] = container;
|
||||||
print('[EventAssignedEquipmentSection] Cached container: ${container.id} (${container.name})');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print('[EventAssignedEquipmentSection] Equipment cache: ${_equipmentCache.keys.toList()}');
|
|
||||||
print('[EventAssignedEquipmentSection] Container cache: ${_containerCache.keys.toList()}');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[EventAssignedEquipmentSection] Error loading equipment/containers: $e');
|
print('[EventAssignedEquipmentSection] Error loading equipment/containers: $e');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -173,18 +147,16 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Charger les équipements des conteneurs pour vérifier les conflits
|
// Charger les équipements et conteneurs
|
||||||
final containerProvider = context.read<ContainerProvider>();
|
final containerProvider = context.read<ContainerProvider>();
|
||||||
final equipmentProvider = context.read<EquipmentProvider>();
|
final equipmentProvider = context.read<EquipmentProvider>();
|
||||||
|
|
||||||
final allContainers = await containerProvider.containersStream.first;
|
final allContainers = await containerProvider.containersStream.first;
|
||||||
final allEquipment = await equipmentProvider.equipmentStream.first;
|
final allEquipment = await equipmentProvider.equipmentStream.first;
|
||||||
|
|
||||||
// Collecter TOUS les équipements à vérifier (directs + enfants des boîtes)
|
final allConflicts = <String, List<AvailabilityConflict>>{};
|
||||||
final equipmentIds = newEquipment.map((e) => e.equipmentId).toList();
|
|
||||||
final equipmentNames = <String, String>{};
|
|
||||||
|
|
||||||
// Ajouter les équipements directs
|
// 1. Vérifier les conflits pour les équipements directs
|
||||||
for (var eq in newEquipment) {
|
for (var eq in newEquipment) {
|
||||||
final equipment = allEquipment.firstWhere(
|
final equipment = allEquipment.firstWhere(
|
||||||
(e) => e.id == eq.equipmentId,
|
(e) => e.id == eq.equipmentId,
|
||||||
@@ -199,10 +171,51 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
|||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
equipmentNames[eq.equipmentId] = equipment.id;
|
|
||||||
|
// Pour les équipements quantifiables (consommables/câbles)
|
||||||
|
if (equipment.hasQuantity) {
|
||||||
|
// Vérifier la quantité disponible
|
||||||
|
final availableQty = await _availabilityService.getAvailableQuantity(
|
||||||
|
equipment: equipment,
|
||||||
|
startDate: widget.startDate!,
|
||||||
|
endDate: widget.endDate!,
|
||||||
|
excludeEventId: widget.eventId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ⚠️ Ne créer un conflit QUE si la quantité demandée est supérieure à la quantité disponible
|
||||||
|
if (eq.quantity > availableQty) {
|
||||||
|
// Il y a vraiment un conflit de quantité
|
||||||
|
final conflicts = await _availabilityService.checkEquipmentAvailabilityWithQuantity(
|
||||||
|
equipment: equipment,
|
||||||
|
requestedQuantity: eq.quantity,
|
||||||
|
startDate: widget.startDate!,
|
||||||
|
endDate: widget.endDate!,
|
||||||
|
excludeEventId: widget.eventId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ne garder que les conflits réels (quand il n'y a pas assez de stock)
|
||||||
|
if (conflicts.isNotEmpty) {
|
||||||
|
allConflicts[eq.equipmentId] = conflicts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ✅ Sinon, pas de conflit : il y a assez de stock disponible
|
||||||
|
} else {
|
||||||
|
// Pour les équipements non quantifiables (vérification classique)
|
||||||
|
final conflicts = await _availabilityService.checkEquipmentAvailability(
|
||||||
|
equipmentId: equipment.id,
|
||||||
|
equipmentName: equipment.name,
|
||||||
|
startDate: widget.startDate!,
|
||||||
|
endDate: widget.endDate!,
|
||||||
|
excludeEventId: widget.eventId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (conflicts.isNotEmpty) {
|
||||||
|
allConflicts[eq.equipmentId] = conflicts;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajouter les équipements des conteneurs (par composition)
|
// 2. Vérifier les conflits pour les boîtes et leur contenu
|
||||||
for (var containerId in newContainers) {
|
for (var containerId in newContainers) {
|
||||||
final container = allContainers.firstWhere(
|
final container = allContainers.firstWhere(
|
||||||
(c) => c.id == containerId,
|
(c) => c.id == containerId,
|
||||||
@@ -217,76 +230,105 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ajouter tous les équipements enfants pour vérification
|
// Récupérer les équipements de la boîte
|
||||||
for (var childEquipmentId in container.equipmentIds) {
|
final containerEquipment = container.equipmentIds
|
||||||
if (!equipmentIds.contains(childEquipmentId)) {
|
.map((eqId) => allEquipment.firstWhere(
|
||||||
equipmentIds.add(childEquipmentId);
|
(e) => e.id == eqId,
|
||||||
|
orElse: () => EquipmentModel(
|
||||||
|
id: eqId,
|
||||||
|
name: 'Inconnu',
|
||||||
|
category: EquipmentCategory.other,
|
||||||
|
status: EquipmentStatus.available,
|
||||||
|
parentBoxIds: [],
|
||||||
|
maintenanceIds: [],
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
|
||||||
final equipment = allEquipment.firstWhere(
|
// Vérifier chaque équipement de la boîte individuellement
|
||||||
(e) => e.id == childEquipmentId,
|
final containerConflicts = <AvailabilityConflict>[];
|
||||||
orElse: () => EquipmentModel(
|
|
||||||
id: childEquipmentId,
|
for (var equipment in containerEquipment) {
|
||||||
name: 'Inconnu',
|
if (equipment.hasQuantity) {
|
||||||
category: EquipmentCategory.other,
|
// Pour les consommables/câbles, vérifier la quantité disponible
|
||||||
status: EquipmentStatus.available,
|
final availableQty = await _availabilityService.getAvailableQuantity(
|
||||||
parentBoxIds: [],
|
equipment: equipment,
|
||||||
maintenanceIds: [],
|
startDate: widget.startDate!,
|
||||||
createdAt: DateTime.now(),
|
endDate: widget.endDate!,
|
||||||
updatedAt: DateTime.now(),
|
excludeEventId: widget.eventId,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
equipmentNames[childEquipmentId] = '${equipment.id} (dans ${container.name})';
|
|
||||||
|
// La boîte contient 1 unité de cet équipement
|
||||||
|
// Si la quantité disponible est insuffisante, créer un conflit
|
||||||
|
if (availableQty < 1) {
|
||||||
|
final conflicts = await _availabilityService.checkEquipmentAvailability(
|
||||||
|
equipmentId: equipment.id,
|
||||||
|
equipmentName: equipment.name,
|
||||||
|
startDate: widget.startDate!,
|
||||||
|
endDate: widget.endDate!,
|
||||||
|
excludeEventId: widget.eventId,
|
||||||
|
);
|
||||||
|
containerConflicts.addAll(conflicts);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Pour les équipements non quantifiables
|
||||||
|
final conflicts = await _availabilityService.checkEquipmentAvailability(
|
||||||
|
equipmentId: equipment.id,
|
||||||
|
equipmentName: equipment.name,
|
||||||
|
startDate: widget.startDate!,
|
||||||
|
endDate: widget.endDate!,
|
||||||
|
excludeEventId: widget.eventId,
|
||||||
|
);
|
||||||
|
containerConflicts.addAll(conflicts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (containerConflicts.isNotEmpty) {
|
||||||
|
allConflicts[containerId] = containerConflicts;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier les conflits pour TOUS les équipements (directs + enfants)
|
if (allConflicts.isNotEmpty) {
|
||||||
final conflicts = await _availabilityService.checkMultipleEquipmentAvailability(
|
|
||||||
equipmentIds: equipmentIds,
|
|
||||||
equipmentNames: equipmentNames,
|
|
||||||
startDate: widget.startDate!,
|
|
||||||
endDate: widget.endDate!,
|
|
||||||
excludeEventId: widget.eventId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (conflicts.isNotEmpty) {
|
|
||||||
// Afficher le dialog de conflits
|
// Afficher le dialog de conflits
|
||||||
final action = await showDialog<String>(
|
final action = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => EquipmentConflictDialog(conflicts: conflicts),
|
builder: (context) => EquipmentConflictDialog(conflicts: allConflicts),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (action == 'cancel') {
|
if (action == 'cancel') {
|
||||||
return; // Annuler l'ajout
|
return; // Annuler l'ajout
|
||||||
} else if (action == 'force_removed') {
|
} else if (action == 'force_removed') {
|
||||||
// Identifier quels équipements retirer
|
// Identifier quels équipements/conteneurs retirer
|
||||||
final removedIds = conflicts.keys.toSet();
|
final removedIds = allConflicts.keys.toSet();
|
||||||
|
|
||||||
// Retirer les équipements directs en conflit
|
// Retirer les équipements directs en conflit
|
||||||
newEquipment.removeWhere((eq) => removedIds.contains(eq.equipmentId));
|
newEquipment.removeWhere((eq) => removedIds.contains(eq.equipmentId));
|
||||||
|
|
||||||
// Retirer les boîtes dont au moins un équipement enfant est en conflit
|
// Retirer les boîtes en conflit
|
||||||
final containersToRemove = <String>[];
|
newContainers.removeWhere((containerId) => removedIds.contains(containerId));
|
||||||
for (var containerId in newContainers) {
|
|
||||||
final container = allContainers.firstWhere((c) => c.id == containerId);
|
|
||||||
final hasConflict = container.equipmentIds.any((eqId) => removedIds.contains(eqId));
|
|
||||||
|
|
||||||
if (hasConflict) {
|
// Informer l'utilisateur des boîtes retirées
|
||||||
containersToRemove.add(containerId);
|
for (var containerId in removedIds.where((id) => newContainers.contains(id))) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var containerId in containersToRemove) {
|
|
||||||
newContainers.remove(containerId);
|
|
||||||
|
|
||||||
// Informer l'utilisateur
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final containerName = allContainers.firstWhere((c) => c.id == containerId).name;
|
final container = allContainers.firstWhere(
|
||||||
|
(c) => c.id == containerId,
|
||||||
|
orElse: () => ContainerModel(
|
||||||
|
id: containerId,
|
||||||
|
name: 'Inconnu',
|
||||||
|
type: ContainerType.flightCase,
|
||||||
|
status: EquipmentStatus.available,
|
||||||
|
equipmentIds: [],
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('La boîte "$containerName" a été retirée car elle contient du matériel en conflit.'),
|
content: Text('La boîte "${container.name}" a été retirée en raison de conflits.'),
|
||||||
backgroundColor: Colors.orange,
|
backgroundColor: Colors.orange,
|
||||||
duration: const Duration(seconds: 4),
|
duration: const Duration(seconds: 3),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -299,8 +341,21 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
|||||||
final updatedEquipment = [...widget.assignedEquipment];
|
final updatedEquipment = [...widget.assignedEquipment];
|
||||||
final updatedContainers = [...widget.assignedContainers];
|
final updatedContainers = [...widget.assignedContainers];
|
||||||
|
|
||||||
|
// Pour chaque nouvel équipement
|
||||||
for (var eq in newEquipment) {
|
for (var eq in newEquipment) {
|
||||||
if (!updatedEquipment.any((e) => e.equipmentId == eq.equipmentId)) {
|
final existingIndex = updatedEquipment.indexWhere((e) => e.equipmentId == eq.equipmentId);
|
||||||
|
|
||||||
|
if (existingIndex != -1) {
|
||||||
|
// L'équipement existe déjà : mettre à jour la quantité
|
||||||
|
updatedEquipment[existingIndex] = EventEquipment(
|
||||||
|
equipmentId: eq.equipmentId,
|
||||||
|
quantity: eq.quantity, // Utiliser la nouvelle quantité
|
||||||
|
isPrepared: updatedEquipment[existingIndex].isPrepared,
|
||||||
|
isReturned: updatedEquipment[existingIndex].isReturned,
|
||||||
|
returnedQuantity: updatedEquipment[existingIndex].returnedQuantity,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// L'équipement n'existe pas : l'ajouter
|
||||||
updatedEquipment.add(eq);
|
updatedEquipment.add(eq);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,11 +402,6 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
|||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
print('[EventAssignedEquipmentSection] Removing container $containerId');
|
|
||||||
if (container != null) {
|
|
||||||
print('[EventAssignedEquipmentSection] Removing ${container.equipmentIds.length} children: ${container.equipmentIds}');
|
|
||||||
}
|
|
||||||
print('[EventAssignedEquipmentSection] Equipment before: ${widget.assignedEquipment.length}, after: ${updatedEquipment.length}');
|
|
||||||
|
|
||||||
// Notifier le changement avec les deux listes mises à jour
|
// Notifier le changement avec les deux listes mises à jour
|
||||||
widget.onChanged(updatedEquipment, updatedContainers);
|
widget.onChanged(updatedEquipment, updatedContainers);
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ class UserCard extends StatefulWidget {
|
|||||||
|
|
||||||
class _UserCardState extends State<UserCard> {
|
class _UserCardState extends State<UserCard> {
|
||||||
ImageProvider? _profileImage;
|
ImageProvider? _profileImage;
|
||||||
String? _lastUrl;
|
|
||||||
bool _isLoadingImage = false;
|
bool _isLoadingImage = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -44,7 +43,6 @@ class _UserCardState extends State<UserCard> {
|
|||||||
if (url.isNotEmpty) {
|
if (url.isNotEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoadingImage = true;
|
_isLoadingImage = true;
|
||||||
_lastUrl = url;
|
|
||||||
});
|
});
|
||||||
final image = NetworkImage(url);
|
final image = NetworkImage(url);
|
||||||
image.resolve(const ImageConfiguration()).addListener(
|
image.resolve(const ImageConfiguration()).addListener(
|
||||||
@@ -71,7 +69,6 @@ class _UserCardState extends State<UserCard> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_profileImage = null;
|
_profileImage = null;
|
||||||
_isLoadingImage = false;
|
_isLoadingImage = false;
|
||||||
_lastUrl = null;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user