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/models/equipment_model.dart'; import 'package:em2rp/views/equipment_form_page.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_format_selector_dialog.dart'; import 'package:em2rp/mixins/selection_mode_mixin.dart'; class EquipmentManagementPage extends StatefulWidget { const EquipmentManagementPage({super.key}); @override State createState() => _EquipmentManagementPageState(); } class _EquipmentManagementPageState extends State with SelectionModeMixin { final TextEditingController _searchController = TextEditingController(); EquipmentCategory? _selectedCategory; @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().setSearchQuery(''); }, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), onChanged: (value) { context.read().setSearchQuery(value); }, ), ), 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() .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), ), ), ), 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() .setSearchQuery(''); }, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), isDense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), onChanged: (value) { context.read().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() .setSelectedCategory(null); }, ), ..._buildCategoryListTiles(), ], ), ), ], ), ), // Contenu principal Expanded(child: _buildEquipmentList()), ], ); } List _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().setSelectedCategory(category); } }, selectedColor: AppColors.rouge, labelStyle: TextStyle( color: color, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), ); }).toList(); } List _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().setSelectedCategory(category); }, ); }).toList(); } Widget _buildEquipmentList() { return Consumer( builder: (context, provider, child) { return StreamBuilder>( stream: provider.equipmentStream, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Center( child: Text('Erreur: ${snapshot.error}'), ); } if (!snapshot.hasData || snapshot.data!.isEmpty) { 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]), ), const SizedBox(height: 8), Text( 'Ajoutez votre premier équipement', style: TextStyle(fontSize: 14, color: Colors.grey[500]), ), ], ), ); } // Tri par nom final equipment = snapshot.data!; equipment.sort((a, b) => a.name.compareTo(b.name)); return ListView.builder( padding: const EdgeInsets.all(16), itemCount: equipment.length, itemBuilder: (context, index) { return _buildEquipmentCard(equipment[index]); }, ); }, ); }, ); } Widget _buildEquipmentCard(EquipmentModel equipment) { final isSelected = isItemSelected(equipment.id); return Card( margin: const EdgeInsets.only(bottom: 12), color: isSelectionMode && isSelected ? AppColors.rouge.withOpacity(0.1) : null, child: ListTile( leading: isSelectionMode ? Checkbox( value: isSelected, onChanged: (value) => toggleItemSelection(equipment.id), activeColor: AppColors.rouge, ) : CircleAvatar( backgroundColor: equipment.status.color.withOpacity(0.2), child: equipment.category.getIcon( size: 20, color: Colors.black, ), ), title: Row( children: [ Expanded( child: Text( equipment.id, style: const TextStyle(fontWeight: FontWeight.bold), ), ), // Afficher le statut uniquement si ce n'est pas un consommable ou câble if (equipment.category != EquipmentCategory.consumable && equipment.category != EquipmentCategory.cable) _buildStatusBadge(equipment.status), ], ), 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, ), ), ), ], ], ), ); } Widget _buildStatusBadge(EquipmentStatus status) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: status.color.withOpacity(0.2), borderRadius: BorderRadius.circular(12), border: Border.all(color: status.color), ), child: Text( status.label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: status.color, ), ), ); } // 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() .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(); 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; // Récupérer les équipements sélectionnés final provider = context.read(); final List 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; } if (selectedEquipment.isEmpty) return; if (selectedEquipment.length == 1) { // Un seul équipement : afficher le dialogue simple showDialog( context: context, builder: (context) => QRCodeDialog.forEquipment(selectedEquipment.first), ); } else { // Plusieurs équipements : afficher le sélecteur de format showDialog( context: context, builder: (context) => QRCodeFormatSelectorDialog( equipmentList: selectedEquipment, ), ); } } 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(), }; await context.read().updateEquipment( equipment.id, updatedData, ); 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), ), ); } }