feat: implement equipment and container loading rollback functionality with corresponding backend cloud functions
This commit is contained in:
+162
-6
@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/providers/event_provider.dart';
|
||||
import 'package:em2rp/providers/local_user_provider.dart';
|
||||
import 'package:em2rp/views/event_preparation_page.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
|
||||
/// Boutons de préparation et retour d'événement
|
||||
@@ -52,16 +54,16 @@ class _EventPreparationButtonsState extends State<EventPreparationButtons> {
|
||||
IconData buttonIcon;
|
||||
bool isCompleted = false;
|
||||
|
||||
if (prep != PreparationStatus.completed) {
|
||||
if (prep != PreparationStatus.completed && prep != PreparationStatus.completedWithMissing) {
|
||||
buttonText = 'Préparation dépôt';
|
||||
buttonIcon = Icons.inventory_2;
|
||||
} else if (loading != LoadingStatus.completed) {
|
||||
} else if (loading != LoadingStatus.completed && loading != LoadingStatus.completedWithMissing) {
|
||||
buttonText = 'Chargement aller';
|
||||
buttonIcon = Icons.local_shipping;
|
||||
} else if (unloading != UnloadingStatus.completed) {
|
||||
} else if (unloading != UnloadingStatus.completed && unloading != UnloadingStatus.completedWithMissing) {
|
||||
buttonText = 'Chargement retour';
|
||||
buttonIcon = Icons.unarchive;
|
||||
} else if (returnStatus != ReturnStatus.completed) {
|
||||
} else if (returnStatus != ReturnStatus.completed && returnStatus != ReturnStatus.completedWithMissing) {
|
||||
buttonText = 'Retour dépôt';
|
||||
buttonIcon = Icons.assignment_return;
|
||||
} else {
|
||||
@@ -131,9 +133,9 @@ class _EventPreparationButtonsState extends State<EventPreparationButtons> {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.green, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Colors.green, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
@@ -147,9 +149,163 @@ class _EventPreparationButtonsState extends State<EventPreparationButtons> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Bouton de retour en arrière si au moins une étape est commencée/validée
|
||||
if (prep != PreparationStatus.notStarted) ...[
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _showRollbackDialog(context, event),
|
||||
icon: const Icon(Icons.undo, size: 18),
|
||||
label: const Text('Revenir à une étape précédente'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.orange[800],
|
||||
side: BorderSide(color: Colors.orange[300]!),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showRollbackDialog(BuildContext context, EventModel event) async {
|
||||
final prep = event.preparationStatus ?? PreparationStatus.notStarted;
|
||||
final loading = event.loadingStatus ?? LoadingStatus.notStarted;
|
||||
final unloading = event.unloadingStatus ?? UnloadingStatus.notStarted;
|
||||
final returnStatus = event.returnStatus ?? ReturnStatus.notStarted;
|
||||
|
||||
final List<Map<String, String>> steps = [];
|
||||
|
||||
if (prep == PreparationStatus.completed || prep == PreparationStatus.completedWithMissing) {
|
||||
steps.add({'key': 'PREPARATION', 'label': 'Préparation dépôt'});
|
||||
}
|
||||
if (loading == LoadingStatus.completed || loading == LoadingStatus.completedWithMissing) {
|
||||
steps.add({'key': 'LOADING', 'label': 'Chargement aller'});
|
||||
}
|
||||
if (unloading == UnloadingStatus.completed || unloading == UnloadingStatus.completedWithMissing) {
|
||||
steps.add({'key': 'UNLOADING', 'label': 'Chargement retour'});
|
||||
}
|
||||
if (returnStatus == ReturnStatus.completed || returnStatus == ReturnStatus.completedWithMissing) {
|
||||
steps.add({'key': 'RETURN', 'label': 'Retour dépôt'});
|
||||
}
|
||||
|
||||
if (steps.isEmpty) return;
|
||||
|
||||
final String? selectedStep = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.undo, color: Colors.orange),
|
||||
SizedBox(width: 8),
|
||||
Text('Revenir en arrière'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Sélectionnez l\'étape à laquelle vous souhaitez revenir :'),
|
||||
const SizedBox(height: 12),
|
||||
...steps.map((step) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.arrow_back, color: AppColors.rouge),
|
||||
title: Text(step['label']!),
|
||||
onTap: () => Navigator.of(context).pop(step['key']),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (selectedStep != null && context.mounted) {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Confirmer le retour en arrière'),
|
||||
content: const Text(
|
||||
'Êtes-vous sûr de vouloir revenir à cette étape ?\n\n'
|
||||
'Toutes les validations des étapes ultérieures seront effacées, '
|
||||
'et si le retour était terminé, les stocks restaurés seront annulés.'
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.rouge,
|
||||
),
|
||||
child: const Text('Confirmer'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirm == true && context.mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
final apiService = FirebaseFunctionsApiService();
|
||||
await apiService.call('rollbackEventStep', {
|
||||
'eventId': event.id,
|
||||
'targetStep': selectedStep,
|
||||
});
|
||||
|
||||
if (context.mounted) {
|
||||
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||
final userProvider = Provider.of<LocalUserProvider>(context, listen: false);
|
||||
if (userProvider.currentUser != null) {
|
||||
await eventProvider.refreshEvents(userProvider.currentUser!.uid);
|
||||
}
|
||||
|
||||
Navigator.of(context).pop(); // Fermer le loader
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Retour en arrière effectué avec succès'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop(); // Fermer le loader
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erreur : $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ enum ChecklistStep {
|
||||
}
|
||||
|
||||
/// Widget pour afficher un équipement dans une checklist de préparation/retour
|
||||
class EquipmentChecklistItem extends StatelessWidget {
|
||||
class EquipmentChecklistItem extends StatefulWidget {
|
||||
final EquipmentModel equipment;
|
||||
final EventEquipment eventEquipment;
|
||||
final ChecklistStep step;
|
||||
@@ -34,92 +34,120 @@ class EquipmentChecklistItem extends StatelessWidget {
|
||||
this.wasMissingBefore = false,
|
||||
});
|
||||
|
||||
/// Retourne la quantité actuelle selon l'étape
|
||||
@override
|
||||
State<EquipmentChecklistItem> createState() => _EquipmentChecklistItemState();
|
||||
}
|
||||
|
||||
class _EquipmentChecklistItemState extends State<EquipmentChecklistItem> {
|
||||
late TextEditingController _quantityController;
|
||||
|
||||
int _getCurrentQuantity() {
|
||||
switch (step) {
|
||||
switch (widget.step) {
|
||||
case ChecklistStep.preparation:
|
||||
return eventEquipment.quantityAtPreparation ?? eventEquipment.quantity;
|
||||
return widget.eventEquipment.quantityAtPreparation ?? widget.eventEquipment.quantity;
|
||||
case ChecklistStep.loading:
|
||||
return eventEquipment.quantityAtLoading ?? eventEquipment.quantityAtPreparation ?? eventEquipment.quantity;
|
||||
return widget.eventEquipment.quantityAtLoading ?? widget.eventEquipment.quantityAtPreparation ?? widget.eventEquipment.quantity;
|
||||
case ChecklistStep.unloading:
|
||||
return eventEquipment.quantityAtUnloading ?? eventEquipment.quantityAtLoading ?? eventEquipment.quantity;
|
||||
return widget.eventEquipment.quantityAtUnloading ?? widget.eventEquipment.quantityAtLoading ?? widget.eventEquipment.quantityAtPreparation ?? widget.eventEquipment.quantity;
|
||||
case ChecklistStep.return_:
|
||||
return eventEquipment.quantityAtReturn ?? eventEquipment.quantityAtUnloading ?? eventEquipment.quantity;
|
||||
return widget.eventEquipment.quantityAtReturn ?? widget.eventEquipment.quantityAtUnloading ?? widget.eventEquipment.quantityAtLoading ?? widget.eventEquipment.quantityAtPreparation ?? widget.eventEquipment.quantity;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_quantityController = TextEditingController(text: _getCurrentQuantity().toString());
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant EquipmentChecklistItem oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final currentQty = _getCurrentQuantity();
|
||||
final controllerQty = int.tryParse(_quantityController.text);
|
||||
if (controllerQty != currentQty) {
|
||||
_quantityController.text = currentQty.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_quantityController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasQuantity = equipment.hasQuantity;
|
||||
final hasQuantity = widget.equipment.hasQuantity;
|
||||
|
||||
// Déterminer la quantité actuelle selon l'étape
|
||||
final int currentQuantity = _getCurrentQuantity();
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: isChild ? 32.0 : 0.0, // Indentation pour les enfants
|
||||
left: widget.isChild ? 32.0 : 0.0, // Indentation pour les enfants
|
||||
top: 4.0,
|
||||
bottom: 4.0,
|
||||
),
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: isChild ? 0 : 1, // Pas d'élévation pour les enfants
|
||||
color: wasMissingBefore
|
||||
elevation: widget.isChild ? 0 : 1, // Pas d'élévation pour les enfants
|
||||
color: widget.wasMissingBefore
|
||||
? Colors.orange.shade50
|
||||
: (isChild ? Colors.grey.shade50 : Colors.white),
|
||||
: (widget.isChild ? Colors.grey.shade50 : Colors.white),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(
|
||||
color: wasMissingBefore
|
||||
color: widget.wasMissingBefore
|
||||
? Colors.orange
|
||||
: (isValidated ? Colors.green : Colors.grey.shade300),
|
||||
width: (isValidated || wasMissingBefore) ? 2 : 1,
|
||||
: (widget.isValidated ? Colors.green : Colors.grey.shade300),
|
||||
width: (widget.isValidated || widget.wasMissingBefore) ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
dense: isChild, // Plus compact pour les enfants
|
||||
dense: widget.isChild, // Plus compact pour les enfants
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: isChild ? 8.0 : 16.0,
|
||||
vertical: isChild ? 4.0 : 8.0,
|
||||
horizontal: widget.isChild ? 8.0 : 16.0,
|
||||
vertical: widget.isChild ? 4.0 : 8.0,
|
||||
),
|
||||
leading: Container(
|
||||
width: isChild ? 32 : 40,
|
||||
height: isChild ? 32 : 40,
|
||||
width: widget.isChild ? 32 : 40,
|
||||
height: widget.isChild ? 32 : 40,
|
||||
decoration: BoxDecoration(
|
||||
color: wasMissingBefore
|
||||
color: widget.wasMissingBefore
|
||||
? Colors.orange.shade100
|
||||
: (isValidated ? Colors.green.shade100 : Colors.grey.shade100),
|
||||
: (widget.isValidated ? Colors.green.shade100 : Colors.grey.shade100),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
wasMissingBefore
|
||||
widget.wasMissingBefore
|
||||
? Icons.warning
|
||||
: (isValidated ? Icons.check_circle : Icons.radio_button_unchecked),
|
||||
color: wasMissingBefore
|
||||
: (widget.isValidated ? Icons.check_circle : Icons.radio_button_unchecked),
|
||||
color: widget.wasMissingBefore
|
||||
? Colors.orange
|
||||
: (isValidated ? Colors.green : Colors.grey),
|
||||
size: isChild ? 18 : 24,
|
||||
: (widget.isValidated ? Colors.green : Colors.grey),
|
||||
size: widget.isChild ? 18 : 24,
|
||||
),
|
||||
onPressed: onToggle,
|
||||
onPressed: widget.onToggle,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
equipment.name,
|
||||
widget.equipment.name,
|
||||
style: TextStyle(
|
||||
fontWeight: isChild ? FontWeight.w500 : FontWeight.w600,
|
||||
fontSize: isChild ? 13 : 15,
|
||||
decoration: isValidated ? TextDecoration.lineThrough : null,
|
||||
color: isValidated ? Colors.grey : null,
|
||||
fontWeight: widget.isChild ? FontWeight.w500 : FontWeight.w600,
|
||||
fontSize: widget.isChild ? 13 : 15,
|
||||
decoration: widget.isValidated ? TextDecoration.lineThrough : null,
|
||||
color: widget.isValidated ? Colors.grey : null,
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (equipment.model != null)
|
||||
if (widget.equipment.model != null)
|
||||
Text(
|
||||
equipment.model!,
|
||||
widget.equipment.model!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
@@ -127,12 +155,12 @@ class EquipmentChecklistItem extends StatelessWidget {
|
||||
),
|
||||
|
||||
// Indicateur si manquant à l'étape précédente
|
||||
if (wasMissingBefore)
|
||||
if (widget.wasMissingBefore)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber, size: 14, color: Colors.orange),
|
||||
const Icon(Icons.warning_amber, size: 14, color: Colors.orange),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Était manquant à l\'étape précédente',
|
||||
@@ -151,7 +179,7 @@ class EquipmentChecklistItem extends StatelessWidget {
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
const Text(
|
||||
'Quantité : ',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
@@ -159,11 +187,11 @@ class EquipmentChecklistItem extends StatelessWidget {
|
||||
color: AppColors.bleuFonce,
|
||||
),
|
||||
),
|
||||
if (onQuantityChanged != null)
|
||||
if (widget.onQuantityChanged != null)
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: TextFormField(
|
||||
initialValue: currentQuantity.toString(),
|
||||
controller: _quantityController,
|
||||
keyboardType: TextInputType.number,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
@@ -175,14 +203,14 @@ class EquipmentChecklistItem extends StatelessWidget {
|
||||
),
|
||||
onChanged: (value) {
|
||||
final qty = int.tryParse(value) ?? currentQuantity;
|
||||
onQuantityChanged!(qty);
|
||||
widget.onQuantityChanged!(qty);
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
currentQuantity.toString(),
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.bleuFonce,
|
||||
@@ -193,16 +221,16 @@ class EquipmentChecklistItem extends StatelessWidget {
|
||||
],
|
||||
],
|
||||
),
|
||||
trailing: isValidated
|
||||
trailing: widget.isValidated
|
||||
? Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
children: [
|
||||
Icon(Icons.check, size: 16, color: Colors.green),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
|
||||
Reference in New Issue
Block a user