import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:cloud_functions/cloud_functions.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/container_model.dart'; import 'package:em2rp/providers/equipment_provider.dart'; import 'package:em2rp/providers/container_provider.dart'; import 'package:em2rp/providers/event_provider.dart'; import 'package:em2rp/providers/local_user_provider.dart'; import 'package:em2rp/services/data_service.dart'; import 'package:em2rp/services/api_service.dart'; import 'package:em2rp/services/qr_code_processing_service.dart'; import 'package:em2rp/services/audio_feedback_service.dart'; import 'package:em2rp/services/equipment_service.dart'; import 'package:em2rp/views/widgets/equipment/equipment_checklist_item.dart' show EquipmentChecklistItem, ChecklistStep; import 'package:em2rp/views/widgets/equipment/container_checklist_item.dart'; import 'package:em2rp/views/widgets/common/qr_code_scanner_dialog.dart'; import 'package:em2rp/views/widgets/event_preparation/code_not_found_dialog.dart'; import 'package:em2rp/views/widgets/event_preparation/add_equipment_to_event_dialog.dart'; import 'package:em2rp/utils/debug_log.dart'; import 'package:em2rp/views/widgets/equipment/missing_equipment_dialog.dart'; import 'package:em2rp/utils/colors.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 { final EventModel initialEvent; const EventPreparationPage({ super.key, required this.initialEvent, }); @override State createState() => _EventPreparationPageState(); } class _EventPreparationPageState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late final DataService _dataService; late final QRCodeProcessingService _qrCodeService; Map _equipmentCache = {}; Map _containerCache = {}; Map _returnedQuantities = {}; // État local des validations (non sauvegardé jusqu'à la validation finale) Map _localValidationState = {}; // Gestion des quantités par étape Map _quantitiesAtPreparation = {}; Map _quantitiesAtLoading = {}; Map _quantitiesAtUnloading = {}; Map _quantitiesAtReturn = {}; bool _isLoading = true; bool _isValidating = false; bool _showSuccessAnimation = false; bool _loadSimultaneously = false; // Checkbox "charger en même temps" // Stockage de l'événement actuel late EventModel _currentEvent; // 🆕 Pour la saisie manuelle de codes final TextEditingController _manualCodeController = TextEditingController(); final FocusNode _manualCodeFocusNode = FocusNode(); // Détermine l'étape actuelle selon le statut de l'événement PreparationStep get _currentStep { 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; } @override void initState() { super.initState(); _currentEvent = widget.initialEvent; _dataService = DataService(FirebaseFunctionsApiService()); _qrCodeService = QRCodeProcessingService(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 500), ); // Vérification de sécurité et chargement après le premier frame 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; } // Charger les équipements après le premier frame pour éviter setState pendant build _loadEquipmentAndContainers(); }); } /// 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(); _manualCodeController.dispose(); _manualCodeFocusNode.dispose(); super.dispose(); } Future _loadEquipmentAndContainers() async { setState(() => _isLoading = true); try { // 🔧 FIX: Utiliser getEventWithDetails pour charger toutes les données d'un coup DebugLog.info('[EventPreparationPage] Loading event with details: ${_currentEvent.id}'); final result = await _dataService.getEventWithDetails(_currentEvent.id); final equipmentsMap = result['equipments'] as Map; final containersMap = result['containers'] as Map; DebugLog.info('[EventPreparationPage] Loaded ${equipmentsMap.length} equipments and ${containersMap.length} containers with details'); // Remplir les caches _equipmentCache.clear(); _containerCache.clear(); // Remplir le cache d'équipements equipmentsMap.forEach((id, data) { try { final equipment = EquipmentModel.fromMap(data as Map, id); _equipmentCache[id] = equipment; } catch (e) { DebugLog.error('[EventPreparationPage] Error parsing equipment $id', e); } }); // Remplir le cache de containers containersMap.forEach((id, data) { try { final container = ContainerModel.fromMap(data as Map, id); _containerCache[id] = container; } catch (e) { DebugLog.error('[EventPreparationPage] Error parsing container $id', e); } }); // Initialiser les états de validation et quantités pour chaque équipement assigné for (var eq in _currentEvent.assignedEquipment) { final equipmentItem = _equipmentCache[eq.equipmentId]; // S'assurer que l'équipement est dans le cache (même si inconnu) if (equipmentItem == null) { _equipmentCache[eq.equipmentId] = EquipmentModel( id: eq.equipmentId, name: 'Équipement inconnu', category: EquipmentCategory.other, status: EquipmentStatus.available, maintenanceIds: [], createdAt: DateTime.now(), updatedAt: DateTime.now(), ); } // 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 ?? false)) { _returnedQuantities[eq.equipmentId] = eq.quantityAtReturn ?? eq.quantity; } } // S'assurer que les containers assignés sont dans le cache (même si inconnus) for (var containerId in _currentEvent.assignedContainers) { if (!_containerCache.containsKey(containerId)) { _containerCache[containerId] = ContainerModel( id: containerId, name: 'Conteneur inconnu', type: ContainerType.flightCase, status: EquipmentStatus.available, equipmentIds: [], updatedAt: DateTime.now(), createdAt: DateTime.now(), ); } } } catch (e) { DebugLog.error('[EventPreparationPage] Error', e); } finally { setState(() => _isLoading = false); } } /// Basculer l'état de validation d'un équipement (état local uniquement) Future _toggleEquipmentValidation(String equipmentId) async { final currentState = _localValidationState[equipmentId] ?? false; // Si on veut valider (passer de false à true) et que c'était manquant avant if (!currentState && _wasMissingAtPreviousStep(equipmentId)) { final confirmed = await _confirmValidationIfWasMissingBefore(equipmentId); if (!confirmed) { return; // Annulation, ne rien faire } } setState(() { _localValidationState[equipmentId] = !currentState; }); } /// Marquer TOUT comme validé et enregistrer (bouton "Tout confirmer") Future _validateAllAndConfirm() async { // Marquer tout comme validé localement setState(() { for (var eq in _currentEvent.assignedEquipment) { _localValidationState[eq.equipmentId] = true; } }); // Puis enregistrer await _confirmCurrentState(); } /// Enregistrer l'état actuel TEL QUEL (cochés = validés, non cochés = manquants) Future _confirmCurrentState() async { setState(() => _isValidating = true); try { // Déterminer les manquants = équipements NON validés final Map missingAtThisStep = {}; for (var eq in _currentEvent.assignedEquipment) { final isValidated = _localValidationState[eq.equipmentId] ?? false; missingAtThisStep[eq.equipmentId] = !isValidated; // Manquant si pas validé } // Préparer la liste des équipements avec leur nouvel état final updatedEquipment = _currentEvent.assignedEquipment.map((eq) { final isValidated = _localValidationState[eq.equipmentId] ?? false; final isMissing = missingAtThisStep[eq.equipmentId] ?? false; // Récupérer les quantités selon l'étape final qtyAtPrep = _quantitiesAtPreparation[eq.equipmentId]; final qtyAtLoad = _quantitiesAtLoading[eq.equipmentId]; final qtyAtUnload = _quantitiesAtUnloading[eq.equipmentId]; final qtyAtRet = _quantitiesAtReturn[eq.equipmentId]; switch (_currentStep) { case PreparationStep.preparation: if (_loadSimultaneously) { return eq.copyWith( isPrepared: isValidated, isLoaded: isValidated, isMissingAtPreparation: isMissing, isMissingAtLoading: isMissing, // Propager quantityAtPreparation: qtyAtPrep, quantityAtLoading: qtyAtPrep, // Même quantité ); } return eq.copyWith( isPrepared: isValidated, isMissingAtPreparation: isMissing, quantityAtPreparation: qtyAtPrep, ); case PreparationStep.loadingOutbound: return eq.copyWith( isLoaded: isValidated, isMissingAtLoading: isMissing, quantityAtLoading: qtyAtLoad, ); case PreparationStep.unloadingReturn: if (_loadSimultaneously) { return eq.copyWith( isUnloaded: isValidated, isReturned: isValidated, isMissingAtUnloading: isMissing, isMissingAtReturn: isMissing, // Propager quantityAtUnloading: qtyAtUnload, quantityAtReturn: qtyAtRet ?? qtyAtUnload, ); } return eq.copyWith( isUnloaded: isValidated, isMissingAtUnloading: isMissing, quantityAtUnloading: qtyAtUnload, ); case PreparationStep.return_: return eq.copyWith( isReturned: isValidated, isMissingAtReturn: isMissing, quantityAtReturn: qtyAtRet, ); } }).toList(); // Mettre à jour Firestore selon l'étape final updateData = { 'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(), }; // Ajouter les statuts selon l'étape et la checkbox String validationType = 'CHECK'; switch (_currentStep) { case PreparationStep.preparation: updateData['preparationStatus'] = preparationStatusToString(PreparationStatus.completed); validationType = 'CHECK_OUT'; if (_loadSimultaneously) { updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.completed); validationType = 'LOADING'; } break; case PreparationStep.loadingOutbound: updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.completed); validationType = 'LOADING'; break; case PreparationStep.unloadingReturn: updateData['unloadingStatus'] = unloadingStatusToString(UnloadingStatus.completed); validationType = 'UNLOADING'; if (_loadSimultaneously) { updateData['returnStatus'] = returnStatusToString(ReturnStatus.completed); validationType = 'CHECK_IN'; } break; case PreparationStep.return_: updateData['returnStatus'] = returnStatusToString(ReturnStatus.completed); validationType = 'CHECK_IN'; break; } // Sauvegarder dans Firestore via l'API await _dataService.updateEventEquipment( eventId: _currentEvent.id, assignedEquipment: updatedEquipment.map((e) => e.toMap()).toList(), preparationStatus: updateData['preparationStatus'], loadingStatus: updateData['loadingStatus'], unloadingStatus: updateData['unloadingStatus'], returnStatus: updateData['returnStatus'], ); // Mettre à jour les statuts des équipements si nécessaire if (_currentStep == PreparationStep.preparation || (_currentStep == PreparationStep.unloadingReturn && _loadSimultaneously)) { await _updateEquipmentStatuses(updatedEquipment); } // NOUVEAU: Appeler la Cloud Function pour traiter la validation // et créer les alertes automatiquement try { DebugLog.info('[EventPreparationPage] Appel processEquipmentValidation'); final equipmentList = updatedEquipment.map((eq) { final equipment = _equipmentCache[eq.equipmentId]; return { 'equipmentId': eq.equipmentId, 'name': equipment?.name ?? 'Équipement inconnu', 'status': _determineEquipmentStatus(eq), 'quantity': _getQuantityForStep(eq), 'expectedQuantity': eq.quantity, 'isMissingAtPreparation': eq.isMissingAtPreparation, 'isMissingAtReturn': eq.isMissingAtReturn, }; }).toList(); final result = await FirebaseFunctions.instanceFor(region: 'us-central1') .httpsCallable('processEquipmentValidation') .call({ 'eventId': _currentEvent.id, 'equipmentList': equipmentList, 'validationType': validationType, }); final alertsCreated = result.data['alertsCreated'] ?? 0; if (alertsCreated > 0) { DebugLog.info('[EventPreparationPage] $alertsCreated alertes créées automatiquement'); } } catch (e) { DebugLog.error('[EventPreparationPage] Erreur appel processEquipmentValidation', e); // Ne pas bloquer la validation si les alertes échouent } // Recharger l'événement depuis le provider final eventProvider = context.read(); // Recharger la liste des événements pour rafraîchir les données final userId = context.read().uid; if (userId != null) { await eventProvider.loadUserEvents(userId, canViewAllEvents: true); } setState(() => _showSuccessAnimation = true); _animationController.forward(); await Future.delayed(const Duration(milliseconds: 500)); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( 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, ), ); } } finally { if (mounted) setState(() => _isValidating = false); } } Future _updateEquipmentStatuses(List equipment) async { for (var eq in equipment) { try { final equipmentData = _equipmentCache[eq.equipmentId]; if (equipmentData == null) continue; // Déterminer le nouveau statut EquipmentStatus newStatus; if (eq.isReturned) { newStatus = EquipmentStatus.available; } else if (eq.isPrepared || eq.isLoaded) { newStatus = EquipmentStatus.inUse; } else { continue; // Pas de changement } // Ne mettre à jour que les équipements non quantifiables if (!equipmentData.hasQuantity) { await _dataService.updateEquipmentStatusOnly( equipmentId: eq.equipmentId, status: equipmentStatusToString(newStatus), ); } // Gérer les stocks pour les consommables if (equipmentData.hasQuantity && eq.isReturned && eq.quantityAtReturn != null) { final currentAvailable = equipmentData.availableQuantity ?? 0; await _dataService.updateEquipmentStatusOnly( equipmentId: eq.equipmentId, availableQuantity: currentAvailable + eq.quantityAtReturn!, ); } } catch (e) { // Erreur silencieuse pour ne pas bloquer le processus } } } String _getSuccessMessage() { switch (_currentStep) { case PreparationStep.preparation: 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é'; } switch (_currentStep) { case PreparationStep.preparation: 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_; } } // ======================================================================== // 🆕 NOUVELLES MÉTHODES POUR LE SCAN QR ET LA SAISIE MANUELLE // ======================================================================== /// Ouvrir le scanner QR en mode multi-scan Future _openQRScanner() async { await showDialog( context: context, builder: (context) => QRCodeScannerDialog( multiScanMode: true, onCodeScanned: _handleScannedCode, ), ); } /// Traiter un code (scanné ou saisi manuellement) Future _handleScannedCode(String code) async { final result = await _qrCodeService.processCode( code: code.trim(), event: _currentEvent, step: _currentStep, equipmentCache: _equipmentCache, containerCache: _containerCache, validationState: _localValidationState, currentQuantities: _getCurrentQuantitiesMap(), ); if (result.success) { // ✅ Succès : mettre à jour l'état setState(() { if (result.updatedValidationState != null) { _localValidationState.addAll(result.updatedValidationState!); } if (result.updatedQuantities != null) { _updateQuantitiesMap(result.updatedQuantities!); } }); // 🔊 Jouer le feedback sonore et haptique await AudioFeedbackService.playFullFeedback(isSuccess: true); // Feedback visuel _showSuccessFeedback(result.message ?? 'Code traité avec succès'); } else if (result.codeNotFoundInEvent) { // 🔍 Code non trouvé dans l'événement → proposer de l'ajouter await _handleCodeNotFoundInEvent(code.trim()); } else { // ❌ Erreur (ex: quantité déjà atteinte, déjà coché) await AudioFeedbackService.playFullFeedback(isSuccess: false); _showErrorFeedback(result.message ?? 'Erreur lors du traitement'); } } /// Gérer un code non trouvé dans l'événement Future _handleCodeNotFoundInEvent(String code) async { // Afficher le dialog de confirmation final shouldSearch = await showDialog( context: context, builder: (context) => CodeNotFoundDialog(scannedCode: code), ); if (shouldSearch != true) return; // Afficher le dialog de chargement showDialog( context: context, barrierDismissible: false, builder: (context) => const AddEquipmentToEventDialog( state: AddEquipmentState.loading, ), ); try { // Identifier le type selon le préfixe final isContainer = code.startsWith('BOX_'); if (isContainer) { await _addContainerToEvent(code); } else { await _addEquipmentToEvent(code); } // 🔊 Bip de succès await AudioFeedbackService.playFullFeedback(isSuccess: true); } catch (e) { DebugLog.error('[EventPreparationPage] Error adding item to event', e); // Fermer le dialog de chargement et afficher l'erreur if (mounted) Navigator.of(context).pop(); await showDialog( context: context, builder: (context) => AddEquipmentToEventDialog( state: AddEquipmentState.error, errorMessage: e.toString(), ), ); // 🔊 Bip d'erreur await AudioFeedbackService.playFullFeedback(isSuccess: false); } } /// Ajouter un équipement à l'événement Future _addEquipmentToEvent(String equipmentId) async { // Rechercher l'équipement dans la base de données final equipmentProvider = context.read(); await equipmentProvider.ensureLoaded(); // Chercher d'abord dans le cache EquipmentModel? equipment = equipmentProvider.allEquipment .cast() .firstWhere( (eq) => eq?.id == equipmentId, orElse: () => null, ); // Si pas dans le cache, charger depuis Firestore if (equipment == null) { final equipmentService = EquipmentService(); equipment = await equipmentService.getEquipmentById(equipmentId); } if (equipment == null) { throw Exception('Équipement non trouvé dans la base de données'); } // Ajouter l'équipement à l'événement final newEventEquipment = EventEquipment( equipmentId: equipmentId, quantity: 1, ); final updatedEquipment = List.from(_currentEvent.assignedEquipment) ..add(newEventEquipment); await _dataService.updateEvent(_currentEvent.id, { 'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(), }); // Mettre à jour l'état local setState(() { _currentEvent = _currentEvent.copyWith( assignedEquipment: updatedEquipment, ); _equipmentCache[equipmentId] = equipment!; _localValidationState[equipmentId] = false; }); // Fermer le dialog de chargement et afficher le succès if (mounted) Navigator.of(context).pop(); await showDialog( context: context, builder: (context) => AddEquipmentToEventDialog( state: AddEquipmentState.success, itemName: equipment!.name, ), ); } /// Ajouter un container à l'événement Future _addContainerToEvent(String containerId) async { // Rechercher le container dans la base de données final containerProvider = context.read(); await containerProvider.ensureLoaded(); final container = await containerProvider.getContainerById(containerId); if (container == null) { throw Exception('Container non trouvé dans la base de données'); } // Ajouter le container à l'événement final updatedContainers = List.from(_currentEvent.assignedContainers) ..add(containerId); await _dataService.updateEvent(_currentEvent.id, { 'assignedContainers': updatedContainers, }); // Mettre à jour l'état local setState(() { _currentEvent = _currentEvent.copyWith( assignedContainers: updatedContainers, ); _containerCache[containerId] = container; }); // Fermer le dialog de chargement et afficher le succès if (mounted) Navigator.of(context).pop(); await showDialog( context: context, builder: (context) => AddEquipmentToEventDialog( state: AddEquipmentState.success, itemName: 'Container ${container.name}', ), ); } /// Traiter la saisie manuelle d'un code Future _handleManualCodeEntry(String code) async { if (code.trim().isEmpty) return; await _handleScannedCode(code.trim()); // Effacer le champ après traitement _manualCodeController.clear(); } /// Obtenir les quantités actuelles selon l'étape Map _getCurrentQuantitiesMap() { switch (_currentStep) { case PreparationStep.preparation: return _quantitiesAtPreparation; case PreparationStep.loadingOutbound: return _quantitiesAtLoading; case PreparationStep.unloadingReturn: return _quantitiesAtUnloading; case PreparationStep.return_: return _quantitiesAtReturn; } } /// Mettre à jour les quantités selon l'étape void _updateQuantitiesMap(Map quantities) { switch (_currentStep) { case PreparationStep.preparation: _quantitiesAtPreparation.addAll(quantities); break; case PreparationStep.loadingOutbound: _quantitiesAtLoading.addAll(quantities); break; case PreparationStep.unloadingReturn: _quantitiesAtUnloading.addAll(quantities); break; case PreparationStep.return_: _quantitiesAtReturn.addAll(quantities); break; } } /// Obtenir la quantité requise selon l'étape (nouvelle logique) int _getTargetQuantity(EventEquipment eventEquipment) { switch (_currentStep) { case PreparationStep.preparation: return eventEquipment.quantity; // Quantité initiale case PreparationStep.loadingOutbound: return eventEquipment.quantityAtPreparation ?? eventEquipment.quantity; case PreparationStep.unloadingReturn: return eventEquipment.quantityAtLoading ?? eventEquipment.quantityAtPreparation ?? eventEquipment.quantity; case PreparationStep.return_: return eventEquipment.quantityAtUnloading ?? eventEquipment.quantityAtLoading ?? eventEquipment.quantityAtPreparation ?? eventEquipment.quantity; } } /// Afficher un message de succès void _showSuccessFeedback(String message) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Icons.check_circle, color: Colors.white), const SizedBox(width: 12), Expanded(child: Text(message)), ], ), backgroundColor: Colors.green, duration: const Duration(seconds: 2), ), ); } /// Afficher un message d'erreur void _showErrorFeedback(String message) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Icons.error, color: Colors.white), const SizedBox(width: 12), Expanded(child: Text(message)), ], ), backgroundColor: Colors.orange, duration: const Duration(seconds: 3), ), ); } // ======================================================================== // FIN DES NOUVELLES MÉTHODES // ======================================================================== Future _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 _confirmCurrentState(); } else { // Afficher le dialog des manquants final missingEquipmentModels = missingEquipmentIds .map((id) => _equipmentCache[id]) .whereType() .toList(); final missingEventEquipment = _currentEvent.assignedEquipment .where((eq) => missingEquipmentIds.contains(eq.equipmentId)) .toList(); final action = await showDialog( context: context, builder: (context) => MissingEquipmentDialog( missingEquipment: missingEquipmentModels, eventEquipment: missingEventEquipment, isReturnMode: _currentStep == PreparationStep.return_ || _currentStep == PreparationStep.unloadingReturn, ), ); if (action == 'confirm_anyway') { // Confirmer malgré les manquants (enregistrer l'état actuel TEL QUEL) await _confirmCurrentState(); } else if (action == 'mark_as_validated') { // Marquer les manquants comme validés localement for (var equipmentId in missingEquipmentIds) { _localValidationState[equipmentId] = true; } setState(() {}); // Puis confirmer await _confirmCurrentState(); } // Si 'return_to_list', ne rien faire } } /// Valider tous les enfants d'un container void _validateAllContainerChildren(String containerId) { final container = _containerCache[containerId]; if (container == null) return; setState(() { for (final equipmentId in container.equipmentIds) { if (_equipmentCache.containsKey(equipmentId)) { _localValidationState[equipmentId] = true; } } }); } /// Mettre à jour la quantité d'un équipement à l'étape actuelle void _updateEquipmentQuantity(String equipmentId, int newQuantity) { setState(() { switch (_currentStep) { case PreparationStep.preparation: _quantitiesAtPreparation[equipmentId] = newQuantity; break; case PreparationStep.loadingOutbound: _quantitiesAtLoading[equipmentId] = newQuantity; break; case PreparationStep.unloadingReturn: _quantitiesAtUnloading[equipmentId] = newQuantity; break; case PreparationStep.return_: _quantitiesAtReturn[equipmentId] = newQuantity; break; } }); } /// Vérifier si un équipement était manquant à l'étape précédente bool _wasMissingAtPreviousStep(String equipmentId) { final eq = _currentEvent.assignedEquipment.firstWhere( (e) => e.equipmentId == equipmentId, orElse: () => EventEquipment(equipmentId: equipmentId), ); switch (_currentStep) { case PreparationStep.preparation: return false; // Pas d'étape avant case PreparationStep.loadingOutbound: return eq.isMissingAtPreparation; case PreparationStep.unloadingReturn: return eq.isMissingAtLoading; case PreparationStep.return_: return eq.isMissingAtUnloading; } } /// Afficher pop-up de confirmation si l'équipement était manquant avant Future _confirmValidationIfWasMissingBefore(String equipmentId) async { if (!_wasMissingAtPreviousStep(equipmentId)) { return true; // Pas de problème, continuer } final equipment = _equipmentCache[equipmentId]; final result = await showDialog( context: context, builder: (context) => AlertDialog( title: const Row( children: [ Icon(Icons.warning_amber, color: Colors.orange), SizedBox(width: 8), Text('Confirmation'), ], ), content: Text( 'L\'équipement "${equipment?.name ?? equipmentId}" était manquant à l\'étape précédente.\n\n' 'Êtes-vous sûr de le marquer comme présent maintenant ?', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: Colors.orange, ), child: const Text('Confirmer'), ), ], ), ); return result ?? false; } /// Détermine le statut d'un équipement selon l'étape actuelle String _determineEquipmentStatus(EventEquipment eq) { // Vérifier d'abord si l'équipement est perdu (LOST) if (_shouldMarkAsLost(eq)) { return 'LOST'; } // Vérifier si manquant à l'étape actuelle if (_isMissingAtCurrentStep(eq)) { return 'MISSING'; } // Vérifier les quantités final currentQty = _getQuantityForStep(eq); if (currentQty != null && currentQty < eq.quantity) { return 'QUANTITY_MISMATCH'; } return 'AVAILABLE'; } /// Vérifie si un équipement doit être marqué comme LOST bool _shouldMarkAsLost(EventEquipment eq) { // Seulement aux étapes de retour if (_currentStep != PreparationStep.return_ && !(_currentStep == PreparationStep.unloadingReturn && _loadSimultaneously)) { return false; } // Si manquant maintenant mais PAS manquant à la préparation = LOST return eq.isMissingAtReturn && !eq.isMissingAtPreparation; } /// Vérifie si un équipement est manquant à l'étape actuelle bool _isMissingAtCurrentStep(EventEquipment eq) { switch (_currentStep) { case PreparationStep.preparation: return eq.isMissingAtPreparation; case PreparationStep.loadingOutbound: return eq.isMissingAtLoading; case PreparationStep.unloadingReturn: return eq.isMissingAtUnloading; case PreparationStep.return_: return eq.isMissingAtReturn; } } /// Récupère la quantité pour l'étape actuelle int? _getQuantityForStep(EventEquipment eq) { switch (_currentStep) { case PreparationStep.preparation: return eq.quantityAtPreparation; case PreparationStep.loadingOutbound: return eq.quantityAtLoading; case PreparationStep.unloadingReturn: return eq.quantityAtUnloading; case PreparationStep.return_: return eq.quantityAtReturn; } } @override Widget build(BuildContext context) { final allValidated = _isStepCompleted(); final stepTitle = _getStepTitle(); return Scaffold( appBar: AppBar( title: Text(stepTitle), backgroundColor: AppColors.bleuFonce, ), body: Stack( children: [ _isLoading ? const Center(child: CircularProgressIndicator()) : Column( children: [ Container( 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( 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), // Checkbox "charger en même temps" (uniquement pour préparation ou chargement retour) 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, ), // 🆕 Champ de saisie manuelle de code const SizedBox(height: 16), TextField( controller: _manualCodeController, focusNode: _manualCodeFocusNode, decoration: InputDecoration( labelText: 'Saisie manuelle d\'un code', hintText: 'Entrez un ID d\'équipement ou container', prefixIcon: const Icon(Icons.keyboard, color: AppColors.bleuFonce), suffixIcon: _manualCodeController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { _manualCodeController.clear(); setState(() {}); }, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: AppColors.bleuFonce, width: 2), ), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), onSubmitted: _handleManualCodeEntry, onChanged: (value) => setState(() {}), textInputAction: TextInputAction.done, ), // 🆕 Bouton Scanner QR Code const SizedBox(height: 12), ElevatedButton.icon( onPressed: _openQRScanner, icon: const Icon(Icons.qr_code_scanner), label: const Text('Scanner QR Code'), style: ElevatedButton.styleFrom( backgroundColor: Colors.blue[700], padding: const EdgeInsets.symmetric(vertical: 12), ), ), const SizedBox(height: 8), ElevatedButton.icon( onPressed: allValidated ? null : _validateAllAndConfirm, 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( padding: const EdgeInsets.all(16), children: _buildChecklistItems(), ), ), ], ), if (_showSuccessAnimation) Center( child: ScaleTransition( scale: _animationController, child: Container( padding: const EdgeInsets.all(32), decoration: const BoxDecoration( color: Colors.green, shape: BoxShape.circle, ), child: const Icon( Icons.check, size: 64, color: Colors.white, ), ), ), ), ], ), bottomNavigationBar: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), 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), ), ), child: _isValidating ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Text( 'Confirmer ${_getStepTitle().toLowerCase()}', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ), ); } /// Construit la liste des items de checklist en groupant par containers List _buildChecklistItems() { final List items = []; // Set pour tracker les équipements déjà affichés dans un container final Set equipmentIdsInContainers = {}; // Map des EventEquipment par ID pour accès rapide final Map eventEquipmentsMap = { for (var eq in _currentEvent.assignedEquipment) eq.equipmentId: eq, }; // 1. Afficher les containers avec leurs enfants for (final containerId in _currentEvent.assignedContainers) { final container = _containerCache[containerId]; if (container == null) continue; // Récupérer les équipements enfants de ce container final List childEquipments = []; for (final equipmentId in container.equipmentIds) { final equipment = _equipmentCache[equipmentId]; if (equipment != null && eventEquipmentsMap.containsKey(equipmentId)) { childEquipments.add(equipment); equipmentIdsInContainers.add(equipmentId); } } if (childEquipments.isEmpty) continue; // Vérifier si tous les enfants sont validés final allChildrenValidated = childEquipments.every( (eq) => _localValidationState[eq.id] ?? false, ); // Map des états de validation des enfants final Map childValidationStates = { for (var eq in childEquipments) eq.id: _localValidationState[eq.id] ?? false, }; // Map des enfants manquants à l'étape précédente final Map wasMissingBeforeMap = { for (var eq in childEquipments) eq.id: _wasMissingAtPreviousStep(eq.id), }; items.add( ContainerChecklistItem( container: container, childEquipments: childEquipments, eventEquipmentsMap: eventEquipmentsMap, step: _getChecklistStep(), isValidated: allChildrenValidated, childValidationStates: childValidationStates, onToggleContainer: () => _validateAllContainerChildren(containerId), onToggleChild: (equipmentId) => _toggleEquipmentValidation(equipmentId), onQuantityChanged: (equipmentId, qty) => _updateEquipmentQuantity(equipmentId, qty), wasMissingBeforeMap: wasMissingBeforeMap, ), ); } // 2. Afficher les équipements standalone (pas dans un container) for (final eventEquipment in _currentEvent.assignedEquipment) { // Skip si déjà affiché dans un container if (equipmentIdsInContainers.contains(eventEquipment.equipmentId)) { continue; } final equipment = _equipmentCache[eventEquipment.equipmentId]; if (equipment == null) continue; items.add( EquipmentChecklistItem( equipment: equipment, eventEquipment: eventEquipment, step: _getChecklistStep(), isValidated: _localValidationState[equipment.id] ?? false, onToggle: () => _toggleEquipmentValidation(equipment.id), onQuantityChanged: (qty) => _updateEquipmentQuantity(equipment.id, qty), isChild: false, // Équipement standalone (pas indenté) wasMissingBefore: _wasMissingAtPreviousStep(equipment.id), ), ); } return items; } }