Cette mise à jour améliore la génération de QR codes pour les équipements et les containers en ajoutant un retour visuel à l'utilisateur et une gestion des erreurs plus robuste. **Changements :** - **Ajout d'un indicateur de chargement :** Un `CircularProgressIndicator` est désormais affiché pendant que les données des équipements ou des containers sélectionnés sont récupérées, informant l'utilisateur qu'une opération est en cours. - **Gestion des erreurs :** Un bloc `try...catch` a été ajouté autour de la logique de génération dans les pages de gestion des équipements (`EquipmentManagementPage`) et des containers (`ContainerManagementPage`). - **Affichage des erreurs :** En cas d'échec, le chargement est stoppé et une `SnackBar` rouge apparaît pour notifier l'utilisateur de l'erreur, améliorant ainsi la robustesse de la fonctionnalité.
1183 lines
41 KiB
Dart
1183 lines
41 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/equipment_provider.dart';
|
|
import 'package:em2rp/providers/container_provider.dart';
|
|
import 'package:em2rp/models/equipment_model.dart';
|
|
import 'package:em2rp/models/container_model.dart';
|
|
import 'package:em2rp/views/equipment_form_page.dart';
|
|
import 'package:em2rp/views/equipment_detail_page.dart';
|
|
import 'package:em2rp/views/container_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/views/widgets/equipment/equipment_status_badge.dart';
|
|
import 'package:em2rp/utils/debug_log.dart';
|
|
import 'package:em2rp/mixins/selection_mode_mixin.dart';
|
|
import 'package:em2rp/views/widgets/management/management_list.dart';
|
|
|
|
class EquipmentManagementPage extends StatefulWidget {
|
|
const EquipmentManagementPage({super.key});
|
|
|
|
@override
|
|
State<EquipmentManagementPage> createState() =>
|
|
_EquipmentManagementPageState();
|
|
}
|
|
|
|
|
|
class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|
with SelectionModeMixin<EquipmentManagementPage> {
|
|
final TextEditingController _searchController = TextEditingController();
|
|
EquipmentCategory? _selectedCategory;
|
|
List<EquipmentModel>? _cachedEquipment;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
DebugLog.info('[EquipmentManagementPage] initState called');
|
|
// Charger les équipements au démarrage
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
DebugLog.info('[EquipmentManagementPage] Loading equipments...');
|
|
context.read<EquipmentProvider>().loadEquipments();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_searchController.dispose();
|
|
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: '/equipment_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 du matériel.',
|
|
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: [
|
|
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: _deleteSelectedEquipment,
|
|
),
|
|
],
|
|
],
|
|
)
|
|
: CustomAppBar(
|
|
title: 'Gestion du matériel',
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.checklist),
|
|
tooltip: 'Mode sélection',
|
|
onPressed: toggleSelectionMode,
|
|
),
|
|
],
|
|
),
|
|
drawer: const MainDrawer(currentPage: '/equipment_management'),
|
|
body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(),
|
|
floatingActionButton: isSelectionMode ? null : _buildFAB(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFAB() {
|
|
return PermissionGate(
|
|
requiredPermissions: const ['manage_equipment'],
|
|
child: FloatingActionButton.extended(
|
|
onPressed: _createNewEquipment,
|
|
backgroundColor: AppColors.rouge,
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('Ajouter un équipement'),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMobileLayout() {
|
|
return Column(
|
|
children: [
|
|
// Barre de recherche et bouton boîtes
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _searchController,
|
|
decoration: InputDecoration(
|
|
hintText: 'Rechercher par nom, modèle ou ID...',
|
|
prefixIcon: const Icon(Icons.search),
|
|
suffixIcon: _searchController.text.isNotEmpty
|
|
? IconButton(
|
|
icon: const Icon(Icons.clear),
|
|
onPressed: () {
|
|
_searchController.clear();
|
|
context.read<EquipmentProvider>().setSearchQuery('');
|
|
},
|
|
)
|
|
: null,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
onChanged: (value) {
|
|
context.read<EquipmentProvider>().setSearchQuery(value);
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// Bouton Scanner QR
|
|
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,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// Bouton Gérer les boîtes
|
|
IconButton.filled(
|
|
onPressed: () {
|
|
Navigator.pushNamed(context, '/container_management');
|
|
},
|
|
icon: const Icon(Icons.inventory_2),
|
|
tooltip: 'Gérer les boîtes',
|
|
style: IconButton.styleFrom(
|
|
backgroundColor: AppColors.rouge,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// Menu horizontal de filtres par catégorie
|
|
SizedBox(
|
|
height: 60,
|
|
child: ListView(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
|
child: ChoiceChip(
|
|
label: const Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.all_inclusive, size: 16, color: Colors.white),
|
|
SizedBox(width: 8),
|
|
Text('Tout'),
|
|
],
|
|
),
|
|
selected: _selectedCategory == null,
|
|
onSelected: (selected) {
|
|
if (selected) {
|
|
setState(() => _selectedCategory = null);
|
|
context
|
|
.read<EquipmentProvider>()
|
|
.setSelectedCategory(null);
|
|
}
|
|
},
|
|
selectedColor: AppColors.rouge,
|
|
labelStyle: TextStyle(
|
|
color: _selectedCategory == null
|
|
? Colors.white
|
|
: AppColors.rouge,
|
|
fontWeight: _selectedCategory == null
|
|
? FontWeight.bold
|
|
: FontWeight.normal,
|
|
),
|
|
),
|
|
),
|
|
..._buildCategoryChips(),
|
|
],
|
|
),
|
|
),
|
|
const Divider(),
|
|
// Liste des équipements
|
|
Expanded(child: _buildEquipmentList()),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildDesktopLayout() {
|
|
return Row(
|
|
children: [
|
|
// Sidebar gauche avec filtres
|
|
Container(
|
|
width: 280,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
border: const Border(
|
|
right: BorderSide(color: Colors.grey, width: 1),
|
|
),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Bouton Gérer les boîtes
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: ElevatedButton.icon(
|
|
onPressed: () {
|
|
Navigator.pushNamed(context, '/container_management');
|
|
},
|
|
icon: const Icon(Icons.inventory_2, color: Colors.white),
|
|
label: const Text(
|
|
'Gérer les boîtes',
|
|
style: TextStyle(color: Colors.white),
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.rouge,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 20,
|
|
vertical: 16,
|
|
),
|
|
minimumSize: const Size(double.infinity, 50),
|
|
),
|
|
),
|
|
),
|
|
// Bouton Scanner QR
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 16.0),
|
|
child: ElevatedButton.icon(
|
|
onPressed: _scanQRCode,
|
|
icon: const Icon(Icons.qr_code_scanner, color: Colors.white),
|
|
label: const Text(
|
|
'Scanner QR Code',
|
|
style: TextStyle(color: Colors.white),
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.grey[700],
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 20,
|
|
vertical: 16,
|
|
),
|
|
minimumSize: const Size(double.infinity, 50),
|
|
),
|
|
),
|
|
),
|
|
const Divider(),
|
|
// En-tête filtres
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.filter_list, color: AppColors.rouge, size: 20),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
'Filtres',
|
|
style:
|
|
Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.rouge,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// Barre de recherche
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: TextField(
|
|
controller: _searchController,
|
|
decoration: InputDecoration(
|
|
hintText: 'Rechercher...',
|
|
prefixIcon: const Icon(Icons.search, size: 20),
|
|
suffixIcon: _searchController.text.isNotEmpty
|
|
? IconButton(
|
|
icon: const Icon(Icons.clear, size: 20),
|
|
onPressed: () {
|
|
_searchController.clear();
|
|
context
|
|
.read<EquipmentProvider>()
|
|
.setSearchQuery('');
|
|
},
|
|
)
|
|
: null,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
isDense: true,
|
|
contentPadding:
|
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
),
|
|
onChanged: (value) {
|
|
context.read<EquipmentProvider>().setSearchQuery(value);
|
|
},
|
|
),
|
|
),
|
|
// Filtres par catégorie
|
|
Padding(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
'Catégories',
|
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: ListView(
|
|
children: [
|
|
ListTile(
|
|
leading: Icon(
|
|
Icons.all_inclusive,
|
|
color: _selectedCategory == null
|
|
? AppColors.rouge
|
|
: Colors.grey[600],
|
|
),
|
|
title: Text(
|
|
'Tout',
|
|
style: TextStyle(
|
|
color: _selectedCategory == null
|
|
? AppColors.rouge
|
|
: Colors.black87,
|
|
fontWeight: _selectedCategory == null
|
|
? FontWeight.bold
|
|
: FontWeight.normal,
|
|
),
|
|
),
|
|
selected: _selectedCategory == null,
|
|
selectedTileColor: AppColors.rouge.withOpacity(0.1),
|
|
onTap: () {
|
|
setState(() => _selectedCategory = null);
|
|
context
|
|
.read<EquipmentProvider>()
|
|
.setSelectedCategory(null);
|
|
},
|
|
),
|
|
..._buildCategoryListTiles(),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// Contenu principal
|
|
Expanded(child: _buildEquipmentList()),
|
|
],
|
|
);
|
|
}
|
|
|
|
List<Widget> _buildCategoryChips() {
|
|
return EquipmentCategory.values.map((category) {
|
|
final isSelected = _selectedCategory == category;
|
|
final color = isSelected ? Colors.white : AppColors.rouge;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
|
child: ChoiceChip(
|
|
label: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
category.getIcon(
|
|
size: 16,
|
|
color: color,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(category.label),
|
|
],
|
|
),
|
|
selected: isSelected,
|
|
onSelected: (selected) {
|
|
if (selected) {
|
|
setState(() => _selectedCategory = category);
|
|
context.read<EquipmentProvider>().setSelectedCategory(category);
|
|
}
|
|
},
|
|
selectedColor: AppColors.rouge,
|
|
labelStyle: TextStyle(
|
|
color: color,
|
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
),
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
List<Widget> _buildCategoryListTiles() {
|
|
return EquipmentCategory.values.map((category) {
|
|
final isSelected = _selectedCategory == category;
|
|
final color = isSelected ? AppColors.rouge : Colors.grey[600]!;
|
|
|
|
return ListTile(
|
|
leading: category.getIcon(
|
|
size: 24,
|
|
color: color,
|
|
),
|
|
title: Text(
|
|
category.label,
|
|
style: TextStyle(
|
|
color: isSelected ? AppColors.rouge : Colors.black87,
|
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
),
|
|
selected: isSelected,
|
|
selectedTileColor: AppColors.rouge.withOpacity(0.1),
|
|
onTap: () {
|
|
setState(() => _selectedCategory = category);
|
|
context.read<EquipmentProvider>().setSelectedCategory(category);
|
|
},
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
Widget _buildEquipmentList() {
|
|
return Consumer<EquipmentProvider>(
|
|
builder: (context, provider, child) {
|
|
DebugLog.info('[EquipmentManagementPage] Building list - isLoading: ${provider.isLoading}, equipment count: ${provider.equipment.length}');
|
|
|
|
if (provider.isLoading && _cachedEquipment == null) {
|
|
DebugLog.info('[EquipmentManagementPage] Showing loading indicator');
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
final equipments = provider.equipment;
|
|
|
|
if (equipments.isEmpty && !provider.isLoading) {
|
|
DebugLog.info('[EquipmentManagementPage] No equipment found');
|
|
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 équipement trouvé',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
DebugLog.info('[EquipmentManagementPage] Building list with ${equipments.length} items');
|
|
return ListView.builder(
|
|
itemCount: equipments.length,
|
|
itemBuilder: (context, index) {
|
|
return _buildEquipmentCard(equipments[index]);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildEquipmentCard(EquipmentModel equipment) {
|
|
final isSelected = isItemSelected(equipment.id);
|
|
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
color: isSelectionMode && isSelected
|
|
? AppColors.rouge.withValues(alpha: 0.1)
|
|
: null,
|
|
child: ListTile(
|
|
leading: isSelectionMode
|
|
? Checkbox(
|
|
value: isSelected,
|
|
onChanged: (value) => toggleItemSelection(equipment.id),
|
|
activeColor: AppColors.rouge,
|
|
)
|
|
: CircleAvatar(
|
|
backgroundColor: equipment.category.color.withValues(alpha: 0.2),
|
|
child: equipment.category.getIcon(
|
|
size: 20,
|
|
color: equipment.category.color,
|
|
),
|
|
),
|
|
title: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
equipment.id,
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
// Afficher le badge de statut calculé dynamiquement
|
|
if (equipment.category != EquipmentCategory.consumable &&
|
|
equipment.category != EquipmentCategory.cable)
|
|
EquipmentStatusBadge(equipment: equipment),
|
|
],
|
|
),
|
|
subtitle: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'${equipment.brand ?? ''} ${equipment.model ?? ''}'
|
|
.trim()
|
|
.isNotEmpty
|
|
? '${equipment.brand ?? ''} ${equipment.model ?? ''}'.trim()
|
|
: 'Marque/Modèle non défini',
|
|
style: TextStyle(color: Colors.grey[600], fontSize: 14),
|
|
),
|
|
// Afficher la quantité disponible pour les consommables/câbles
|
|
if (equipment.category == EquipmentCategory.consumable ||
|
|
equipment.category == EquipmentCategory.cable) ...[
|
|
const SizedBox(height: 4),
|
|
_buildQuantityDisplay(equipment),
|
|
],
|
|
],
|
|
),
|
|
trailing: isSelectionMode
|
|
? null
|
|
: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Bouton Restock (uniquement pour consommables/câbles avec permission)
|
|
if (equipment.category == EquipmentCategory.consumable ||
|
|
equipment.category == EquipmentCategory.cable)
|
|
PermissionGate(
|
|
requiredPermissions: const ['manage_equipment'],
|
|
child: IconButton(
|
|
icon: const Icon(Icons.add_shopping_cart,
|
|
color: AppColors.rouge),
|
|
tooltip: 'Restock',
|
|
onPressed: () => _showRestockDialog(equipment),
|
|
),
|
|
),
|
|
// Bouton QR Code
|
|
IconButton(
|
|
icon: const Icon(Icons.qr_code, color: AppColors.rouge),
|
|
tooltip: 'QR Code',
|
|
onPressed: () => showDialog(
|
|
context: context,
|
|
builder: (context) => QRCodeDialog.forEquipment(equipment),
|
|
),
|
|
),
|
|
// Bouton Modifier (permission required)
|
|
PermissionGate(
|
|
requiredPermissions: const ['manage_equipment'],
|
|
child: IconButton(
|
|
icon: const Icon(Icons.edit, color: AppColors.rouge),
|
|
tooltip: 'Modifier',
|
|
onPressed: () => _editEquipment(equipment),
|
|
),
|
|
),
|
|
// Bouton Supprimer (permission required)
|
|
PermissionGate(
|
|
requiredPermissions: const ['manage_equipment'],
|
|
child: IconButton(
|
|
icon: const Icon(Icons.delete, color: Colors.red),
|
|
tooltip: 'Supprimer',
|
|
onPressed: () => _deleteEquipment(equipment),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
onTap: isSelectionMode
|
|
? () => toggleItemSelection(equipment.id)
|
|
: () => _viewEquipmentDetails(equipment),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildQuantityDisplay(EquipmentModel equipment) {
|
|
final availableQty = equipment.availableQuantity ?? 0;
|
|
final totalQty = equipment.totalQuantity ?? 0;
|
|
final criticalThreshold = equipment.criticalThreshold ?? 0;
|
|
final isCritical =
|
|
criticalThreshold > 0 && availableQty <= criticalThreshold;
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: isCritical
|
|
? Colors.red.withOpacity(0.15)
|
|
: Colors.grey.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: isCritical ? Colors.red : Colors.grey.shade400,
|
|
width: isCritical ? 2 : 1,
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
isCritical ? Icons.warning : Icons.inventory,
|
|
size: 16,
|
|
color: isCritical ? Colors.red : Colors.grey[700],
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
'Disponible: $availableQty / $totalQty',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: isCritical ? FontWeight.bold : FontWeight.normal,
|
|
color: isCritical ? Colors.red : Colors.grey[700],
|
|
),
|
|
),
|
|
if (isCritical) ...[
|
|
const SizedBox(width: 6),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: const Text(
|
|
'CRITIQUE',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
|
|
// Actions
|
|
void _createNewEquipment() {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const EquipmentFormPage(),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _editEquipment(EquipmentModel equipment) {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => EquipmentFormPage(equipment: equipment),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _deleteEquipment(EquipmentModel equipment) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Confirmer la suppression'),
|
|
content: Text('Voulez-vous vraiment supprimer "${equipment.name}" ?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Annuler'),
|
|
),
|
|
TextButton(
|
|
onPressed: () async {
|
|
Navigator.pop(context);
|
|
try {
|
|
await context
|
|
.read<EquipmentProvider>()
|
|
.deleteEquipment(equipment.id);
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Équipement supprimé avec succès')),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Erreur: $e')),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
|
child: const Text('Supprimer'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _deleteSelectedEquipment() async {
|
|
if (!hasSelection) return;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Confirmer la suppression'),
|
|
content: Text(
|
|
'Voulez-vous vraiment supprimer $selectedCount équipement(s) ?',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Annuler'),
|
|
),
|
|
TextButton(
|
|
onPressed: () async {
|
|
Navigator.pop(context);
|
|
try {
|
|
final provider = context.read<EquipmentProvider>();
|
|
for (final id in selectedIds) {
|
|
await provider.deleteEquipment(id);
|
|
}
|
|
disableSelectionMode();
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'$selectedCount équipement(s) supprimé(s) avec succès'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Erreur: $e')),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
|
child: const Text('Supprimer'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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 équipements sélectionnés
|
|
final provider = context.read<EquipmentProvider>();
|
|
final List<EquipmentModel> selectedEquipment = [];
|
|
|
|
// On doit récupérer les équipements depuis le stream
|
|
await for (final equipmentList in provider.equipmentStream.take(1)) {
|
|
for (final equipment in equipmentList) {
|
|
if (isItemSelected(equipment.id)) {
|
|
selectedEquipment.add(equipment);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Fermer l'indicateur de chargement
|
|
if (mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
|
|
if (selectedEquipment.isEmpty) return;
|
|
|
|
if (selectedEquipment.length == 1) {
|
|
// Un seul équipement : afficher le dialogue simple
|
|
if (mounted) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => QRCodeDialog.forEquipment(selectedEquipment.first),
|
|
);
|
|
}
|
|
} else {
|
|
// Plusieurs équipements : afficher le sélecteur de format
|
|
if (mounted) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => QRCodeFormatSelectorDialog<EquipmentModel>(
|
|
itemList: selectedEquipment,
|
|
getId: (eq) => eq.id,
|
|
getTitle: (eq) => '${eq.brand ?? ''} ${eq.model ?? ''}'.trim(),
|
|
dialogTitle: 'Générer ${selectedEquipment.length} QR Code(s)',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Fermer l'indicateur si une erreur survient
|
|
if (mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
|
|
DebugLog.error('[EquipmentManagementPage] 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,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _showRestockDialog(EquipmentModel equipment) {
|
|
final TextEditingController quantityController = TextEditingController();
|
|
bool addToTotal = false;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => StatefulBuilder(
|
|
builder: (context, setState) {
|
|
return AlertDialog(
|
|
title: Row(
|
|
children: [
|
|
const Icon(Icons.add_shopping_cart, color: AppColors.rouge),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text('Restock - ${equipment.name}'),
|
|
),
|
|
],
|
|
),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Quantités actuelles',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text('Disponible:'),
|
|
Text(
|
|
'${equipment.availableQuantity ?? 0}',
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text('Total:'),
|
|
Text(
|
|
'${equipment.totalQuantity ?? 0}',
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
TextField(
|
|
controller: quantityController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Quantité à ajouter/retirer',
|
|
border: OutlineInputBorder(),
|
|
prefixIcon: Icon(Icons.inventory),
|
|
hintText: 'Ex: 10 ou -5',
|
|
helperText:
|
|
'Nombre positif pour ajouter, négatif pour retirer',
|
|
),
|
|
keyboardType:
|
|
const TextInputType.numberWithOptions(signed: true),
|
|
autofocus: true,
|
|
),
|
|
const SizedBox(height: 16),
|
|
CheckboxListTile(
|
|
title: const Text('Ajouter à la quantité totale'),
|
|
subtitle:
|
|
const Text('Mettre à jour aussi la quantité totale'),
|
|
value: addToTotal,
|
|
contentPadding: EdgeInsets.zero,
|
|
onChanged: (bool? value) {
|
|
setState(() {
|
|
addToTotal = value ?? false;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
final quantityText = quantityController.text.trim();
|
|
if (quantityText.isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Veuillez entrer une quantité')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
final quantity = int.tryParse(quantityText);
|
|
if (quantity == null) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Quantité invalide')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
Navigator.pop(context);
|
|
|
|
try {
|
|
final currentAvailable = equipment.availableQuantity ?? 0;
|
|
final currentTotal = equipment.totalQuantity ?? 0;
|
|
|
|
final newAvailable = currentAvailable + quantity;
|
|
final newTotal =
|
|
addToTotal ? currentTotal + quantity : currentTotal;
|
|
|
|
if (newAvailable < 0) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'La quantité disponible ne peut pas être négative')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (newTotal < 0) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'La quantité totale ne peut pas être négative')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
final updatedData = {
|
|
'availableQuantity': newAvailable,
|
|
'totalQuantity': newTotal,
|
|
'updatedAt': DateTime.now().toIso8601String(),
|
|
};
|
|
|
|
final updatedEquipment = equipment.copyWith(
|
|
availableQuantity: newAvailable,
|
|
totalQuantity: newTotal,
|
|
updatedAt: DateTime.now(),
|
|
);
|
|
|
|
await context.read<EquipmentProvider>().updateEquipment(updatedEquipment);
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
quantity > 0
|
|
? 'Ajout de $quantity unité(s) effectué'
|
|
: 'Retrait de ${quantity.abs()} unité(s) effectué',
|
|
),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Erreur: $e')),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.rouge,
|
|
),
|
|
child: const Text('Valider',
|
|
style: TextStyle(color: Colors.white)),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
void _viewEquipmentDetails(EquipmentModel equipment) {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => EquipmentDetailPage(equipment: equipment),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 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 é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,
|
|
parentBoxIds: [],
|
|
maintenanceIds: [],
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
),
|
|
);
|
|
|
|
if (mounted) {
|
|
Navigator.of(context).pop(); // Fermer l'indicateur
|
|
}
|
|
|
|
if (equipment.id.isNotEmpty) {
|
|
// Équipement trouvé
|
|
if (mounted) {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => EquipmentDetailPage(equipment: equipment),
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Si pas trouvé dans les équipements, chercher 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 (container.id.isNotEmpty) {
|
|
// Conteneur trouvé
|
|
if (mounted) {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => ContainerDetailPage(container: container),
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Rien trouvé
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Aucun équipement ou conteneur trouvé avec l\'ID : $scannedCode'),
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
DebugLog.error('[EquipmentManagementPage] 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,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|