feat: ajout de la gestion de la préparation d'un événement avec page permettant de le gérer
This commit is contained in:
@@ -1,383 +1,701 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.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/providers/event_provider.dart';
|
||||
import 'package:em2rp/models/container_model.dart';
|
||||
import 'package:em2rp/providers/equipment_provider.dart';
|
||||
import 'package:em2rp/providers/local_user_provider.dart';
|
||||
import 'package:em2rp/providers/container_provider.dart';
|
||||
import 'package:em2rp/services/event_preparation_service.dart';
|
||||
import 'package:em2rp/services/event_preparation_service_extended.dart';
|
||||
import 'package:em2rp/views/widgets/equipment/equipment_checklist_item.dart' show EquipmentChecklistItem, ChecklistStep;
|
||||
import 'package:em2rp/views/widgets/equipment/missing_equipment_dialog.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
||||
import 'package:em2rp/views/widgets/event/equipment_checklist_item.dart';
|
||||
import 'package:em2rp/views/widgets/event/missing_equipment_dialog.dart';
|
||||
import 'package:em2rp/views/widgets/event/preparation_success_dialog.dart';
|
||||
|
||||
/// Type d'étape de préparation
|
||||
enum PreparationStep {
|
||||
preparation, // Préparation dépôt
|
||||
loadingOutbound, // Chargement aller
|
||||
unloadingReturn, // Chargement retour (déchargement)
|
||||
return_, // Retour dépôt
|
||||
}
|
||||
|
||||
/// Page de préparation ou de retour d'un événement
|
||||
class EventPreparationPage extends StatefulWidget {
|
||||
final String eventId;
|
||||
final EventModel initialEvent;
|
||||
|
||||
const EventPreparationPage({
|
||||
super.key,
|
||||
required this.eventId,
|
||||
required this.initialEvent,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EventPreparationPage> createState() => _EventPreparationPageState();
|
||||
}
|
||||
|
||||
class _EventPreparationPageState extends State<EventPreparationPage> {
|
||||
class _EventPreparationPageState extends State<EventPreparationPage> with SingleTickerProviderStateMixin {
|
||||
final EventPreparationService _preparationService = EventPreparationService();
|
||||
EventModel? _event;
|
||||
Map<String, EquipmentModel> _equipmentMap = {};
|
||||
Map<String, int> _returnedQuantities = {}; // Pour les quantités retournées (consommables)
|
||||
final EventPreparationServiceExtended _extendedService = EventPreparationServiceExtended();
|
||||
late AnimationController _animationController;
|
||||
|
||||
Map<String, EquipmentModel> _equipmentCache = {};
|
||||
Map<String, ContainerModel> _containerCache = {};
|
||||
Map<String, int> _returnedQuantities = {};
|
||||
|
||||
// État local des validations (non sauvegardé jusqu'à la validation finale)
|
||||
Map<String, bool> _localValidationState = {};
|
||||
|
||||
bool _isLoading = true;
|
||||
bool _isSaving = false;
|
||||
bool _isValidating = false;
|
||||
bool _showSuccessAnimation = false;
|
||||
bool _loadSimultaneously = false; // Checkbox "charger en même temps"
|
||||
|
||||
// Mode déterminé automatiquement
|
||||
bool get _isReturnMode {
|
||||
if (_event == null) return false;
|
||||
// Mode retour si préparation complétée et retour pas encore complété
|
||||
return _event!.preparationStatus == PreparationStatus.completed ||
|
||||
_event!.preparationStatus == PreparationStatus.completedWithMissing;
|
||||
// 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;
|
||||
}
|
||||
|
||||
String get _pageTitle => _isReturnMode ? 'Retour matériel' : 'Préparation matériel';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadEventAndEquipment();
|
||||
_currentEvent = widget.initialEvent;
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
);
|
||||
|
||||
// Vérification de sécurité : bloquer l'accès si toutes les étapes sont complétées
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_isCurrentStepCompleted()) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Cette étape est déjà terminée'),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
_loadEquipmentAndContainers();
|
||||
}
|
||||
|
||||
Future<void> _loadEventAndEquipment() async {
|
||||
/// Vérifie si l'étape actuelle est déjà complétée
|
||||
bool _isCurrentStepCompleted() {
|
||||
switch (_currentStep) {
|
||||
case PreparationStep.preparation:
|
||||
return (_currentEvent.preparationStatus ?? PreparationStatus.notStarted) == PreparationStatus.completed;
|
||||
case PreparationStep.loadingOutbound:
|
||||
return (_currentEvent.loadingStatus ?? LoadingStatus.notStarted) == LoadingStatus.completed;
|
||||
case PreparationStep.unloadingReturn:
|
||||
return (_currentEvent.unloadingStatus ?? UnloadingStatus.notStarted) == UnloadingStatus.completed;
|
||||
case PreparationStep.return_:
|
||||
return (_currentEvent.returnStatus ?? ReturnStatus.notStarted) == ReturnStatus.completed;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Recharger l'événement depuis Firestore
|
||||
Future<void> _reloadEvent() async {
|
||||
try {
|
||||
// Charger l'événement
|
||||
final eventProvider = context.read<EventProvider>();
|
||||
final event = await eventProvider.getEvent(widget.eventId);
|
||||
final doc = await FirebaseFirestore.instance
|
||||
.collection('events')
|
||||
.doc(_currentEvent.id)
|
||||
.get();
|
||||
|
||||
if (event == null) {
|
||||
throw Exception('Événement non trouvé');
|
||||
if (doc.exists) {
|
||||
setState(() {
|
||||
_currentEvent = EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
print('[EventPreparationPage] Error reloading event: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Charger tous les équipements assignés
|
||||
Future<void> _loadEquipmentAndContainers() async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
final equipmentProvider = context.read<EquipmentProvider>();
|
||||
final Map<String, EquipmentModel> equipmentMap = {};
|
||||
final containerProvider = context.read<ContainerProvider>();
|
||||
|
||||
for (var assignedEq in event.assignedEquipment) {
|
||||
final equipment = await equipmentProvider.getEquipmentById(assignedEq.equipmentId);
|
||||
if (equipment != null) {
|
||||
equipmentMap[assignedEq.equipmentId] = equipment;
|
||||
final equipment = await equipmentProvider.equipmentStream.first;
|
||||
final containers = await containerProvider.containersStream.first;
|
||||
|
||||
// Initialiser les quantités retournées pour les consommables
|
||||
if (_isReturnMode &&
|
||||
(equipment.category == EquipmentCategory.consumable ||
|
||||
equipment.category == EquipmentCategory.cable)) {
|
||||
_returnedQuantities[assignedEq.equipmentId] = assignedEq.returnedQuantity ?? assignedEq.quantity;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_event = event;
|
||||
_equipmentMap = equipmentMap;
|
||||
_isLoading = false;
|
||||
});
|
||||
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<void> _validateAll() async {
|
||||
setState(() => _isValidating = true);
|
||||
|
||||
try {
|
||||
// Si "tout valider" est cliqué, marquer tout comme validé localement
|
||||
for (var equipmentId in _localValidationState.keys) {
|
||||
_localValidationState[equipmentId] = true;
|
||||
}
|
||||
|
||||
// Préparer la liste des équipements avec leur nouvel état
|
||||
final updatedEquipment = _currentEvent.assignedEquipment.map((eq) {
|
||||
final isValidated = _localValidationState[eq.equipmentId] ?? false;
|
||||
|
||||
switch (_currentStep) {
|
||||
case PreparationStep.preparation:
|
||||
if (_loadSimultaneously) {
|
||||
return eq.copyWith(isPrepared: isValidated, isLoaded: isValidated);
|
||||
}
|
||||
return eq.copyWith(isPrepared: isValidated);
|
||||
|
||||
case PreparationStep.loadingOutbound:
|
||||
return eq.copyWith(isLoaded: isValidated);
|
||||
|
||||
case PreparationStep.unloadingReturn:
|
||||
if (_loadSimultaneously) {
|
||||
final returnedQty = _returnedQuantities[eq.equipmentId] ?? eq.quantity;
|
||||
return eq.copyWith(isUnloaded: isValidated, isReturned: isValidated, returnedQuantity: returnedQty);
|
||||
}
|
||||
return eq.copyWith(isUnloaded: isValidated);
|
||||
|
||||
case PreparationStep.return_:
|
||||
final returnedQty = _returnedQuantities[eq.equipmentId] ?? eq.quantity;
|
||||
return eq.copyWith(isReturned: isValidated, returnedQuantity: returnedQty);
|
||||
}
|
||||
}).toList();
|
||||
|
||||
// Mettre à jour Firestore selon l'étape
|
||||
final updateData = <String, dynamic>{
|
||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||
};
|
||||
|
||||
// Ajouter les statuts selon l'étape et la checkbox
|
||||
switch (_currentStep) {
|
||||
case PreparationStep.preparation:
|
||||
updateData['preparationStatus'] = preparationStatusToString(PreparationStatus.completed);
|
||||
if (_loadSimultaneously) {
|
||||
updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.completed);
|
||||
}
|
||||
break;
|
||||
|
||||
case PreparationStep.loadingOutbound:
|
||||
updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.completed);
|
||||
break;
|
||||
|
||||
case PreparationStep.unloadingReturn:
|
||||
updateData['unloadingStatus'] = unloadingStatusToString(UnloadingStatus.completed);
|
||||
if (_loadSimultaneously) {
|
||||
updateData['returnStatus'] = returnStatusToString(ReturnStatus.completed);
|
||||
}
|
||||
break;
|
||||
|
||||
case PreparationStep.return_:
|
||||
updateData['returnStatus'] = returnStatusToString(ReturnStatus.completed);
|
||||
break;
|
||||
}
|
||||
|
||||
// Sauvegarder dans Firestore
|
||||
await FirebaseFirestore.instance
|
||||
.collection('events')
|
||||
.doc(_currentEvent.id)
|
||||
.update(updateData);
|
||||
|
||||
// Mettre à jour les statuts des équipements si nécessaire
|
||||
if (_currentStep == PreparationStep.preparation ||
|
||||
(_currentStep == PreparationStep.unloadingReturn && _loadSimultaneously)) {
|
||||
await _updateEquipmentStatuses(updatedEquipment);
|
||||
}
|
||||
|
||||
setState(() => _showSuccessAnimation = true);
|
||||
_animationController.forward();
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erreur: $e'),
|
||||
content: Text(_getSuccessMessage()),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erreur : $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleEquipmentValidation(String equipmentId, bool isValidated) async {
|
||||
try {
|
||||
if (_isReturnMode) {
|
||||
if (isValidated) {
|
||||
final returnedQty = _returnedQuantities[equipmentId];
|
||||
await _preparationService.validateEquipmentReturn(
|
||||
widget.eventId,
|
||||
equipmentId,
|
||||
returnedQuantity: returnedQty,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await _preparationService.validateEquipmentPreparation(widget.eventId, equipmentId);
|
||||
}
|
||||
|
||||
// Recharger l'événement
|
||||
await _loadEventAndEquipment();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Erreur: $e'), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _validateAllQuickly() async {
|
||||
setState(() => _isSaving = true);
|
||||
|
||||
try {
|
||||
if (_isReturnMode) {
|
||||
await _preparationService.validateAllReturn(widget.eventId, _returnedQuantities);
|
||||
} else {
|
||||
await _preparationService.validateAllPreparation(widget.eventId);
|
||||
}
|
||||
|
||||
// Afficher le dialog de succès avec animation
|
||||
if (mounted) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => PreparationSuccessDialog(isReturnMode: _isReturnMode),
|
||||
);
|
||||
|
||||
// Retour à la page précédente
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Erreur: $e'), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setState(() => _isSaving = false);
|
||||
if (mounted) setState(() => _isValidating = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _validatePreparation() async {
|
||||
if (_event == null) return;
|
||||
Future<void> _updateEquipmentStatuses(List<EventEquipment> equipment) async {
|
||||
for (var eq in equipment) {
|
||||
try {
|
||||
final doc = await FirebaseFirestore.instance
|
||||
.collection('equipments')
|
||||
.doc(eq.equipmentId)
|
||||
.get();
|
||||
|
||||
// Vérifier quels équipements ne sont pas validés
|
||||
final missingEquipment = <EquipmentModel>[];
|
||||
final missingIds = <String>[];
|
||||
if (doc.exists) {
|
||||
final equipmentData = EquipmentModel.fromMap(
|
||||
doc.data() as Map<String, dynamic>,
|
||||
doc.id,
|
||||
);
|
||||
|
||||
for (var assignedEq in _event!.assignedEquipment) {
|
||||
final isValidated = _isReturnMode ? assignedEq.isReturned : assignedEq.isPrepared;
|
||||
// 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
|
||||
}
|
||||
|
||||
if (!isValidated) {
|
||||
final equipment = _equipmentMap[assignedEq.equipmentId];
|
||||
if (equipment != null) {
|
||||
missingEquipment.add(equipment);
|
||||
missingIds.add(assignedEq.equipmentId);
|
||||
// Ne mettre à jour que les équipements non quantifiables
|
||||
if (!equipmentData.hasQuantity) {
|
||||
await FirebaseFirestore.instance
|
||||
.collection('equipments')
|
||||
.doc(eq.equipmentId)
|
||||
.update({
|
||||
'status': equipmentStatusToString(newStatus),
|
||||
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||
});
|
||||
}
|
||||
|
||||
// Gérer les stocks pour les consommables
|
||||
if (equipmentData.hasQuantity && eq.isReturned && eq.returnedQuantity != null) {
|
||||
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
||||
await FirebaseFirestore.instance
|
||||
.collection('equipments')
|
||||
.doc(eq.equipmentId)
|
||||
.update({
|
||||
'availableQuantity': currentAvailable + eq.returnedQuantity!,
|
||||
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error updating equipment status for ${eq.equipmentId}: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si tout est validé, on finalise directement
|
||||
if (missingEquipment.isEmpty) {
|
||||
await _validateAllQuickly();
|
||||
return;
|
||||
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é';
|
||||
}
|
||||
|
||||
// Sinon, afficher le dialog des manquants
|
||||
if (mounted) {
|
||||
final result = await showDialog<String>(
|
||||
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 _validateAll();
|
||||
} else {
|
||||
// Afficher le dialog des manquants
|
||||
final missingEquipmentModels = missingEquipmentIds
|
||||
.map((id) => _equipmentCache[id])
|
||||
.whereType<EquipmentModel>()
|
||||
.toList();
|
||||
|
||||
final missingEventEquipment = _currentEvent.assignedEquipment
|
||||
.where((eq) => missingEquipmentIds.contains(eq.equipmentId))
|
||||
.toList();
|
||||
|
||||
final action = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => MissingEquipmentDialog(
|
||||
missingEquipments: missingEquipment,
|
||||
eventId: widget.eventId,
|
||||
isReturnMode: _isReturnMode,
|
||||
missingEquipment: missingEquipmentModels,
|
||||
eventEquipment: missingEventEquipment,
|
||||
isReturnMode: _currentStep == PreparationStep.return_ ||
|
||||
_currentStep == PreparationStep.unloadingReturn,
|
||||
),
|
||||
);
|
||||
|
||||
if (result == 'confirm_with_missing') {
|
||||
setState(() => _isSaving = true);
|
||||
try {
|
||||
if (_isReturnMode) {
|
||||
await _preparationService.completeReturnWithMissing(widget.eventId, missingIds);
|
||||
} else {
|
||||
await _preparationService.completePreparationWithMissing(widget.eventId, missingIds);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => PreparationSuccessDialog(isReturnMode: _isReturnMode),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Erreur: $e'), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setState(() => _isSaving = false);
|
||||
}
|
||||
} else if (result == 'validate_missing') {
|
||||
// Valider tous les manquants
|
||||
setState(() => _isSaving = true);
|
||||
try {
|
||||
for (var equipmentId in missingIds) {
|
||||
await _toggleEquipmentValidation(equipmentId, true);
|
||||
}
|
||||
await _validateAllQuickly();
|
||||
} finally {
|
||||
setState(() => _isSaving = false);
|
||||
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 userProvider = context.watch<LocalUserProvider>();
|
||||
final userId = userProvider.uid;
|
||||
final hasManagePermission = userProvider.hasPermission('manage_events');
|
||||
|
||||
// Vérifier si l'utilisateur fait partie de l'équipe
|
||||
final isInWorkforce = _event?.workforce.any((ref) => ref.id == userId) ?? false;
|
||||
final hasPermission = hasManagePermission || isInWorkforce;
|
||||
|
||||
if (!hasPermission) {
|
||||
return Scaffold(
|
||||
appBar: const CustomAppBar(title: 'Accès refusé'),
|
||||
body: const Center(
|
||||
child: Text('Vous n\'avez pas les permissions pour accéder à cette page.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
final allValidated = _isStepCompleted();
|
||||
final stepTitle = _getStepTitle();
|
||||
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(title: _pageTitle),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _event == null
|
||||
? const Center(child: Text('Événement introuvable'))
|
||||
appBar: AppBar(
|
||||
title: Text(stepTitle),
|
||||
backgroundColor: AppColors.bleuFonce,
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
_isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Column(
|
||||
children: [
|
||||
// En-tête avec info de l'événement
|
||||
_buildEventHeader(),
|
||||
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),
|
||||
|
||||
// Bouton "Tout confirmer"
|
||||
_buildQuickValidateButton(),
|
||||
// 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,
|
||||
),
|
||||
|
||||
// Liste des équipements
|
||||
Expanded(child: _buildEquipmentList()),
|
||||
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];
|
||||
|
||||
// Bouton de validation final
|
||||
_buildValidateButton(),
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.rouge.withOpacity(0.1),
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_event!.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
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),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${_event!.assignedEquipment.length} équipement(s) assigné(s)',
|
||||
style: TextStyle(color: Colors.grey.shade700),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickValidateButton() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _isSaving ? null : _validateAllQuickly,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.check_circle, color: Colors.white),
|
||||
label: Text(
|
||||
_isReturnMode ? 'Tout confirmer comme retourné' : 'Tout confirmer comme préparé',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEquipmentList() {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _event!.assignedEquipment.length,
|
||||
itemBuilder: (context, index) {
|
||||
final assignedEq = _event!.assignedEquipment[index];
|
||||
final equipment = _equipmentMap[assignedEq.equipmentId];
|
||||
|
||||
if (equipment == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final isValidated = _isReturnMode ? assignedEq.isReturned : assignedEq.isPrepared;
|
||||
|
||||
return EquipmentChecklistItem(
|
||||
equipment: equipment,
|
||||
isValidated: isValidated,
|
||||
onValidate: (value) => _toggleEquipmentValidation(assignedEq.equipmentId, value),
|
||||
isReturnMode: _isReturnMode,
|
||||
quantity: assignedEq.quantity,
|
||||
returnedQuantity: _returnedQuantities[assignedEq.equipmentId],
|
||||
onReturnedQuantityChanged: _isReturnMode
|
||||
? (value) {
|
||||
setState(() {
|
||||
_returnedQuantities[assignedEq.equipmentId] = value;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildValidateButton() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: _isSaving ? null : _validatePreparation,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.rouge,
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
),
|
||||
child: _isSaving
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: Text(
|
||||
_isReturnMode ? 'Finaliser le retour' : 'Finaliser la préparation',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user