feat: Refonte de la checklist de préparation avec gestion des manquants et des containers
Cette mise à jour refond entièrement l'interface et la logique de la checklist de préparation d'événement. Elle introduit la notion d'équipements "manquants", une gestion visuelle des containers et de leur contenu, et une logique plus fine pour le suivi des quantités et des statuts à chaque étape.
**Features et Améliorations :**
- **Gestion des Équipements Manquants :**
- Le modèle `EventEquipment` a été enrichi pour tracer si un équipement est manquant à chaque étape (`isMissingAtPreparation`, `isMissingAtLoading`, etc.).
- Un équipement non validé lors de la confirmation d'une étape est désormais marqué comme "manquant" pour les étapes suivantes.
- Les équipements qui étaient manquants à l'étape précédente sont maintenant visuellement mis en évidence avec une bordure et une icône orange, et une confirmation est demandée pour les valider.
- **Refonte de la Checklist (UI/UX) :**
- **Groupement par Container :** La checklist affiche désormais les containers comme des en-têtes de groupe. Les équipements qu'ils contiennent sont listés en dessous, avec une indentation visuelle.
- **Validation Groupée :** Il est possible de valider tous les équipements d'un container en un seul clic sur l'en-tête du container.
- **Nouveau Widget `ContainerChecklistItem` :** Créé pour afficher un container et ses équipements enfants dans la checklist.
- **Refonte de `EquipmentChecklistItem` :** Le widget a été entièrement revu pour un design plus clair, une meilleure gestion des états (validé, manquant), et un affichage compact pour les équipements enfants.
- **Logique de Suivi Améliorée :**
- **Quantités par Étape :** Le modèle `EventEquipment` et l'interface de préparation permettent maintenant de suivre les quantités réelles à chaque étape (`quantityAtPreparation`, `quantityAtLoading`, etc.), au lieu d'une seule quantité de retour.
- **Marquage Automatique des "Perdus" :** À l'étape finale du retour, un équipement qui était présent au départ mais qui est maintenant manquant sera automatiquement marqué avec le statut "lost" dans la base de données.
- **Flux de Validation :** Le processus de confirmation distingue désormais la validation de tous les équipements et la confirmation de l'état actuel (y compris les manquants).
- **Export ICS Enrichi :**
- L'export ICS inclut désormais les noms résolus des utilisateurs (main d'œuvre) pour plus de clarté, en plus des détails de l'événement.
- Le contenu généré mentionne la version de l'application.
This commit is contained in:
114
em2rp/lib/views/widgets/equipment/container_checklist_item.dart
Normal file
114
em2rp/lib/views/widgets/equipment/container_checklist_item.dart
Normal file
@@ -0,0 +1,114 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/models/container_model.dart';
|
||||
import 'package:em2rp/models/equipment_model.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/views/widgets/equipment/equipment_checklist_item.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
|
||||
/// Widget pour afficher un container avec ses équipements enfants dans une checklist
|
||||
class ContainerChecklistItem extends StatelessWidget {
|
||||
final ContainerModel container;
|
||||
final List<EquipmentModel> childEquipments;
|
||||
final Map<String, EventEquipment> eventEquipmentsMap; // Map equipmentId -> EventEquipment
|
||||
final ChecklistStep step;
|
||||
final bool isValidated; // Tous les enfants validés
|
||||
final Map<String, bool> childValidationStates;
|
||||
final VoidCallback onToggleContainer; // Callback pour clic sur le container (valide tout)
|
||||
final Function(String equipmentId) onToggleChild;
|
||||
final Function(String equipmentId, int quantity)? onQuantityChanged;
|
||||
final Map<String, bool> wasMissingBeforeMap; // Map des enfants manquants à l'étape précédente
|
||||
|
||||
const ContainerChecklistItem({
|
||||
super.key,
|
||||
required this.container,
|
||||
required this.childEquipments,
|
||||
required this.eventEquipmentsMap,
|
||||
required this.step,
|
||||
required this.isValidated,
|
||||
required this.childValidationStates,
|
||||
required this.onToggleContainer,
|
||||
required this.onToggleChild,
|
||||
this.onQuantityChanged,
|
||||
required this.wasMissingBeforeMap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final validatedCount = childValidationStates.values.where((v) => v).length;
|
||||
final totalCount = childEquipments.length;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Header du container (cliquable pour tout valider)
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 0),
|
||||
elevation: 2,
|
||||
color: isValidated ? Colors.green.shade50 : Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(
|
||||
color: isValidated ? Colors.green : AppColors.rouge.withValues(alpha: 0.3),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
onTap: onToggleContainer, // Clic sur le container = valider tout son contenu
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.rouge.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
container.type.iconData,
|
||||
color: AppColors.rouge,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
container.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'$validatedCount / $totalCount équipement(s) validé(s)\nCliquer pour tout valider',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
isValidated ? Icons.check_circle : Icons.inventory,
|
||||
color: isValidated ? Colors.green : AppColors.rouge,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Enfants (équipements) indentés
|
||||
...childEquipments.map((equipment) {
|
||||
final eventEquipment = eventEquipmentsMap[equipment.id];
|
||||
if (eventEquipment == null) return const SizedBox.shrink();
|
||||
|
||||
return EquipmentChecklistItem(
|
||||
equipment: equipment,
|
||||
eventEquipment: eventEquipment,
|
||||
step: step,
|
||||
isValidated: childValidationStates[equipment.id] ?? false,
|
||||
onToggle: () => onToggleChild(equipment.id),
|
||||
onQuantityChanged: onQuantityChanged != null
|
||||
? (qty) => onQuantityChanged!(equipment.id, qty)
|
||||
: null,
|
||||
isChild: true, // Affichage indenté et plus petit
|
||||
wasMissingBefore: wasMissingBeforeMap[equipment.id] ?? false,
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ class EquipmentChecklistItem extends StatelessWidget {
|
||||
final ChecklistStep step;
|
||||
final bool isValidated; // État de validation (passé depuis le parent)
|
||||
final VoidCallback onToggle;
|
||||
final ValueChanged<int>? onReturnedQuantityChanged;
|
||||
final ValueChanged<int>? onQuantityChanged; // Callback pour changer la quantité à cette étape
|
||||
final bool isChild; // Indique si c'est un enfant de container (affichage indenté et plus petit)
|
||||
final bool wasMissingBefore; // Était manquant à l'étape précédente
|
||||
|
||||
const EquipmentChecklistItem({
|
||||
super.key,
|
||||
@@ -27,110 +29,171 @@ class EquipmentChecklistItem extends StatelessWidget {
|
||||
this.step = ChecklistStep.preparation,
|
||||
required this.isValidated,
|
||||
required this.onToggle,
|
||||
this.onReturnedQuantityChanged,
|
||||
this.onQuantityChanged,
|
||||
this.isChild = false, // Par défaut, pas d'indentation
|
||||
this.wasMissingBefore = false,
|
||||
});
|
||||
|
||||
/// Retourne la quantité actuelle selon l'étape
|
||||
int _getCurrentQuantity() {
|
||||
switch (step) {
|
||||
case ChecklistStep.preparation:
|
||||
return eventEquipment.quantityAtPreparation ?? eventEquipment.quantity;
|
||||
case ChecklistStep.loading:
|
||||
return eventEquipment.quantityAtLoading ?? eventEquipment.quantityAtPreparation ?? eventEquipment.quantity;
|
||||
case ChecklistStep.unloading:
|
||||
return eventEquipment.quantityAtUnloading ?? eventEquipment.quantityAtLoading ?? eventEquipment.quantity;
|
||||
case ChecklistStep.return_:
|
||||
return eventEquipment.quantityAtReturn ?? eventEquipment.quantityAtUnloading ?? eventEquipment.quantity;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasQuantity = equipment.hasQuantity;
|
||||
final showQuantityInput = step == ChecklistStep.return_ && hasQuantity;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 0),
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(
|
||||
color: isValidated ? Colors.green : Colors.grey.shade300,
|
||||
width: isValidated ? 2 : 1,
|
||||
),
|
||||
// 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
|
||||
top: 4.0,
|
||||
bottom: 4.0,
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: isValidated ? Colors.green.shade100 : Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: isChild ? 0 : 1, // Pas d'élévation pour les enfants
|
||||
color: wasMissingBefore
|
||||
? Colors.orange.shade50
|
||||
: (isChild ? Colors.grey.shade50 : Colors.white),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(
|
||||
color: wasMissingBefore
|
||||
? Colors.orange
|
||||
: (isValidated ? Colors.green : Colors.grey.shade300),
|
||||
width: (isValidated || wasMissingBefore) ? 2 : 1,
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
isValidated ? Icons.check_circle : Icons.radio_button_unchecked,
|
||||
color: isValidated ? Colors.green : Colors.grey,
|
||||
),
|
||||
child: ListTile(
|
||||
dense: isChild, // Plus compact pour les enfants
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: isChild ? 8.0 : 16.0,
|
||||
vertical: isChild ? 4.0 : 8.0,
|
||||
),
|
||||
leading: Container(
|
||||
width: isChild ? 32 : 40,
|
||||
height: isChild ? 32 : 40,
|
||||
decoration: BoxDecoration(
|
||||
color: wasMissingBefore
|
||||
? Colors.orange.shade100
|
||||
: (isValidated ? Colors.green.shade100 : Colors.grey.shade100),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
onPressed: onToggle,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
equipment.name,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: isValidated ? TextDecoration.lineThrough : null,
|
||||
color: isValidated ? Colors.grey : null,
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (equipment.model != null)
|
||||
Text(
|
||||
equipment.model!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
wasMissingBefore
|
||||
? Icons.warning
|
||||
: (isValidated ? Icons.check_circle : Icons.radio_button_unchecked),
|
||||
color: wasMissingBefore
|
||||
? Colors.orange
|
||||
: (isValidated ? Colors.green : Colors.grey),
|
||||
size: isChild ? 18 : 24,
|
||||
),
|
||||
if (hasQuantity) ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Quantité : ${eventEquipment.quantity}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.bleuFonce,
|
||||
),
|
||||
onPressed: onToggle,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
equipment.name,
|
||||
style: TextStyle(
|
||||
fontWeight: isChild ? FontWeight.w500 : FontWeight.w600,
|
||||
fontSize: isChild ? 13 : 15,
|
||||
decoration: isValidated ? TextDecoration.lineThrough : null,
|
||||
color: isValidated ? Colors.grey : null,
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (equipment.model != null)
|
||||
Text(
|
||||
equipment.model!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
if (showQuantityInput && onReturnedQuantityChanged != null) ...[
|
||||
const SizedBox(width: 16),
|
||||
const Icon(Icons.arrow_forward, size: 12, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
),
|
||||
|
||||
// Indicateur si manquant à l'étape précédente
|
||||
if (wasMissingBefore)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber, size: 14, color: Colors.orange),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Était manquant à l\'étape précédente',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.orange.shade700,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Quantité (éditable ou affichage seul)
|
||||
if (hasQuantity) ...[
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Retourné : ',
|
||||
'Quantité : ',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.bleuFonce,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
if (onQuantityChanged != null)
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: TextFormField(
|
||||
initialValue: currentQuantity.toString(),
|
||||
keyboardType: TextInputType.number,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
hintText: '${eventEquipment.quantity}',
|
||||
onChanged: (value) {
|
||||
final qty = int.tryParse(value) ?? currentQuantity;
|
||||
onQuantityChanged!(qty);
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
currentQuantity.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.bleuFonce,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
final qty = int.tryParse(value) ?? eventEquipment.quantity;
|
||||
onReturnedQuantityChanged!(qty);
|
||||
},
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
trailing: isValidated
|
||||
),
|
||||
trailing: isValidated
|
||||
? Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
@@ -154,8 +217,9 @@ class EquipmentChecklistItem extends StatelessWidget {
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
), // Fin ListTile
|
||||
), // Fin Card
|
||||
); // Fin Padding
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user