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:
@@ -9,8 +9,8 @@ import 'package:em2rp/providers/event_provider.dart';
|
||||
import 'package:em2rp/providers/local_user_provider.dart';
|
||||
import 'package:em2rp/services/data_service.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
import 'package:em2rp/views/widgets/equipment/equipment_checklist_item.dart' show EquipmentChecklistItem, ChecklistStep;
|
||||
import 'package:em2rp/views/widgets/equipment/container_checklist_item.dart';
|
||||
import 'package:em2rp/utils/debug_log.dart';
|
||||
import 'package:em2rp/views/widgets/equipment/missing_equipment_dialog.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
@@ -47,6 +47,13 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
// État local des validations (non sauvegardé jusqu'à la validation finale)
|
||||
Map<String, bool> _localValidationState = {};
|
||||
|
||||
|
||||
// NOUVEAU : Gestion des quantités par étape
|
||||
Map<String, int> _quantitiesAtPreparation = {};
|
||||
Map<String, int> _quantitiesAtLoading = {};
|
||||
Map<String, int> _quantitiesAtUnloading = {};
|
||||
Map<String, int> _quantitiesAtReturn = {};
|
||||
|
||||
bool _isLoading = true;
|
||||
bool _isValidating = false;
|
||||
bool _showSuccessAnimation = false;
|
||||
@@ -184,7 +191,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
if ((_currentStep == PreparationStep.return_ ||
|
||||
_currentStep == PreparationStep.unloadingReturn) &&
|
||||
equipmentItem.hasQuantity) {
|
||||
_returnedQuantities[eq.equipmentId] = eq.returnedQuantity ?? eq.quantity;
|
||||
_returnedQuantities[eq.equipmentId] = eq.quantityAtReturn ?? eq.quantity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,48 +218,114 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
}
|
||||
|
||||
/// Basculer l'état de validation d'un équipement (état local uniquement)
|
||||
void _toggleEquipmentValidation(String equipmentId) {
|
||||
Future<void> _toggleEquipmentValidation(String equipmentId) async {
|
||||
final currentState = _localValidationState[equipmentId] ?? false;
|
||||
|
||||
// Si on veut valider (passer de false à true) et que c'était manquant avant
|
||||
if (!currentState && _wasMissingAtPreviousStep(equipmentId)) {
|
||||
final confirmed = await _confirmValidationIfWasMissingBefore(equipmentId);
|
||||
if (!confirmed) {
|
||||
return; // Annulation, ne rien faire
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_localValidationState[equipmentId] = !(_localValidationState[equipmentId] ?? false);
|
||||
_localValidationState[equipmentId] = !currentState;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _validateAll() async {
|
||||
/// Marquer TOUT comme validé et enregistrer (bouton "Tout confirmer")
|
||||
Future<void> _validateAllAndConfirm() async {
|
||||
// Marquer tout comme validé localement
|
||||
setState(() {
|
||||
for (var eq in _currentEvent.assignedEquipment) {
|
||||
_localValidationState[eq.equipmentId] = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Puis enregistrer
|
||||
await _confirmCurrentState();
|
||||
}
|
||||
|
||||
/// Enregistrer l'état actuel TEL QUEL (cochés = validés, non cochés = manquants)
|
||||
Future<void> _confirmCurrentState() async {
|
||||
setState(() => _isValidating = true);
|
||||
|
||||
try {
|
||||
// Si "tout valider" est cliqué, marquer tout comme validé localement
|
||||
for (var equipmentId in _localValidationState.keys) {
|
||||
_localValidationState[equipmentId] = true;
|
||||
// Déterminer les manquants = équipements NON validés
|
||||
final Map<String, bool> missingAtThisStep = {};
|
||||
for (var eq in _currentEvent.assignedEquipment) {
|
||||
final isValidated = _localValidationState[eq.equipmentId] ?? false;
|
||||
missingAtThisStep[eq.equipmentId] = !isValidated; // Manquant si pas validé
|
||||
}
|
||||
|
||||
// Préparer la liste des équipements avec leur nouvel état
|
||||
final updatedEquipment = _currentEvent.assignedEquipment.map((eq) {
|
||||
final isValidated = _localValidationState[eq.equipmentId] ?? false;
|
||||
final isMissing = missingAtThisStep[eq.equipmentId] ?? false;
|
||||
|
||||
// Récupérer les quantités selon l'étape
|
||||
final qtyAtPrep = _quantitiesAtPreparation[eq.equipmentId];
|
||||
final qtyAtLoad = _quantitiesAtLoading[eq.equipmentId];
|
||||
final qtyAtUnload = _quantitiesAtUnloading[eq.equipmentId];
|
||||
final qtyAtRet = _quantitiesAtReturn[eq.equipmentId];
|
||||
|
||||
switch (_currentStep) {
|
||||
case PreparationStep.preparation:
|
||||
if (_loadSimultaneously) {
|
||||
return eq.copyWith(isPrepared: isValidated, isLoaded: isValidated);
|
||||
return eq.copyWith(
|
||||
isPrepared: isValidated,
|
||||
isLoaded: isValidated,
|
||||
isMissingAtPreparation: isMissing,
|
||||
isMissingAtLoading: isMissing, // Propager
|
||||
quantityAtPreparation: qtyAtPrep,
|
||||
quantityAtLoading: qtyAtPrep, // Même quantité
|
||||
);
|
||||
}
|
||||
return eq.copyWith(isPrepared: isValidated);
|
||||
return eq.copyWith(
|
||||
isPrepared: isValidated,
|
||||
isMissingAtPreparation: isMissing,
|
||||
quantityAtPreparation: qtyAtPrep,
|
||||
);
|
||||
|
||||
case PreparationStep.loadingOutbound:
|
||||
return eq.copyWith(isLoaded: isValidated);
|
||||
return eq.copyWith(
|
||||
isLoaded: isValidated,
|
||||
isMissingAtLoading: isMissing,
|
||||
quantityAtLoading: qtyAtLoad,
|
||||
);
|
||||
|
||||
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,
|
||||
isReturned: isValidated,
|
||||
isMissingAtUnloading: isMissing,
|
||||
isMissingAtReturn: isMissing, // Propager
|
||||
quantityAtUnloading: qtyAtUnload,
|
||||
quantityAtReturn: qtyAtRet ?? qtyAtUnload,
|
||||
);
|
||||
}
|
||||
return eq.copyWith(isUnloaded: isValidated);
|
||||
return eq.copyWith(
|
||||
isUnloaded: isValidated,
|
||||
isMissingAtUnloading: isMissing,
|
||||
quantityAtUnloading: qtyAtUnload,
|
||||
);
|
||||
|
||||
case PreparationStep.return_:
|
||||
final returnedQty = _returnedQuantities[eq.equipmentId] ?? eq.quantity;
|
||||
return eq.copyWith(isReturned: isValidated, returnedQuantity: returnedQty);
|
||||
return eq.copyWith(
|
||||
isReturned: isValidated,
|
||||
isMissingAtReturn: isMissing,
|
||||
quantityAtReturn: qtyAtRet,
|
||||
);
|
||||
}
|
||||
}).toList();
|
||||
|
||||
// Si on est à la dernière étape (retour), vérifier les équipements LOST
|
||||
if (_currentStep == PreparationStep.return_) {
|
||||
await _checkAndMarkLostEquipment(updatedEquipment);
|
||||
}
|
||||
|
||||
// Mettre à jour Firestore selon l'étape
|
||||
final updateData = <String, dynamic>{
|
||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||
@@ -362,11 +435,11 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
}
|
||||
|
||||
// Gérer les stocks pour les consommables
|
||||
if (equipmentData.hasQuantity && eq.isReturned && eq.returnedQuantity != null) {
|
||||
if (equipmentData.hasQuantity && eq.isReturned && eq.quantityAtReturn != null) {
|
||||
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
||||
await _dataService.updateEquipmentStatusOnly(
|
||||
equipmentId: eq.equipmentId,
|
||||
availableQuantity: currentAvailable + eq.returnedQuantity!,
|
||||
availableQuantity: currentAvailable + eq.quantityAtReturn!,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -463,7 +536,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
|
||||
if (missingEquipmentIds.isEmpty) {
|
||||
// Tout est validé, confirmer directement
|
||||
await _validateAll();
|
||||
await _confirmCurrentState();
|
||||
} else {
|
||||
// Afficher le dialog des manquants
|
||||
final missingEquipmentModels = missingEquipmentIds
|
||||
@@ -486,8 +559,8 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
);
|
||||
|
||||
if (action == 'confirm_anyway') {
|
||||
// Confirmer malgré les manquants
|
||||
await _validateAll();
|
||||
// Confirmer malgré les manquants (enregistrer l'état actuel TEL QUEL)
|
||||
await _confirmCurrentState();
|
||||
} else if (action == 'mark_as_validated') {
|
||||
// Marquer les manquants comme validés localement
|
||||
for (var equipmentId in missingEquipmentIds) {
|
||||
@@ -495,12 +568,137 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
}
|
||||
setState(() {});
|
||||
// Puis confirmer
|
||||
await _validateAll();
|
||||
await _confirmCurrentState();
|
||||
}
|
||||
// Si 'return_to_list', ne rien faire
|
||||
}
|
||||
}
|
||||
|
||||
/// Valider tous les enfants d'un container
|
||||
void _validateAllContainerChildren(String containerId) {
|
||||
final container = _containerCache[containerId];
|
||||
if (container == null) return;
|
||||
|
||||
setState(() {
|
||||
for (final equipmentId in container.equipmentIds) {
|
||||
if (_equipmentCache.containsKey(equipmentId)) {
|
||||
_localValidationState[equipmentId] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Mettre à jour la quantité d'un équipement à l'étape actuelle
|
||||
void _updateEquipmentQuantity(String equipmentId, int newQuantity) {
|
||||
setState(() {
|
||||
switch (_currentStep) {
|
||||
case PreparationStep.preparation:
|
||||
_quantitiesAtPreparation[equipmentId] = newQuantity;
|
||||
break;
|
||||
case PreparationStep.loadingOutbound:
|
||||
_quantitiesAtLoading[equipmentId] = newQuantity;
|
||||
break;
|
||||
case PreparationStep.unloadingReturn:
|
||||
_quantitiesAtUnloading[equipmentId] = newQuantity;
|
||||
break;
|
||||
case PreparationStep.return_:
|
||||
_quantitiesAtReturn[equipmentId] = newQuantity;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Vérifier si un équipement était manquant à l'étape précédente
|
||||
bool _wasMissingAtPreviousStep(String equipmentId) {
|
||||
final eq = _currentEvent.assignedEquipment.firstWhere(
|
||||
(e) => e.equipmentId == equipmentId,
|
||||
orElse: () => EventEquipment(equipmentId: equipmentId),
|
||||
);
|
||||
|
||||
switch (_currentStep) {
|
||||
case PreparationStep.preparation:
|
||||
return false; // Pas d'étape avant
|
||||
case PreparationStep.loadingOutbound:
|
||||
return eq.isMissingAtPreparation;
|
||||
case PreparationStep.unloadingReturn:
|
||||
return eq.isMissingAtLoading;
|
||||
case PreparationStep.return_:
|
||||
return eq.isMissingAtUnloading;
|
||||
}
|
||||
}
|
||||
|
||||
/// Afficher pop-up de confirmation si l'équipement était manquant avant
|
||||
Future<bool> _confirmValidationIfWasMissingBefore(String equipmentId) async {
|
||||
if (!_wasMissingAtPreviousStep(equipmentId)) {
|
||||
return true; // Pas de problème, continuer
|
||||
}
|
||||
|
||||
final equipment = _equipmentCache[equipmentId];
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber, color: Colors.orange),
|
||||
SizedBox(width: 8),
|
||||
Text('Confirmation'),
|
||||
],
|
||||
),
|
||||
content: Text(
|
||||
'L\'équipement "${equipment?.name ?? equipmentId}" était manquant à l\'étape précédente.\n\n'
|
||||
'Êtes-vous sûr de le marquer comme présent maintenant ?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
child: const Text('Confirmer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
/// Vérifier et marquer les équipements LOST (logique intelligente)
|
||||
Future<void> _checkAndMarkLostEquipment(List<EventEquipment> updatedEquipment) async {
|
||||
for (final eq in updatedEquipment) {
|
||||
final isMissingNow = eq.isMissingAtReturn;
|
||||
|
||||
if (isMissingNow) {
|
||||
// Vérifier si c'était manquant dès la préparation (étape 0)
|
||||
final wasMissingAtPreparation = eq.isMissingAtPreparation;
|
||||
|
||||
if (!wasMissingAtPreparation) {
|
||||
// Était présent au départ mais manquant maintenant = LOST
|
||||
try {
|
||||
await _dataService.updateEquipmentStatusOnly(
|
||||
equipmentId: eq.equipmentId,
|
||||
status: EquipmentStatus.lost.toString(),
|
||||
);
|
||||
|
||||
DebugLog.info('[EventPreparationPage] Équipement ${eq.equipmentId} marqué comme LOST');
|
||||
|
||||
// TODO: Créer une alerte "Équipement perdu"
|
||||
// await _createLostEquipmentAlert(eq.equipmentId);
|
||||
} catch (e) {
|
||||
DebugLog.error('[EventPreparationPage] Erreur marquage LOST ${eq.equipmentId}', e);
|
||||
}
|
||||
} else {
|
||||
// Manquant dès le début = PAS lost, juste manquant
|
||||
DebugLog.info('[EventPreparationPage] Équipement ${eq.equipmentId} manquant depuis le début (pas LOST)');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final allValidated = _isStepCompleted();
|
||||
@@ -579,7 +777,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: allValidated ? null : _validateAll,
|
||||
onPressed: allValidated ? null : _validateAllAndConfirm,
|
||||
icon: const Icon(Icons.check_circle_outline),
|
||||
label: Text(
|
||||
allValidated
|
||||
@@ -595,32 +793,9 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _currentEvent.assignedEquipment.length,
|
||||
itemBuilder: (context, index) {
|
||||
final eventEquipment = _currentEvent.assignedEquipment[index];
|
||||
final equipment = _equipmentCache[eventEquipment.equipmentId];
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
children: _buildChecklistItems(),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -686,5 +861,92 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit la liste des items de checklist en groupant par containers
|
||||
List<Widget> _buildChecklistItems() {
|
||||
final List<Widget> items = [];
|
||||
|
||||
// Set pour tracker les équipements déjà affichés dans un container
|
||||
final Set<String> equipmentIdsInContainers = {};
|
||||
|
||||
// Map des EventEquipment par ID pour accès rapide
|
||||
final Map<String, EventEquipment> eventEquipmentsMap = {
|
||||
for (var eq in _currentEvent.assignedEquipment) eq.equipmentId: eq,
|
||||
};
|
||||
|
||||
// 1. Afficher les containers avec leurs enfants
|
||||
for (final containerId in _currentEvent.assignedContainers) {
|
||||
final container = _containerCache[containerId];
|
||||
if (container == null) continue;
|
||||
|
||||
// Récupérer les équipements enfants de ce container
|
||||
final List<EquipmentModel> childEquipments = [];
|
||||
for (final equipmentId in container.equipmentIds) {
|
||||
final equipment = _equipmentCache[equipmentId];
|
||||
if (equipment != null && eventEquipmentsMap.containsKey(equipmentId)) {
|
||||
childEquipments.add(equipment);
|
||||
equipmentIdsInContainers.add(equipmentId);
|
||||
}
|
||||
}
|
||||
|
||||
if (childEquipments.isEmpty) continue;
|
||||
|
||||
// Vérifier si tous les enfants sont validés
|
||||
final allChildrenValidated = childEquipments.every(
|
||||
(eq) => _localValidationState[eq.id] ?? false,
|
||||
);
|
||||
|
||||
// Map des états de validation des enfants
|
||||
final Map<String, bool> childValidationStates = {
|
||||
for (var eq in childEquipments) eq.id: _localValidationState[eq.id] ?? false,
|
||||
};
|
||||
|
||||
// Map des enfants manquants à l'étape précédente
|
||||
final Map<String, bool> wasMissingBeforeMap = {
|
||||
for (var eq in childEquipments) eq.id: _wasMissingAtPreviousStep(eq.id),
|
||||
};
|
||||
|
||||
items.add(
|
||||
ContainerChecklistItem(
|
||||
container: container,
|
||||
childEquipments: childEquipments,
|
||||
eventEquipmentsMap: eventEquipmentsMap,
|
||||
step: _getChecklistStep(),
|
||||
isValidated: allChildrenValidated,
|
||||
childValidationStates: childValidationStates,
|
||||
onToggleContainer: () => _validateAllContainerChildren(containerId),
|
||||
onToggleChild: (equipmentId) => _toggleEquipmentValidation(equipmentId),
|
||||
onQuantityChanged: (equipmentId, qty) => _updateEquipmentQuantity(equipmentId, qty),
|
||||
wasMissingBeforeMap: wasMissingBeforeMap,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Afficher les équipements standalone (pas dans un container)
|
||||
for (final eventEquipment in _currentEvent.assignedEquipment) {
|
||||
// Skip si déjà affiché dans un container
|
||||
if (equipmentIdsInContainers.contains(eventEquipment.equipmentId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final equipment = _equipmentCache[eventEquipment.equipmentId];
|
||||
if (equipment == null) continue;
|
||||
|
||||
items.add(
|
||||
EquipmentChecklistItem(
|
||||
equipment: equipment,
|
||||
eventEquipment: eventEquipment,
|
||||
step: _getChecklistStep(),
|
||||
isValidated: _localValidationState[equipment.id] ?? false,
|
||||
onToggle: () => _toggleEquipmentValidation(equipment.id),
|
||||
onQuantityChanged: (qty) => _updateEquipmentQuantity(equipment.id, qty),
|
||||
isChild: false, // Équipement standalone (pas indenté)
|
||||
wasMissingBefore: _wasMissingAtPreviousStep(equipment.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -155,8 +155,27 @@ class _EventDetailsHeaderState extends State<EventDetailsHeader> {
|
||||
),
|
||||
);
|
||||
|
||||
// Générer le contenu ICS
|
||||
final icsContent = await IcsExportService.generateIcsContent(widget.event);
|
||||
// Charger les utilisateurs pour résoudre leurs noms
|
||||
final dataService = DataService(FirebaseFunctionsApiService());
|
||||
final users = await dataService.getUsers();
|
||||
|
||||
// Créer une Map des IDs utilisateurs vers leurs noms complets
|
||||
final Map<String, String> userNames = {};
|
||||
for (final user in users) {
|
||||
final userId = user['id'] as String?;
|
||||
final firstName = user['firstName'] as String? ?? '';
|
||||
final lastName = user['lastName'] as String? ?? '';
|
||||
if (userId != null && (firstName.isNotEmpty || lastName.isNotEmpty)) {
|
||||
userNames[userId] = '$firstName $lastName'.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Générer le contenu ICS avec le nom du type et les noms des utilisateurs
|
||||
final icsContent = await IcsExportService.generateIcsContent(
|
||||
widget.event,
|
||||
eventTypeName: _eventTypeName ?? 'Non spécifié',
|
||||
userNames: userNames, // Passer les noms des utilisateurs
|
||||
);
|
||||
final fileName = IcsExportService.generateFileName(widget.event);
|
||||
|
||||
// Créer un blob et télécharger le fichier
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:em2rp/models/equipment_model.dart';
|
||||
|
||||
/// Widget pour afficher un équipement avec checkbox de validation
|
||||
class EquipmentChecklistItem extends StatelessWidget {
|
||||
final EquipmentModel equipment;
|
||||
final bool isValidated;
|
||||
final ValueChanged<bool> onValidate;
|
||||
final bool isReturnMode;
|
||||
final int? quantity;
|
||||
final int? returnedQuantity;
|
||||
final ValueChanged<int>? onReturnedQuantityChanged;
|
||||
|
||||
const EquipmentChecklistItem({
|
||||
super.key,
|
||||
required this.equipment,
|
||||
required this.isValidated,
|
||||
required this.onValidate,
|
||||
this.isReturnMode = false,
|
||||
this.quantity,
|
||||
this.returnedQuantity,
|
||||
this.onReturnedQuantityChanged,
|
||||
});
|
||||
|
||||
bool get _isConsumable =>
|
||||
equipment.category == EquipmentCategory.consumable ||
|
||||
equipment.category == EquipmentCategory.cable;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: isValidated ? 0 : 2,
|
||||
color: isValidated ? Colors.green.shade50 : Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(
|
||||
color: isValidated ? Colors.green : Colors.grey.shade300,
|
||||
width: isValidated ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Checkbox de validation
|
||||
Checkbox(
|
||||
value: isValidated,
|
||||
onChanged: (value) => onValidate(value ?? false),
|
||||
activeColor: Colors.green,
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Icône de l'équipement
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: equipment.category.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: equipment.category.getIcon(
|
||||
size: 24,
|
||||
color: equipment.category.color,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Informations de l'équipement
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Nom/ID
|
||||
Text(
|
||||
equipment.id,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Marque/Modèle
|
||||
if (equipment.brand != null || equipment.model != null)
|
||||
Text(
|
||||
'${equipment.brand ?? ''} ${equipment.model ?? ''}'.trim(),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Quantité assignée (consommables uniquement)
|
||||
if (_isConsumable && quantity != null)
|
||||
Text(
|
||||
'Quantité assignée : $quantity',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
// Champ de quantité retournée (mode retour + consommables)
|
||||
if (isReturnMode && _isConsumable && onReturnedQuantityChanged != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Quantité retournée :',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 8,
|
||||
),
|
||||
hintText: quantity?.toString() ?? '0',
|
||||
),
|
||||
controller: TextEditingController(
|
||||
text: returnedQuantity?.toString() ?? quantity?.toString() ?? '0',
|
||||
),
|
||||
onChanged: (value) {
|
||||
final intValue = int.tryParse(value) ?? 0;
|
||||
if (onReturnedQuantityChanged != null) {
|
||||
onReturnedQuantityChanged!(intValue);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Icône de statut
|
||||
if (isValidated)
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.green,
|
||||
size: 28,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,8 +379,10 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
equipmentId: eq.equipmentId,
|
||||
quantity: eq.quantity, // Utiliser la nouvelle quantité
|
||||
isPrepared: updatedEquipment[existingIndex].isPrepared,
|
||||
isLoaded: updatedEquipment[existingIndex].isLoaded,
|
||||
isUnloaded: updatedEquipment[existingIndex].isUnloaded,
|
||||
isReturned: updatedEquipment[existingIndex].isReturned,
|
||||
returnedQuantity: updatedEquipment[existingIndex].returnedQuantity,
|
||||
quantityAtReturn: updatedEquipment[existingIndex].quantityAtReturn,
|
||||
);
|
||||
} else {
|
||||
// L'équipement n'existe pas : l'ajouter
|
||||
|
||||
Reference in New Issue
Block a user