Files
EM2_ERP/em2rp/lib/views/event_preparation_page.dart
ElPoyo 60d0e1c6c4 feat: Refonte de la checklist de préparation avec gestion des manquants et des containers
Cette mise à jour refond entièrement l'interface et la logique de la checklist de préparation d'événement. Elle introduit la notion d'équipements "manquants", une gestion visuelle des containers et de leur contenu, et une logique plus fine pour le suivi des quantités et des statuts à chaque étape.

**Features et Améliorations :**

-   **Gestion des Équipements Manquants :**
    -   Le modèle `EventEquipment` a été enrichi pour tracer si un équipement est manquant à chaque étape (`isMissingAtPreparation`, `isMissingAtLoading`, etc.).
    -   Un équipement non validé lors de la confirmation d'une étape est désormais marqué comme "manquant" pour les étapes suivantes.
    -   Les équipements qui étaient manquants à l'étape précédente sont maintenant visuellement mis en évidence avec une bordure et une icône orange, et une confirmation est demandée pour les valider.

-   **Refonte de la Checklist (UI/UX) :**
    -   **Groupement par Container :** La checklist affiche désormais les containers comme des en-têtes de groupe. Les équipements qu'ils contiennent sont listés en dessous, avec une indentation visuelle.
    -   **Validation Groupée :** Il est possible de valider tous les équipements d'un container en un seul clic sur l'en-tête du container.
    -   **Nouveau Widget `ContainerChecklistItem` :** Créé pour afficher un container et ses équipements enfants dans la checklist.
    -   **Refonte de `EquipmentChecklistItem` :** Le widget a été entièrement revu pour un design plus clair, une meilleure gestion des états (validé, manquant), et un affichage compact pour les équipements enfants.

-   **Logique de Suivi Améliorée :**
    -   **Quantités par Étape :** Le modèle `EventEquipment` et l'interface de préparation permettent maintenant de suivre les quantités réelles à chaque étape (`quantityAtPreparation`, `quantityAtLoading`, etc.), au lieu d'une seule quantité de retour.
    -   **Marquage Automatique des "Perdus" :** À l'étape finale du retour, un équipement qui était présent au départ mais qui est maintenant manquant sera automatiquement marqué avec le statut "lost" dans la base de données.
    -   **Flux de Validation :** Le processus de confirmation distingue désormais la validation de tous les équipements et la confirmation de l'état actuel (y compris les manquants).

-   **Export ICS Enrichi :**
    -   L'export ICS inclut désormais les noms résolus des utilisateurs (main d'œuvre) pour plus de clarté, en plus des détails de l'événement.
    -   Le contenu généré mentionne la version de l'application.
2026-01-15 12:05:37 +01:00

953 lines
34 KiB
Dart

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/views/widgets/equipment/equipment_checklist_item.dart' show EquipmentChecklistItem, ChecklistStep;
import 'package:em2rp/views/widgets/equipment/container_checklist_item.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<EventPreparationPage> createState() => _EventPreparationPageState();
}
class _EventPreparationPageState extends State<EventPreparationPage> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late final DataService _dataService;
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 = {};
// NOUVEAU : Gestion des quantités par étape
Map<String, int> _quantitiesAtPreparation = {};
Map<String, int> _quantitiesAtLoading = {};
Map<String, int> _quantitiesAtUnloading = {};
Map<String, int> _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;
// 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<void> _loadEquipmentAndContainers() async {
setState(() => _isLoading = true);
try {
final equipmentProvider = context.read<EquipmentProvider>();
final containerProvider = context.read<ContainerProvider>();
// 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.quantityAtReturn ?? 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) {
DebugLog.error('[EventPreparationPage] Error', e);
} finally {
setState(() => _isLoading = false);
}
}
/// Basculer l'état de validation d'un équipement (état local uniquement)
Future<void> _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<void> _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<void> _confirmCurrentState() async {
setState(() => _isValidating = true);
try {
// Déterminer les manquants = équipements NON validés
final Map<String, bool> 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();
// Si on est à la dernière étape (retour), vérifier les équipements LOST
if (_currentStep == PreparationStep.return_) {
await _checkAndMarkLostEquipment(updatedEquipment);
}
// 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 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<EventProvider>();
// Recharger la liste des événements pour rafraîchir les données
final userId = context.read<LocalUserProvider>().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<void> _updateEquipmentStatuses(List<EventEquipment> 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_;
}
}
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 _confirmCurrentState();
} 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,
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<bool> _confirmValidationIfWasMissingBefore(String equipmentId) async {
if (!_wasMissingAtPreviousStep(equipmentId)) {
return true; // Pas de problème, continuer
}
final equipment = _equipmentCache[equipmentId];
final result = await showDialog<bool>(
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;
}
/// Vérifier et marquer les équipements LOST (logique intelligente)
Future<void> _checkAndMarkLostEquipment(List<EventEquipment> updatedEquipment) async {
for (final eq in updatedEquipment) {
final isMissingNow = eq.isMissingAtReturn;
if (isMissingNow) {
// Vérifier si c'était manquant dès la préparation (étape 0)
final wasMissingAtPreparation = eq.isMissingAtPreparation;
if (!wasMissingAtPreparation) {
// Était présent au départ mais manquant maintenant = LOST
try {
await _dataService.updateEquipmentStatusOnly(
equipmentId: eq.equipmentId,
status: EquipmentStatus.lost.toString(),
);
DebugLog.info('[EventPreparationPage] Équipement ${eq.equipmentId} marqué comme LOST');
// TODO: Créer une alerte "Équipement perdu"
// await _createLostEquipmentAlert(eq.equipmentId);
} catch (e) {
DebugLog.error('[EventPreparationPage] Erreur marquage LOST ${eq.equipmentId}', e);
}
} else {
// Manquant dès le début = PAS lost, juste manquant
DebugLog.info('[EventPreparationPage] Équipement ${eq.equipmentId} manquant depuis le début (pas LOST)');
}
}
}
}
@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<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),
// 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 : _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<Color>(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<Widget> _buildChecklistItems() {
final List<Widget> items = [];
// Set pour tracker les équipements déjà affichés dans un container
final Set<String> equipmentIdsInContainers = {};
// Map des EventEquipment par ID pour accès rapide
final Map<String, EventEquipment> 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<EquipmentModel> 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<String, bool> childValidationStates = {
for (var eq in childEquipments) eq.id: _localValidationState[eq.id] ?? false,
};
// Map des enfants manquants à l'étape précédente
final Map<String, bool> 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;
}
}