import 'package:firebase_storage/firebase_storage.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/models/equipment_model.dart'; import 'package:em2rp/views/equipment_form_page.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:printing/printing.dart'; import 'dart:typed_data'; import 'dart:ui' as ui; class EquipmentManagementPage extends StatefulWidget { const EquipmentManagementPage({super.key}); @override State createState() => _EquipmentManagementPageState(); } enum QRLabelFormat { small, medium, large } class _EquipmentManagementPageState extends State { final TextEditingController _searchController = TextEditingController(); EquipmentCategory? _selectedCategory; bool _isSelectionMode = false; final Set _selectedEquipmentIds = {}; void _toggleSelectionMode() { setState(() { _isSelectionMode = !_isSelectionMode; if (!_isSelectionMode) { _selectedEquipmentIds.clear(); } }); } void _toggleEquipmentSelection(String id) { setState(() { if (_selectedEquipmentIds.contains(id)) { _selectedEquipmentIds.remove(id); } else { _selectedEquipmentIds.add(id); } }); } @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( '${_selectedEquipmentIds.length} sélectionné(s)', style: const TextStyle(color: Colors.white), ), actions: [ if (_selectedEquipmentIds.isNotEmpty) ...[ 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 Padding( padding: const EdgeInsets.all(16.0), 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); }, ), ), // 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: [ // En-tête Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.rouge.withOpacity(0.1), ), child: Row( children: [ Icon(Icons.inventory, color: AppColors.rouge), 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); }, ), ), const Divider(), // 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() { final categories = [ (EquipmentCategory.lighting, Icons.light_mode, 'Lumière'), (EquipmentCategory.sound, Icons.volume_up, 'Son'), (EquipmentCategory.video, Icons.videocam, 'Vidéo'), (EquipmentCategory.effect, Icons.auto_awesome, 'Effets'), (EquipmentCategory.structure, Icons.construction, 'Structure'), (EquipmentCategory.consumable, Icons.inventory_2, 'Consommable'), (EquipmentCategory.cable, Icons.cable, 'Câble'), (EquipmentCategory.other, Icons.more_horiz, 'Autre'), ]; return categories.map((cat) { final isSelected = _selectedCategory == cat.$1; return Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: ChoiceChip( label: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( cat.$2, size: 16, color: isSelected ? Colors.white : AppColors.rouge, ), const SizedBox(width: 8), Text(cat.$3), ], ), selected: isSelected, onSelected: (selected) { if (selected) { setState(() => _selectedCategory = cat.$1); context.read().setSelectedCategory(cat.$1); } }, selectedColor: AppColors.rouge, labelStyle: TextStyle( color: isSelected ? Colors.white : AppColors.rouge, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), ); }).toList(); } List _buildCategoryListTiles() { final categories = [ (EquipmentCategory.lighting, Icons.light_mode, 'Lumière'), (EquipmentCategory.sound, Icons.volume_up, 'Son'), (EquipmentCategory.video, Icons.videocam, 'Vidéo'), (EquipmentCategory.effect, Icons.auto_awesome, 'Effets'), (EquipmentCategory.structure, Icons.construction, 'Structure'), (EquipmentCategory.consumable, Icons.inventory_2, 'Consommable'), (EquipmentCategory.cable, Icons.cable, 'Câble'), (EquipmentCategory.other, Icons.more_horiz, 'Autre'), ]; return categories.map((cat) { final isSelected = _selectedCategory == cat.$1; return ListTile( leading: Icon( cat.$2, color: isSelected ? AppColors.rouge : Colors.grey[600], ), title: Text( cat.$3, 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 = cat.$1); context.read().setSelectedCategory(cat.$1); }, ); }).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 = _selectedEquipmentIds.contains(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) => _toggleEquipmentSelection(equipment.id), activeColor: AppColors.rouge, ) : CircleAvatar( backgroundColor: _getStatusColor(equipment.status).withOpacity(0.2), child: Icon( _getCategoryIcon(equipment.category), color: _getStatusColor(equipment.status), ), ), 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: () => _showSingleQRCode(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 ? () => _toggleEquipmentSelection(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) { final statusInfo = _getStatusInfo(status); return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: statusInfo.$2.withOpacity(0.2), borderRadius: BorderRadius.circular(12), border: Border.all(color: statusInfo.$2), ), child: Text( statusInfo.$1, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: statusInfo.$2, ), ), ); } (String, Color) _getStatusInfo(EquipmentStatus status) { switch (status) { case EquipmentStatus.available: return ('Disponible', Colors.green); case EquipmentStatus.inUse: return ('En prestation', Colors.blue); case EquipmentStatus.rented: return ('Loué', Colors.orange); case EquipmentStatus.lost: return ('Perdu', Colors.red); case EquipmentStatus.outOfService: return ('HS', Colors.red[900]!); case EquipmentStatus.maintenance: return ('Maintenance', Colors.amber); } } Color _getStatusColor(EquipmentStatus status) { return _getStatusInfo(status).$2; } IconData _getCategoryIcon(EquipmentCategory category) { switch (category) { case EquipmentCategory.lighting: return Icons.light_mode; case EquipmentCategory.sound: return Icons.volume_up; case EquipmentCategory.video: return Icons.videocam; case EquipmentCategory.effect: return Icons.auto_awesome; case EquipmentCategory.structure: return Icons.construction; case EquipmentCategory.consumable: return Icons.inventory_2; case EquipmentCategory.cable: return Icons.cable; case EquipmentCategory.other: return Icons.more_horiz; } } // 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 (_selectedEquipmentIds.isEmpty) return; showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Confirmer la suppression'), content: Text( 'Voulez-vous vraiment supprimer ${_selectedEquipmentIds.length} é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 _selectedEquipmentIds) { await provider.deleteEquipment(id); } setState(() { _selectedEquipmentIds.clear(); _isSelectionMode = false; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('${_selectedEquipmentIds.length} é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 (_selectedEquipmentIds.isEmpty) 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 (_selectedEquipmentIds.contains(equipment.id)) { selectedEquipment.add(equipment); } } break; } if (selectedEquipment.isEmpty) return; if (selectedEquipment.length == 1) { _showSingleQRCode(selectedEquipment.first); } else { _showMultipleQRCodesDialog(selectedEquipment); } } void _showSingleQRCode(EquipmentModel equipment) { showDialog( context: context, builder: (context) => Dialog( child: Container( padding: const EdgeInsets.all(24), constraints: const BoxConstraints(maxWidth: 400), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ const Icon(Icons.qr_code, color: AppColors.rouge, size: 32), const SizedBox(width: 12), Expanded( child: Text( 'QR Code - ${equipment.id}', style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context), ), ], ), const SizedBox(height: 24), // QR Code Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey[300]!), ), child: QrImageView( data: equipment.id, version: QrVersions.auto, size: 250, backgroundColor: Colors.white, ), ), const SizedBox(height: 16), // Informations Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( equipment.id, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), const SizedBox(height: 4), Text( '${equipment.brand ?? ''} ${equipment.model ?? ''}'.trim(), style: TextStyle(color: Colors.grey[700]), ), ], ), ), const SizedBox(height: 24), // Bouton télécharger ElevatedButton.icon( onPressed: () => _downloadSingleQRCodeImage(equipment), style: ElevatedButton.styleFrom( backgroundColor: AppColors.rouge, minimumSize: const Size(double.infinity, 48), ), icon: const Icon(Icons.download, color: Colors.white), label: const Text( 'Télécharger l\'image', style: TextStyle(color: Colors.white), ), ), ], ), ), ), ); } void _showMultipleQRCodesDialog(List equipmentList) { showDialog( context: context, builder: (context) => Dialog( child: Container( padding: const EdgeInsets.all(24), constraints: const BoxConstraints(maxWidth: 600, maxHeight: 700), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ const Icon(Icons.qr_code_2, color: AppColors.rouge, size: 32), const SizedBox(width: 12), Expanded( child: Text( 'Générer ${equipmentList.length} QR Codes', style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context), ), ], ), const SizedBox(height: 24), const Text( 'Choisissez un format d\'étiquette :', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), const SizedBox(height: 16), // Liste des équipements Expanded( child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey[300]!), borderRadius: BorderRadius.circular(8), ), child: ListView.separated( shrinkWrap: true, itemCount: equipmentList.length, separatorBuilder: (context, index) => const Divider(height: 1), itemBuilder: (context, index) { final equipment = equipmentList[index]; return ListTile( dense: true, leading: const Icon(Icons.qr_code, size: 20), title: Text( equipment.id, style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( '${equipment.brand ?? ''} ${equipment.model ?? ''}'.trim(), style: const TextStyle(fontSize: 12), ), ); }, ), ), ), const SizedBox(height: 24), // Boutons de format _buildFormatButton( context, icon: Icons.qr_code, title: 'Petits QR Codes', subtitle: 'QR codes compacts (2x2 cm)', onPressed: () { Navigator.pop(context); _generatePDF(equipmentList, QRLabelFormat.small); }, ), const SizedBox(height: 12), _buildFormatButton( context, icon: Icons.qr_code_2, title: 'QR Moyens', subtitle: 'QR codes taille moyenne (4x4 cm)', onPressed: () { Navigator.pop(context); _generatePDF(equipmentList, QRLabelFormat.medium); }, ), const SizedBox(height: 12), _buildFormatButton( context, icon: Icons.label, title: 'Grandes étiquettes', subtitle: 'QR + ID + Marque/Modèle (10x5 cm)', onPressed: () { Navigator.pop(context); _generatePDF(equipmentList, QRLabelFormat.large); }, ), ], ), ), ), ); } Widget _buildFormatButton( BuildContext context, { required IconData icon, required String title, required String subtitle, required VoidCallback onPressed, }) { return InkWell( onTap: onPressed, borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( border: Border.all(color: Colors.grey[300]!), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(icon, color: AppColors.rouge, size: 32), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), const SizedBox(height: 4), Text( subtitle, style: TextStyle( color: Colors.grey[600], fontSize: 13, ), ), ], ), ), const Icon(Icons.arrow_forward_ios, size: 16), ], ), ), ); } Future _downloadSingleQRCodeImage(EquipmentModel equipment) async { try { // Générer l'image QR code en haute résolution final qrImage = await _generateQRImage(equipment.id, size: 1024); // Utiliser la bibliothèque printing pour sauvegarder l'image await Printing.sharePdf( bytes: qrImage, filename: 'QRCode_${equipment.id}.png', ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Image QR Code téléchargée avec succès'), backgroundColor: Colors.green, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Erreur lors du téléchargement de l\'image: $e')), ); } } } Future _generatePDF(List equipmentList, QRLabelFormat format) async { try { final pdf = pw.Document(); switch (format) { case QRLabelFormat.small: await _generateSmallQRCodesPDF(pdf, equipmentList); break; case QRLabelFormat.medium: await _generateMediumQRCodesPDF(pdf, equipmentList); break; case QRLabelFormat.large: await _generateLargeQRCodesPDF(pdf, equipmentList); break; } await Printing.layoutPdf( onLayout: (format) async => pdf.save(), name: 'QRCodes_${DateTime.now().millisecondsSinceEpoch}.pdf', ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('PDF généré avec succès'), backgroundColor: Colors.green, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Erreur lors de la génération du PDF: $e')), ); } } } Future _generateSmallQRCodesPDF(pw.Document pdf, List equipmentList) async { // Petits QR codes : 2x2 cm, 9 par page (3x3) const qrSize = 56.69; // 2cm en points const itemsPerRow = 4; const itemsPerPage = 20; for (int pageStart = 0; pageStart < equipmentList.length; pageStart += itemsPerPage) { final pageEquipment = equipmentList.skip(pageStart).take(itemsPerPage).toList(); final qrImages = await Future.wait( pageEquipment.map((eq) => _generateQRImage(eq.id)), ); pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4, margin: const pw.EdgeInsets.all(20), build: (context) { return pw.Wrap( spacing: 10, runSpacing: 10, children: List.generate(pageEquipment.length, (index) { return pw.Container( width: qrSize, height: qrSize, child: pw.Image(pw.MemoryImage(qrImages[index])), ); }), ); }, ), ); } } Future _generateMediumQRCodesPDF(pw.Document pdf, List equipmentList) async { // QR moyens : 4x4 cm, 6 par page (2x3) const qrSize = 113.39; // 4cm en points const itemsPerRow = 2; const itemsPerPage = 6; for (int pageStart = 0; pageStart < equipmentList.length; pageStart += itemsPerPage) { final pageEquipment = equipmentList.skip(pageStart).take(itemsPerPage).toList(); final qrImages = await Future.wait( pageEquipment.map((eq) => _generateQRImage(eq.id)), ); pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4, margin: const pw.EdgeInsets.all(20), build: (context) { return pw.Wrap( spacing: 20, runSpacing: 20, children: List.generate(pageEquipment.length, (index) { return pw.Container( width: qrSize, height: qrSize, child: pw.Image(pw.MemoryImage(qrImages[index])), ); }), ); }, ), ); } } Future _generateLargeQRCodesPDF(pw.Document pdf, List equipmentList) async { // Grandes étiquettes : 10x5 cm, 4 par page const labelWidth = 283.46; // 10cm en points const labelHeight = 141.73; // 5cm en points const qrSize = 113.39; // 4cm en points const itemsPerPage = 4; for (int pageStart = 0; pageStart < equipmentList.length; pageStart += itemsPerPage) { final pageEquipment = equipmentList.skip(pageStart).take(itemsPerPage).toList(); final qrImages = await Future.wait( pageEquipment.map((eq) => _generateQRImage(eq.id)), ); pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4, margin: const pw.EdgeInsets.all(20), build: (context) { return pw.Wrap( spacing: 10, runSpacing: 10, children: List.generate(pageEquipment.length, (index) { final equipment = pageEquipment[index]; return pw.Container( width: labelWidth, height: labelHeight, padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( border: pw.Border.all(color: PdfColors.grey300), borderRadius: const pw.BorderRadius.all(pw.Radius.circular(5)), ), child: pw.Row( children: [ pw.Image(pw.MemoryImage(qrImages[index]), width: qrSize, height: qrSize), pw.SizedBox(width: 15), pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, mainAxisAlignment: pw.MainAxisAlignment.center, children: [ pw.Text( equipment.id, style: pw.TextStyle( fontSize: 14, fontWeight: pw.FontWeight.bold, ), ), pw.SizedBox(height: 8), pw.Text( 'Marque: ${equipment.brand ?? 'N/A'}', style: const pw.TextStyle(fontSize: 10), ), pw.SizedBox(height: 4), pw.Text( 'Modèle: ${equipment.model ?? 'N/A'}', style: const pw.TextStyle(fontSize: 10), ), ], ), ), ], ), ); }), ); }, ), ); } } Future _generateQRImage(String data, {double size = 512}) async { final qrValidationResult = QrValidator.validate( data: data, version: QrVersions.auto, errorCorrectionLevel: QrErrorCorrectLevel.L, ); if (qrValidationResult.status != QrValidationStatus.valid) { throw Exception('QR code validation failed'); } final qrCode = qrValidationResult.qrCode!; final painter = QrPainter.withQr( qr: qrCode, color: const Color(0xFF000000), emptyColor: const Color(0xFFFFFFFF), gapless: true, ); final picData = await painter.toImageData(size, format: ui.ImageByteFormat.png); return picData!.buffer.asUint8List(); } 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 _showQRCode(EquipmentModel equipment) { // TODO: Afficher le QR code ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('QR Code pour ${equipment.name} - À implémenter')), ); } void _viewEquipmentDetails(EquipmentModel equipment) { // TODO: Naviguer vers la page de détails ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Détails de ${equipment.name} - À implémenter')), ); } }