feat: implement equipment and container loading rollback functionality with corresponding backend cloud functions

This commit is contained in:
ElPoyo
2026-05-27 22:04:46 +02:00
parent 64a9fe382a
commit faff06e4df
15 changed files with 660 additions and 514 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
/// Configuration de la version de l'application
class AppVersion {
static const String version = '1.2.3';
static const String version = '1.2.4';
/// Retourne la version complète de l'application
static String get fullVersion => 'v$version';
@@ -1,8 +1,4 @@
import 'package:em2rp/services/equipment_status_calculator.dart';
import 'package:em2rp/services/api_service.dart';
class EventPreparationService {
final ApiService _apiService = apiService;
/// Retourne true si l'équipement était absent du flux événementiel.
///
@@ -42,35 +38,7 @@ class EventPreparationService {
);
}
// === PRÉPARATION ===
/// Valider un équipement individuel en préparation
Future<void> validateEquipmentPreparation(String eventId, String equipmentId) async {
try {
await _apiService.call('validateEquipmentPreparation', {
'eventId': eventId,
'equipmentId': equipmentId,
});
} catch (e) {
print('Error validating equipment preparation: $e');
rethrow;
}
}
/// Valider tous les équipements en préparation
Future<void> validateAllPreparation(String eventId) async {
try {
await _apiService.call('validateAllPreparation', {
'eventId': eventId,
});
// Invalider le cache des statuts d'équipement
EquipmentStatusCalculator.invalidateGlobalCache();
} catch (e) {
print('Error validating all preparation: $e');
rethrow;
}
}
// Ces méthodes ne sont plus utilisées et devraient être remplacées par des Cloud Functions
// si nécessaire dans le futur
@@ -85,46 +53,8 @@ class EventPreparationService {
}
*/
// === RETOUR ===
/// Valider le retour d'un équipement individuel
Future<void> validateEquipmentReturn(
String eventId,
String equipmentId, {
int? returnedQuantity,
}) async {
try {
await _apiService.call('validateEquipmentReturn', {
'eventId': eventId,
'equipmentId': equipmentId,
if (returnedQuantity != null) 'returnedQuantity': returnedQuantity,
});
} catch (e) {
print('Error validating equipment return: $e');
rethrow;
}
}
/// Valider tous les retours
Future<void> validateAllReturn(
String eventId, [
Map<String, int>? returnedQuantities,
]) async {
try {
await _apiService.call('validateAllReturn', {
'eventId': eventId,
if (returnedQuantities != null) 'returnedQuantities': returnedQuantities,
});
// Invalider le cache des statuts d'équipement
EquipmentStatusCalculator.invalidateGlobalCache();
} catch (e) {
print('Error validating all return: $e');
rethrow;
}
}
/*
@Deprecated('Use Cloud Functions instead')
Future<void> completeReturnWithMissing(
+88 -56
View File
@@ -88,22 +88,22 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
// Logique stricte : on avance étape par étape
// 1. Préparation dépôt
if (prep != PreparationStatus.completed) {
if (prep != PreparationStatus.completed && prep != PreparationStatus.completedWithMissing) {
return PreparationStep.preparation;
}
// 2. Chargement aller (après préparation complète)
if (loading != LoadingStatus.completed) {
if (loading != LoadingStatus.completed && loading != LoadingStatus.completedWithMissing) {
return PreparationStep.loadingOutbound;
}
// 3. Chargement retour (après chargement aller complet)
if (unloading != UnloadingStatus.completed) {
if (unloading != UnloadingStatus.completed && unloading != UnloadingStatus.completedWithMissing) {
return PreparationStep.unloadingReturn;
}
// 4. Retour dépôt (après déchargement complet)
if (returnStatus != ReturnStatus.completed) {
if (returnStatus != ReturnStatus.completed && returnStatus != ReturnStatus.completedWithMissing) {
return PreparationStep.return_;
}
@@ -132,7 +132,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_isCurrentStepCompleted()) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
const SnackBar(
content: Text('Cette étape est déjà terminée'),
backgroundColor: Colors.orange,
),
@@ -141,6 +141,17 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
return;
}
if (!_isPreviousStepCompleted()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('L\'étape précédente n\'est pas terminée. Impossible d\'accéder à cette étape.'),
backgroundColor: Colors.red,
),
);
Navigator.of(context).pop();
return;
}
// Charger les équipements après le premier frame pour éviter setState pendant build
_loadEquipmentAndContainers();
});
@@ -150,13 +161,34 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
bool _isCurrentStepCompleted() {
switch (_currentStep) {
case PreparationStep.preparation:
return (_currentEvent.preparationStatus ?? PreparationStatus.notStarted) == PreparationStatus.completed;
final status = _currentEvent.preparationStatus ?? PreparationStatus.notStarted;
return status == PreparationStatus.completed || status == PreparationStatus.completedWithMissing;
case PreparationStep.loadingOutbound:
return (_currentEvent.loadingStatus ?? LoadingStatus.notStarted) == LoadingStatus.completed;
final status = _currentEvent.loadingStatus ?? LoadingStatus.notStarted;
return status == LoadingStatus.completed || status == LoadingStatus.completedWithMissing;
case PreparationStep.unloadingReturn:
return (_currentEvent.unloadingStatus ?? UnloadingStatus.notStarted) == UnloadingStatus.completed;
final status = _currentEvent.unloadingStatus ?? UnloadingStatus.notStarted;
return status == UnloadingStatus.completed || status == UnloadingStatus.completedWithMissing;
case PreparationStep.return_:
return (_currentEvent.returnStatus ?? ReturnStatus.notStarted) == ReturnStatus.completed;
final status = _currentEvent.returnStatus ?? ReturnStatus.notStarted;
return status == ReturnStatus.completed || status == ReturnStatus.completedWithMissing;
}
}
/// Vérifie si l'étape précédente est bien complétée
bool _isPreviousStepCompleted() {
switch (_currentStep) {
case PreparationStep.preparation:
return true; // Première étape, toujours OK
case PreparationStep.loadingOutbound:
final prep = _currentEvent.preparationStatus ?? PreparationStatus.notStarted;
return prep == PreparationStatus.completed || prep == PreparationStatus.completedWithMissing;
case PreparationStep.unloadingReturn:
final loading = _currentEvent.loadingStatus ?? LoadingStatus.notStarted;
return loading == LoadingStatus.completed || loading == LoadingStatus.completedWithMissing;
case PreparationStep.return_:
final unloading = _currentEvent.unloadingStatus ?? UnloadingStatus.notStarted;
return unloading == UnloadingStatus.completed || unloading == UnloadingStatus.completedWithMissing;
}
}
@@ -239,10 +271,15 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
break;
}
_quantitiesAtPreparation[eq.equipmentId] = eq.quantityAtPreparation ?? eq.quantity;
_quantitiesAtLoading[eq.equipmentId] = eq.quantityAtLoading ?? eq.quantityAtPreparation ?? eq.quantity;
_quantitiesAtUnloading[eq.equipmentId] = eq.quantityAtUnloading ?? eq.quantityAtLoading ?? eq.quantityAtPreparation ?? eq.quantity;
_quantitiesAtReturn[eq.equipmentId] = eq.quantityAtReturn ?? eq.quantityAtUnloading ?? eq.quantityAtLoading ?? eq.quantityAtPreparation ?? eq.quantity;
if ((_currentStep == PreparationStep.return_ ||
_currentStep == PreparationStep.unloadingReturn) &&
(equipmentItem?.hasQuantity ?? false)) {
_returnedQuantities[eq.equipmentId] = eq.quantityAtReturn ?? eq.quantity;
_returnedQuantities[eq.equipmentId] = eq.quantityAtReturn ?? eq.quantityAtUnloading ?? eq.quantityAtLoading ?? eq.quantityAtPreparation ?? eq.quantity;
}
}
@@ -418,9 +455,8 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
returnStatus: updateData['returnStatus'],
);
// Mettre à jour les statuts des équipements si nécessaire
if (_currentStep == PreparationStep.preparation ||
(_currentStep == PreparationStep.unloadingReturn && _loadSimultaneously)) {
// Mettre à jour les statuts des équipements si nécessaire (uniquement pour la préparation, le retour étant géré par le trigger Firestore Cloud Function)
if (_currentStep == PreparationStep.preparation) {
await _updateEquipmentStatuses(updatedEquipment);
}
@@ -505,6 +541,8 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
}
Future<void> _updateEquipmentStatuses(List<EventEquipment> equipment) async {
final List<String> failedUpdates = [];
for (var eq in equipment) {
try {
final equipmentData = _equipmentCache[eq.equipmentId];
@@ -513,7 +551,9 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
// Déterminer le nouveau statut
EquipmentStatus newStatus;
if (eq.isReturned) {
newStatus = EquipmentStatus.available;
// Note : Le retour est géré par le trigger Firestore Cloud Function en tâche de fond.
// On évite les conflits d'écritures client/serveur et les double-restaurations de stock.
continue;
} else if (eq.isPrepared || eq.isLoaded) {
newStatus = EquipmentStatus.inUse;
} else {
@@ -527,19 +567,22 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
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
DebugLog.error('[EventPreparationPage] Échec de la mise à jour du statut pour l\'équipement ${eq.equipmentId}', e);
failedUpdates.add(eq.equipmentId);
}
}
if (failedUpdates.isNotEmpty && mounted) {
final names = failedUpdates.map((id) => _equipmentCache[id]?.name ?? id).join(', ');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Attention : Échec de mise à jour du statut en base pour : $names. Le matériel a tout de même été validé pour l\'événement.'),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 6),
),
);
}
}
String _getSuccessMessage() {
@@ -895,26 +938,28 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
_quantitiesAtReturn.addAll(quantities);
break;
}
// Mettre à jour `_currentEvent.assignedEquipment` pour que l'UI se reconstruise avec les bonnes valeurs
final updatedList = _currentEvent.assignedEquipment.map((eq) {
final qty = quantities[eq.equipmentId];
if (qty != null) {
switch (_currentStep) {
case PreparationStep.preparation:
return eq.copyWith(quantityAtPreparation: qty);
case PreparationStep.loadingOutbound:
return eq.copyWith(quantityAtLoading: qty);
case PreparationStep.unloadingReturn:
return eq.copyWith(quantityAtUnloading: qty);
case PreparationStep.return_:
return eq.copyWith(quantityAtReturn: qty);
}
}
return eq;
}).toList();
_currentEvent = _currentEvent.copyWith(assignedEquipment: updatedList);
}
/// Obtenir la quantité requise selon l'étape (nouvelle logique)
int _getTargetQuantity(EventEquipment eventEquipment) {
switch (_currentStep) {
case PreparationStep.preparation:
return eventEquipment.quantity; // Quantité initiale
case PreparationStep.loadingOutbound:
return eventEquipment.quantityAtPreparation ?? eventEquipment.quantity;
case PreparationStep.unloadingReturn:
return eventEquipment.quantityAtLoading ??
eventEquipment.quantityAtPreparation ??
eventEquipment.quantity;
case PreparationStep.return_:
return eventEquipment.quantityAtUnloading ??
eventEquipment.quantityAtLoading ??
eventEquipment.quantityAtPreparation ??
eventEquipment.quantity;
}
}
/// Afficher un message de succès
void _showSuccessFeedback(String message) {
@@ -1020,20 +1065,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
/// 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;
}
_updateQuantitiesMap({equipmentId: newQuantity});
});
}
@@ -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(