Cette mise à jour majeure introduit une fonctionnalité de scan et de saisie manuelle de codes QR directement depuis la page de préparation d'un événement. Ce système accélère et fiabilise le processus de validation des équipements et des containers pour chaque étape (préparation, chargement, etc.), tout en ajoutant des retours sonores, haptiques et visuels pour une expérience utilisateur améliorée.
**Fonctionnalités et améliorations principales :**
- **Scan et saisie manuelle en préparation d'événement :**
- Ajout d'un champ de "Saisie manuelle" et d'un bouton "Scanner QR Code" sur la page de préparation (`EventPreparationPage`).
- Le scanner peut fonctionner en mode "multi-scan", permettant de valider plusieurs éléments à la suite sans fermer la caméra.
- Le système gère à la fois les équipements individuels et les containers (qui valident automatiquement tout leur contenu).
- **Logique de traitement intelligente (`QRCodeProcessingService`) :**
- Un nouveau service centralise la logique de traitement des codes.
- Pour les équipements quantitatifs, chaque scan incrémente la quantité jusqu'à atteindre la cible requise pour l'étape en cours.
- Pour les équipements non quantitatifs, le premier scan valide l'élément.
- Les scans multiples d'un élément déjà validé ou dont la quantité est atteinte génèrent une erreur.
- **Ajout dynamique d'équipements :**
- Si un code scanné n'est pas assigné à l'événement, une boîte de dialogue propose de rechercher l'équipement ou le container dans la base de données et de l'ajouter à l'événement en cours.
- **Feedbacks utilisateur :**
- Création d'un `AudioFeedbackService` pour fournir des retours sonores (succès/erreur) et haptiques (vibration) lors de chaque scan.
- Des `Snackbars` claires (vertes pour succès, orange pour erreur) informent l'utilisateur du résultat de chaque action.
- **Optimisation du chargement des données :**
- Nouvel endpoint backend `getEventWithDetails` qui charge un événement et toutes ses dépendances (équipements, containers et leurs enfants) en un seul appel, optimisant drastiquement les temps de chargement des pages de préparation et de modification d'événement.
- Le frontend (`EventPreparationPage`, `EventAssignedEquipmentSection`) utilise ce nouvel endpoint, éliminant les chargements multiples et fiabilisant l'affichage des données.
**Refactorisation et corrections :**
- **Structure du code :**
- La logique de traitement des codes est extraite dans le `QRCodeProcessingService`.
- Création de widgets dédiés (`CodeNotFoundDialog`, `AddEquipmentToEventDialog`) pour gérer les nouveaux flux utilisateurs.
- **Fiabilisation de l'état :**
- Mise à jour optimiste de l'UI lors du changement de statut d'un événement (`EventStatusButton`) pour une meilleure réactivité.
- Correction d'un bug dans la suppression d'un container d'un événement, qui pouvait retirer des équipements partagés avec d'autres containers.
- Correction d'un bug lors de l'ajout d'un container à un événement, qui n'ajoutait pas automatiquement ses équipements enfants.
- **Optimisations des performances UI :**
- Amélioration de la fluidité du défilement infini sur la page de gestion des équipements grâce à `RepaintBoundary` et à une gestion optimisée du chargement.
**Déploiement et version :**
- **Scripts de déploiement :** Ajout d'un script PowerShell (`deploy_hosting.ps1`) et amélioration du script Node.js pour automatiser et fiabiliser les déploiements sur Firebase Hosting.
- **Configuration CORS :** Les en-têtes CORS sont désormais configurés pour `version.json`, assurant le bon fonctionnement du mécanisme de mise à jour de l'application.
- **Version de l'application :** Incrémentée à `1.0.6`.
796 lines
23 KiB
Dart
796 lines
23 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:em2rp/utils/colors.dart';
|
|
import 'package:em2rp/utils/permission_gate.dart';
|
|
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
|
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
|
import 'package:em2rp/providers/container_provider.dart';
|
|
import 'package:em2rp/providers/equipment_provider.dart';
|
|
import 'package:em2rp/models/container_model.dart';
|
|
import 'package:em2rp/models/equipment_model.dart';
|
|
import 'package:em2rp/views/equipment_detail_page.dart';
|
|
import 'package:em2rp/views/widgets/common/qr_code_dialog.dart';
|
|
import 'package:em2rp/views/widgets/common/qr_code_scanner_dialog.dart';
|
|
import 'package:em2rp/views/widgets/common/qr_code_format_selector_dialog.dart';
|
|
import 'package:em2rp/mixins/selection_mode_mixin.dart';
|
|
import 'package:em2rp/views/widgets/management/management_card.dart';
|
|
import 'package:em2rp/utils/debug_log.dart';
|
|
import 'package:em2rp/views/widgets/common/search_actions_bar.dart';
|
|
import 'package:em2rp/views/widgets/notification_badge.dart';
|
|
|
|
class ContainerManagementPage extends StatefulWidget {
|
|
const ContainerManagementPage({super.key});
|
|
|
|
@override
|
|
State<ContainerManagementPage> createState() =>
|
|
_ContainerManagementPageState();
|
|
}
|
|
|
|
class _ContainerManagementPageState extends State<ContainerManagementPage>
|
|
with SelectionModeMixin<ContainerManagementPage> {
|
|
final TextEditingController _searchController = TextEditingController();
|
|
final ScrollController _scrollController = ScrollController();
|
|
ContainerType? _selectedType;
|
|
bool _isLoadingMore = false; // Flag pour éviter les appels multiples
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
// Activer le mode pagination
|
|
final provider = context.read<ContainerProvider>();
|
|
provider.enablePagination();
|
|
|
|
// Ajouter le listener de scroll
|
|
_scrollController.addListener(_onScroll);
|
|
|
|
// Charger la première page
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
provider.loadFirstPage();
|
|
});
|
|
}
|
|
|
|
void _onScroll() {
|
|
// Éviter les appels multiples
|
|
if (_isLoadingMore) return;
|
|
|
|
final provider = context.read<ContainerProvider>();
|
|
|
|
// Charger la page suivante quand on arrive à 300px du bas
|
|
if (_scrollController.hasClients &&
|
|
_scrollController.position.pixels >=
|
|
_scrollController.position.maxScrollExtent - 300) {
|
|
|
|
// Vérifier qu'on peut charger plus
|
|
if (provider.hasMore && !provider.isLoadingMore) {
|
|
setState(() => _isLoadingMore = true);
|
|
|
|
provider.loadNextPage().then((_) {
|
|
if (mounted) {
|
|
setState(() => _isLoadingMore = false);
|
|
}
|
|
}).catchError((error) {
|
|
if (mounted) {
|
|
setState(() => _isLoadingMore = false);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollController.removeListener(_onScroll);
|
|
_scrollController.dispose();
|
|
_searchController.dispose();
|
|
context.read<ContainerProvider>().disablePagination();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isMobile = MediaQuery.of(context).size.width < 800;
|
|
|
|
return PermissionGate(
|
|
requiredPermissions: const ['view_equipment'],
|
|
fallback: Scaffold(
|
|
appBar: const CustomAppBar(title: 'Accès refusé'),
|
|
drawer: const MainDrawer(currentPage: '/container_management'),
|
|
body: const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(24.0),
|
|
child: Text(
|
|
'Vous n\'avez pas les permissions nécessaires pour accéder à la gestion des containers.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontSize: 16),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
child: Scaffold(
|
|
appBar: isSelectionMode
|
|
? AppBar(
|
|
backgroundColor: AppColors.rouge,
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.close, color: Colors.white),
|
|
onPressed: toggleSelectionMode,
|
|
),
|
|
title: Text(
|
|
'$selectedCount sélectionné(s)',
|
|
style: const TextStyle(color: Colors.white),
|
|
),
|
|
actions: [
|
|
const NotificationBadge(),
|
|
if (hasSelection) ...[
|
|
IconButton(
|
|
icon: const Icon(Icons.qr_code, color: Colors.white),
|
|
tooltip: 'Générer QR Codes',
|
|
onPressed: _generateQRCodesForSelected,
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.delete, color: Colors.white),
|
|
tooltip: 'Supprimer',
|
|
onPressed: _deleteSelectedContainers,
|
|
),
|
|
],
|
|
],
|
|
)
|
|
: CustomAppBar(
|
|
title: 'Gestion des Containers',
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
tooltip: 'Retour à la gestion des équipements',
|
|
onPressed: () => Navigator.pushReplacementNamed(context, '/equipment_management'),
|
|
),
|
|
showLogoutButton: true,
|
|
),
|
|
drawer: const MainDrawer(currentPage: '/container_management'),
|
|
floatingActionButton: !isSelectionMode
|
|
? FloatingActionButton.extended(
|
|
onPressed: () => _navigateToForm(context),
|
|
backgroundColor: AppColors.rouge,
|
|
icon: const Icon(Icons.add, color: Colors.white),
|
|
label: const Text(
|
|
'Nouveau Container',
|
|
style: TextStyle(color: Colors.white),
|
|
),
|
|
)
|
|
: null,
|
|
body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMobileLayout() {
|
|
return Column(
|
|
children: [
|
|
_buildSearchBar(),
|
|
_buildMobileFilters(),
|
|
Expanded(child: _buildContainerList()),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildDesktopLayout() {
|
|
return Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 250,
|
|
child: _buildSidebar(),
|
|
),
|
|
const VerticalDivider(width: 1, thickness: 1),
|
|
Expanded(
|
|
child: Column(
|
|
children: [
|
|
_buildSearchBar(),
|
|
Expanded(child: _buildContainerList()),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildSearchBar() {
|
|
return SearchActionsBar(
|
|
controller: _searchController,
|
|
hintText: 'Rechercher un container...',
|
|
onChanged: (value) {
|
|
context.read<ContainerProvider>().setSearchQuery(value);
|
|
},
|
|
onClear: () {
|
|
_searchController.clear();
|
|
context.read<ContainerProvider>().setSearchQuery('');
|
|
},
|
|
actions: [
|
|
IconButton.filled(
|
|
onPressed: _scanQRCode,
|
|
icon: const Icon(Icons.qr_code_scanner),
|
|
tooltip: 'Scanner un QR Code',
|
|
style: IconButton.styleFrom(
|
|
backgroundColor: Colors.grey[700],
|
|
foregroundColor: Colors.white,
|
|
),
|
|
),
|
|
if (!isSelectionMode)
|
|
IconButton.filled(
|
|
onPressed: toggleSelectionMode,
|
|
icon: const Icon(Icons.checklist),
|
|
tooltip: 'Mode sélection',
|
|
style: IconButton.styleFrom(
|
|
backgroundColor: AppColors.rouge,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildMobileFilters() {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
color: Colors.grey.shade50,
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
children: [
|
|
_buildTypeChip(null, 'Tous'),
|
|
const SizedBox(width: 8),
|
|
...ContainerType.values.map((type) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(right: 8),
|
|
child: _buildTypeChip(type, type.label),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTypeChip(ContainerType? type, String label) {
|
|
final isSelected = _selectedType == type;
|
|
final color = isSelected ? Colors.white : AppColors.noir;
|
|
|
|
return ChoiceChip(
|
|
label: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (type != null) ...[
|
|
type.getIcon(size: 16, color: color),
|
|
const SizedBox(width: 8),
|
|
],
|
|
Text(label),
|
|
],
|
|
),
|
|
selected: isSelected,
|
|
onSelected: (selected) {
|
|
setState(() {
|
|
_selectedType = selected ? type : null;
|
|
context.read<ContainerProvider>().setSelectedType(_selectedType);
|
|
});
|
|
},
|
|
selectedColor: AppColors.rouge,
|
|
labelStyle: TextStyle(
|
|
color: color,
|
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSidebar() {
|
|
return Container(
|
|
color: Colors.grey.shade50,
|
|
child: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
Text(
|
|
'Filtres',
|
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.noir,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Filtre par type
|
|
Text(
|
|
'Type de container',
|
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.noir,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildFilterOption(null, 'Tous les types'),
|
|
...ContainerType.values.map((type) {
|
|
return _buildFilterOption(type, type.label);
|
|
}),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFilterOption(ContainerType? type, String label) {
|
|
return RadioListTile<ContainerType?>(
|
|
title: Text(label),
|
|
value: type,
|
|
groupValue: _selectedType,
|
|
activeColor: AppColors.rouge,
|
|
dense: true,
|
|
contentPadding: EdgeInsets.zero,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_selectedType = value;
|
|
context.read<ContainerProvider>().setSelectedType(_selectedType);
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
|
|
Widget _buildContainerList() {
|
|
return Consumer<ContainerProvider>(
|
|
builder: (context, provider, child) {
|
|
// Afficher l'indicateur de chargement initial
|
|
if (provider.isLoading && provider.containers.isEmpty) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
final containers = provider.containers;
|
|
|
|
// Afficher le message vide
|
|
if (containers.isEmpty && !provider.isLoading) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.inventory_2_outlined,
|
|
size: 64,
|
|
color: Colors.grey[400],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Aucun container trouvé',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Calculer le nombre total d'items
|
|
final itemCount = containers.length + (provider.hasMore ? 1 : 0);
|
|
|
|
return ListView.builder(
|
|
controller: _scrollController,
|
|
itemCount: itemCount,
|
|
itemBuilder: (context, index) {
|
|
// Dernier élément = indicateur de chargement
|
|
if (index == containers.length) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: provider.isLoadingMore
|
|
? const CircularProgressIndicator()
|
|
: const SizedBox.shrink(),
|
|
),
|
|
);
|
|
}
|
|
|
|
return _buildContainerCard(containers[index]);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildContainerCard(ContainerModel container) {
|
|
final isSelected = isItemSelected(container.id);
|
|
|
|
return ManagementCard<ContainerModel>(
|
|
item: container,
|
|
getId: (c) => c.id,
|
|
getTitle: (c) => c.id,
|
|
getSubtitle: (c) => c.name,
|
|
getIcon: (c) => c.type.getIcon(
|
|
size: 40,
|
|
color: AppColors.rouge,
|
|
),
|
|
getInfoChips: (c) => [
|
|
InfoChip(
|
|
label: c.type.label,
|
|
icon: Icons.category,
|
|
),
|
|
InfoChip(
|
|
label: '${c.itemCount} items',
|
|
icon: Icons.inventory,
|
|
),
|
|
],
|
|
getStatusBadge: (c) => StatusBadge(
|
|
label: c.status.label,
|
|
color: c.status.color,
|
|
),
|
|
actions: const [
|
|
CardAction(
|
|
id: 'view',
|
|
label: 'Voir détails',
|
|
icon: Icons.visibility,
|
|
),
|
|
CardAction(
|
|
id: 'edit',
|
|
label: 'Modifier',
|
|
icon: Icons.edit,
|
|
),
|
|
CardAction(
|
|
id: 'qr',
|
|
label: 'QR Code',
|
|
icon: Icons.qr_code,
|
|
),
|
|
CardAction(
|
|
id: 'delete',
|
|
label: 'Supprimer',
|
|
icon: Icons.delete,
|
|
color: Colors.red,
|
|
),
|
|
],
|
|
onActionSelected: _handleMenuAction,
|
|
onTap: () {
|
|
if (isSelectionMode) {
|
|
toggleItemSelection(container.id);
|
|
} else {
|
|
_viewContainerDetails(container);
|
|
}
|
|
},
|
|
onLongPress: () {
|
|
if (!isSelectionMode) {
|
|
toggleSelectionMode();
|
|
toggleItemSelection(container.id);
|
|
}
|
|
},
|
|
isSelectionMode: isSelectionMode,
|
|
isSelected: isSelected,
|
|
onSelectionChanged: (value) {
|
|
toggleItemSelection(container.id);
|
|
},
|
|
);
|
|
}
|
|
|
|
|
|
void _handleMenuAction(String action, ContainerModel container) {
|
|
switch (action) {
|
|
case 'view':
|
|
_viewContainerDetails(container);
|
|
break;
|
|
case 'edit':
|
|
_editContainer(container);
|
|
break;
|
|
case 'qr':
|
|
_showQRCode(container);
|
|
break;
|
|
case 'delete':
|
|
_deleteContainer(container);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// Afficher le QR code d'un conteneur
|
|
void _showQRCode(ContainerModel container) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => QRCodeDialog.forContainer(container),
|
|
);
|
|
}
|
|
|
|
void _navigateToForm(BuildContext context) async {
|
|
final result = await Navigator.pushNamed(context, '/container_form');
|
|
if (result == true) {
|
|
// Rafraîchir la liste
|
|
}
|
|
}
|
|
|
|
void _viewContainerDetails(ContainerModel container) async {
|
|
await Navigator.pushNamed(
|
|
context,
|
|
'/container_detail',
|
|
arguments: container,
|
|
);
|
|
}
|
|
|
|
void _editContainer(ContainerModel container) async {
|
|
await Navigator.pushNamed(
|
|
context,
|
|
'/container_form',
|
|
arguments: container,
|
|
);
|
|
}
|
|
|
|
|
|
Future<void> _generateQRCodesForSelected() async {
|
|
if (!hasSelection) return;
|
|
|
|
// Afficher un indicateur de chargement
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => const Center(
|
|
child: CircularProgressIndicator(color: AppColors.rouge),
|
|
),
|
|
);
|
|
|
|
try {
|
|
// Récupérer les containers sélectionnés
|
|
final containerProvider = context.read<ContainerProvider>();
|
|
final List<ContainerModel> selectedContainers = [];
|
|
final Map<String, List<EquipmentModel>> containerEquipmentMap = {};
|
|
|
|
for (final id in selectedIds) {
|
|
final container = await containerProvider.getContainerById(id);
|
|
if (container != null) {
|
|
selectedContainers.add(container);
|
|
// Charger les équipements pour ce container
|
|
final equipment = await containerProvider.getContainerEquipment(id);
|
|
containerEquipmentMap[id] = equipment;
|
|
}
|
|
}
|
|
|
|
// Fermer l'indicateur de chargement
|
|
if (mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
|
|
if (selectedContainers.isEmpty) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Aucun container trouvé')),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Afficher le dialogue de sélection de format avec le widget générique
|
|
if (mounted) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => QRCodeFormatSelectorDialog<ContainerModel>(
|
|
itemList: selectedContainers,
|
|
getId: (c) => c.id,
|
|
getTitle: (c) => c.name,
|
|
getDetails: (ContainerModel c) {
|
|
final equipment = containerEquipmentMap[c.id] ?? <EquipmentModel>[];
|
|
return [
|
|
'Contenu (${equipment.length}):',
|
|
...equipment.take(5).map((eq) => '- ${eq.id}'),
|
|
if (equipment.length > 5) '... +${equipment.length - 5}',
|
|
];
|
|
},
|
|
dialogTitle: 'Générer ${selectedContainers.length} QR Code(s)',
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
// Fermer l'indicateur si une erreur survient
|
|
if (mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
|
|
DebugLog.error('[ContainerManagementPage] Error generating QR codes', e);
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Erreur lors de la génération : ${e.toString()}'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _deleteContainer(ContainerModel container) async {
|
|
final confirm = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Confirmer la suppression'),
|
|
content: Text(
|
|
'Êtes-vous sûr de vouloir supprimer le container "${container.name}" ?\n\n'
|
|
'Cette action est irréversible.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
|
child: const Text('Supprimer'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirm == true && mounted) {
|
|
try {
|
|
await context.read<ContainerProvider>().deleteContainer(container.id);
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Container supprimé avec succès')),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Erreur lors de la suppression: $e')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _deleteSelectedContainers() async {
|
|
final confirm = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Confirmer la suppression'),
|
|
content: Text(
|
|
'Êtes-vous sûr de vouloir supprimer $selectedCount container(s) ?\n\n'
|
|
'Cette action est irréversible.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
|
child: const Text('Supprimer'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirm == true && mounted) {
|
|
try {
|
|
final provider = context.read<ContainerProvider>();
|
|
for (final id in selectedIds) {
|
|
await provider.deleteContainer(id);
|
|
}
|
|
disableSelectionMode();
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Containers supprimés avec succès')),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Erreur lors de la suppression: $e')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Scanner un QR Code et ouvrir la vue de détail correspondante
|
|
Future<void> _scanQRCode() async {
|
|
try {
|
|
// Ouvrir le scanner
|
|
final scannedCode = await showDialog<String>(
|
|
context: context,
|
|
builder: (context) => const QRCodeScannerDialog(),
|
|
);
|
|
|
|
if (scannedCode == null || scannedCode.isEmpty) {
|
|
return; // L'utilisateur a annulé
|
|
}
|
|
|
|
if (!mounted) return;
|
|
|
|
// Afficher un indicateur de chargement
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => const Center(
|
|
child: CircularProgressIndicator(color: AppColors.rouge),
|
|
),
|
|
);
|
|
|
|
// Rechercher d'abord dans les conteneurs
|
|
final containerProvider = context.read<ContainerProvider>();
|
|
if (containerProvider.containers.isEmpty) {
|
|
await containerProvider.loadContainers();
|
|
}
|
|
|
|
final container = containerProvider.containers.firstWhere(
|
|
(c) => c.id == scannedCode,
|
|
orElse: () => ContainerModel(
|
|
id: '',
|
|
name: '',
|
|
type: ContainerType.flightCase,
|
|
status: EquipmentStatus.available,
|
|
equipmentIds: [],
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
),
|
|
);
|
|
|
|
if (mounted) {
|
|
Navigator.of(context).pop(); // Fermer l'indicateur
|
|
}
|
|
|
|
if (container.id.isNotEmpty) {
|
|
// Conteneur trouvé
|
|
if (mounted) {
|
|
Navigator.pushNamed(
|
|
context,
|
|
'/container_detail',
|
|
arguments: container,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Si pas trouvé dans les conteneurs, chercher dans les équipements
|
|
final equipmentProvider = context.read<EquipmentProvider>();
|
|
await equipmentProvider.ensureLoaded();
|
|
|
|
final equipment = equipmentProvider.allEquipment.firstWhere(
|
|
(eq) => eq.id == scannedCode,
|
|
orElse: () => EquipmentModel(
|
|
id: '',
|
|
name: '',
|
|
category: EquipmentCategory.other,
|
|
status: EquipmentStatus.available,
|
|
maintenanceIds: [],
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
),
|
|
);
|
|
|
|
if (equipment.id.isNotEmpty) {
|
|
// Équipement trouvé
|
|
if (mounted) {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => EquipmentDetailPage(equipment: equipment),
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Rien trouvé
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Aucun conteneur ou équipement trouvé avec l\'ID : $scannedCode'),
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
DebugLog.error('[ContainerManagementPage] Error scanning QR code', e);
|
|
if (mounted) {
|
|
// Fermer l'indicateur si ouvert
|
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Erreur lors du scan : ${e.toString()}'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|