Files
EM2_ERP/em2rp/lib/views/event_preparation_page.dart
ElPoyo b79791ff7a refactor: Ajout des sous-catégories et refonte de la gestion de l'appartenance
Cette mise à jour structurelle améliore la classification des équipements en introduisant la notion de sous-catégories et supprime la gestion directe de l'appartenance d'un équipement à une boîte (`parentBoxIds`). L'appartenance est désormais uniquement définie côté conteneur. Une nouvelle catégorie "Régie / Backline" est également ajoutée.

**Changements majeurs :**

-   **Suppression de `parentBoxIds` sur `EquipmentModel` :**
    -   Le champ `parentBoxIds` a été retiré du modèle de données `EquipmentModel` et de toutes les logiques associées (création, mise à jour, copie).
    -   La responsabilité de lier un équipement à un conteneur est désormais exclusivement gérée par le `ContainerModel` via sa liste `equipmentIds`.
    -   La logique de synchronisation complexe dans `EquipmentFormPage` qui mettait à jour les conteneurs lors de la modification d'un équipement a été entièrement supprimée, simplifiant considérablement le code.
    -   Le sélecteur de boîtes parentes (`ParentBoxesSelector`) a été retiré du formulaire d'équipement.

-   **Ajout des sous-catégories :**
    -   Un champ optionnel `subCategory` (String) a été ajouté au `EquipmentModel`.
    -   Le formulaire de création/modification d'équipement inclut désormais un nouveau champ "Sous-catégorie" avec autocomplétion.
    -   Ce champ est contextuel : il propose des suggestions basées sur les sous-catégories existantes pour la catégorie principale sélectionnée (ex: "Console", "Micro" pour la catégorie "Son").
    -   La sous-catégorie est maintenant affichée sur les fiches de détail des équipements et dans les listes de la page de gestion, améliorant la visibilité du classement.

**Nouvelle catégorie d'équipement :**

-   Une nouvelle catégorie `backline` ("Régie / Backline") a été ajoutée à `EquipmentCategory` avec une icône (`Icons.piano`) et une couleur associée.

**Refactorisation et nettoyage :**

-   Le `EquipmentProvider` et `EquipmentService` ont été mis à jour pour charger et filtrer les sous-catégories.
-   De nombreuses instanciations d'un `EquipmentModel` vide (`dummy`) à travers l'application ont été nettoyées pour retirer la référence à `parentBoxIds`.

-   **Version de l'application :**
    -   La version a été incrémentée à `1.0.4`.
2026-01-17 12:07:20 +01:00

1020 lines
36 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:cloud_functions/cloud_functions.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,
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();
// 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
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<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;
}
/// 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<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;
}
}