import 'package:flutter/material.dart'; import 'package:provider/provider.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/api_service.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'; /// 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; Map _equipmentCache = {}; Map _containerCache = {}; Map _returnedQuantities = {}; // État local des validations (non sauvegardé jusqu'à la validation finale) Map _localValidationState = {}; 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; // 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()); _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(); super.dispose(); } Future _loadEquipmentAndContainers() async { setState(() => _isLoading = true); try { final equipmentProvider = context.read(); final containerProvider = context.read(); // S'assurer que les équipements sont chargés await equipmentProvider.ensureLoaded(); await containerProvider.ensureLoaded(); final equipment = await equipmentProvider.equipmentStream.first; final containers = await containerProvider.containersStream.first; for (var eq in _currentEvent.assignedEquipment) { final equipmentItem = equipment.firstWhere( (e) => e.id == eq.equipmentId, orElse: () => EquipmentModel( 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; } } for (var containerId in _currentEvent.assignedContainers) { final container = containers.firstWhere( (c) => c.id == containerId, 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) { print('[EventPreparationPage] Error: $e'); } finally { 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 _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 = { '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 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); } // 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.returnedQuantity != null) { final currentAvailable = equipmentData.availableQuantity ?? 0; await _dataService.updateEquipmentStatusOnly( equipmentId: eq.equipmentId, availableQuantity: currentAvailable + eq.returnedQuantity!, ); } } 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_; } } 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 _validateAll(); } 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 await _validateAll(); } 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 _validateAll(); } // Si 'return_to_list', ne rien faire } } @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, ), const SizedBox(height: 8), 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]; if (equipment == null) { 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( 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, ), ), ), ), ); } }