feat: Gestion complète des containers et refactorisation du matériel
Ajout de la gestion des containers (création, édition, suppression, affichage des détails).
Introduction d'un système de génération de QR codes unifié et d'un mode de sélection multiple.
**Features:**
- **Gestion des Containers :**
    - Nouvelle page de gestion des containers (`container_management_page.dart`) avec recherche et filtres.
    - Formulaire de création/édition de containers (`container_form_page.dart`) avec génération d'ID automatique.
    - Page de détails d'un container (`container_detail_page.dart`) affichant son contenu et ses caractéristiques.
    - Ajout des routes et du provider (`ContainerProvider`) nécessaires.
- **Modèle de Données :**
    - Ajout du `ContainerModel` pour représenter les boîtes, flight cases, etc.
    - Le modèle `EquipmentModel` a été enrichi avec des caractéristiques physiques (poids, dimensions).
- **QR Codes :**
    - Nouveau service unifié (`UnifiedPDFGeneratorService`) pour générer des PDFs de QR codes pour n'importe quelle entité.
    - Services `PDFGeneratorService` et `ContainerPDFGeneratorService` transformés en wrappers pour maintenir la compatibilité.
    - Amélioration de la performance de la génération de QR codes en masse.
- **Interface Utilisateur (UI/UX) :**
    - Nouvelle page de détails pour le matériel (`equipment_detail_page.dart`).
    - Ajout d'un `SelectionModeMixin` pour gérer la sélection multiple dans les pages de gestion.
    - Dialogues réutilisables pour l'affichage de QR codes (`QRCodeDialog`) et la sélection de format d'impression (`QRCodeFormatSelectorDialog`).
    - Ajout d'un bouton "Gérer les boîtes" sur la page de gestion du matériel.
**Refactorisation :**
- L' `IdGenerator` a été déplacé dans le répertoire `utils` et étendu pour gérer les containers.
- Mise à jour de nombreuses dépendances `pubspec.yaml` vers des versions plus récentes.
- Séparation de la logique d'affichage des containers et du matériel dans des widgets dédiés (`ContainerHeaderCard`, `EquipmentParentContainers`, etc.).
			
			
This commit is contained in:
		
							
								
								
									
										354
									
								
								em2rp/lib/services/unified_pdf_generator_service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										354
									
								
								em2rp/lib/services/unified_pdf_generator_service.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,354 @@ | ||||
| import 'dart:typed_data'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:pdf/pdf.dart'; | ||||
| import 'package:pdf/widgets.dart' as pw; | ||||
| import 'package:em2rp/services/qr_code_service.dart'; | ||||
|  | ||||
| /// Formats d'étiquettes QR disponibles | ||||
| enum QRLabelFormat { small, medium, large } | ||||
|  | ||||
| /// Interface pour les items qui peuvent avoir un QR code | ||||
| abstract class QRCodeItem { | ||||
|   String get id; | ||||
|   String get displayName; | ||||
| } | ||||
|  | ||||
| /// Service unifié pour la génération de PDFs avec QR codes | ||||
| /// Fonctionne avec n'importe quel type d'objet ayant un ID | ||||
| class UnifiedPDFGeneratorService { | ||||
|   static Uint8List? _cachedLogoBytes; | ||||
|  | ||||
|   /// Tronque un texte s'il dépasse une longueur maximale | ||||
|   static String _truncateText(String text, int maxLength) { | ||||
|     if (text.length <= maxLength) { | ||||
|       return text; | ||||
|     } | ||||
|     return '${text.substring(0, maxLength - 3)}...'; | ||||
|   } | ||||
|  | ||||
|   /// Charge le logo en cache (optimisation) | ||||
|   static Future<void> _ensureLogoLoaded() async { | ||||
|     if (_cachedLogoBytes == null) { | ||||
|       try { | ||||
|         final logoData = await rootBundle.load('assets/logos/LowQRectangleLogoBlack.png'); | ||||
|         _cachedLogoBytes = logoData.buffer.asUint8List(); | ||||
|       } catch (e) { | ||||
|         // Logo non disponible, on continue sans | ||||
|         _cachedLogoBytes = Uint8List(0); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Génère un PDF avec des QR codes simples (juste ID + QR) | ||||
|   static Future<Uint8List> generateSimpleQRCodesPDF<T>({ | ||||
|     required List<T> items, | ||||
|     required String Function(T) getId, | ||||
|     required QRLabelFormat format, | ||||
|     String Function(T)? getDisplayName, | ||||
|   }) async { | ||||
|     final pdf = pw.Document(); | ||||
|  | ||||
|     switch (format) { | ||||
|       case QRLabelFormat.small: | ||||
|         await _generateSmallQRCodesPDF(pdf, items, getId, getDisplayName); | ||||
|         break; | ||||
|       case QRLabelFormat.medium: | ||||
|         await _generateMediumQRCodesPDF(pdf, items, getId, getDisplayName); | ||||
|         break; | ||||
|       case QRLabelFormat.large: | ||||
|         await _generateLargeQRCodesPDF(pdf, items, getId, getDisplayName, null); | ||||
|         break; | ||||
|     } | ||||
|  | ||||
|     return pdf.save(); | ||||
|   } | ||||
|  | ||||
|   /// Génère un PDF avec des QR codes avancés (avec informations supplémentaires) | ||||
|   static Future<Uint8List> generateAdvancedQRCodesPDF<T>({ | ||||
|     required List<T> items, | ||||
|     required String Function(T) getId, | ||||
|     required String Function(T) getTitle, | ||||
|     required List<String> Function(T)? getSubtitle, | ||||
|     required QRLabelFormat format, | ||||
|   }) async { | ||||
|     final pdf = pw.Document(); | ||||
|  | ||||
|     switch (format) { | ||||
|       case QRLabelFormat.small: | ||||
|         await _generateSmallQRCodesPDF(pdf, items, getId, getTitle); | ||||
|         break; | ||||
|       case QRLabelFormat.medium: | ||||
|         await _generateMediumQRCodesPDF(pdf, items, getId, getTitle); | ||||
|         break; | ||||
|       case QRLabelFormat.large: | ||||
|         await _generateLargeQRCodesPDF(pdf, items, getId, getTitle, getSubtitle); | ||||
|         break; | ||||
|     } | ||||
|  | ||||
|     return pdf.save(); | ||||
|   } | ||||
|  | ||||
|   // ========================================================================== | ||||
|   // PETITS QR CODES (2x2 cm, 20 par page) | ||||
|   // ========================================================================== | ||||
|  | ||||
|   static Future<void> _generateSmallQRCodesPDF<T>( | ||||
|     pw.Document pdf, | ||||
|     List<T> items, | ||||
|     String Function(T) getId, | ||||
|     String Function(T)? getDisplayName, | ||||
|   ) async { | ||||
|     const qrSize = 56.69; // 2cm en points | ||||
|     const itemsPerPage = 20; | ||||
|  | ||||
|     // Générer tous les QR codes en une fois (optimisé avec résolution réduite) | ||||
|     final allQRImages = await QRCodeService.generateBulkQRCodes( | ||||
|       items.map((item) => getId(item)).toList(), | ||||
|       size: 150, // Réduit de 200 à 150 pour performance optimale | ||||
|       withLogo: false, | ||||
|       useCache: true, | ||||
|     ); | ||||
|  | ||||
|     for (int pageStart = 0; pageStart < items.length; pageStart += itemsPerPage) { | ||||
|       final pageItems = items.skip(pageStart).take(itemsPerPage).toList(); | ||||
|       final pageQRImages = allQRImages.skip(pageStart).take(itemsPerPage).toList(); | ||||
|  | ||||
|       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(pageItems.length, (index) { | ||||
|                 return pw.Container( | ||||
|                   width: qrSize, | ||||
|                   height: qrSize + 20, | ||||
|                   child: pw.Column( | ||||
|                     mainAxisAlignment: pw.MainAxisAlignment.center, | ||||
|                     crossAxisAlignment: pw.CrossAxisAlignment.center, | ||||
|                     children: [ | ||||
|                       pw.Image(pw.MemoryImage(pageQRImages[index])), | ||||
|                       pw.SizedBox(height: 2), | ||||
|                       pw.Text( | ||||
|                         getId(pageItems[index]), | ||||
|                         style: pw.TextStyle( | ||||
|                           fontSize: 6, | ||||
|                           fontWeight: pw.FontWeight.bold, | ||||
|                         ), | ||||
|                         textAlign: pw.TextAlign.center, | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ); | ||||
|               }), | ||||
|             ); | ||||
|           }, | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // ========================================================================== | ||||
|   // QR CODES MOYENS (4x4 cm, 6 par page) | ||||
|   // ========================================================================== | ||||
|  | ||||
|   static Future<void> _generateMediumQRCodesPDF<T>( | ||||
|     pw.Document pdf, | ||||
|     List<T> items, | ||||
|     String Function(T) getId, | ||||
|     String Function(T)? getDisplayName, | ||||
|   ) async { | ||||
|     const qrSize = 113.39; // 4cm en points | ||||
|     const itemsPerPage = 6; | ||||
|  | ||||
|     // Optimisé avec résolution réduite | ||||
|     final allQRImages = await QRCodeService.generateBulkQRCodes( | ||||
|       items.map((item) => getId(item)).toList(), | ||||
|       size: 250, // Réduit de 400 à 250 pour performance optimale | ||||
|       withLogo: false, | ||||
|       useCache: true, | ||||
|     ); | ||||
|  | ||||
|     for (int pageStart = 0; pageStart < items.length; pageStart += itemsPerPage) { | ||||
|       final pageItems = items.skip(pageStart).take(itemsPerPage).toList(); | ||||
|       final pageQRImages = allQRImages.skip(pageStart).take(itemsPerPage).toList(); | ||||
|  | ||||
|       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(pageItems.length, (index) { | ||||
|                 return pw.Container( | ||||
|                   width: qrSize, | ||||
|                   height: qrSize + 30, | ||||
|                   child: pw.Column( | ||||
|                     crossAxisAlignment: pw.CrossAxisAlignment.center, | ||||
|                     mainAxisAlignment: pw.MainAxisAlignment.center, | ||||
|                     children: [ | ||||
|                       pw.Image(pw.MemoryImage(pageQRImages[index])), | ||||
|                       pw.SizedBox(height: 4), | ||||
|                       pw.Text( | ||||
|                         getId(pageItems[index]), | ||||
|                         style: pw.TextStyle( | ||||
|                           fontSize: 10, | ||||
|                           fontWeight: pw.FontWeight.bold, | ||||
|                         ), | ||||
|                         textAlign: pw.TextAlign.center, | ||||
|                       ), | ||||
|                       if (getDisplayName != null) ...[ | ||||
|                         pw.SizedBox(height: 2), | ||||
|                         pw.Text( | ||||
|                           _truncateText(getDisplayName(pageItems[index]), 25), | ||||
|                           style: const pw.TextStyle( | ||||
|                             fontSize: 8, | ||||
|                             color: PdfColors.grey700, | ||||
|                           ), | ||||
|                           textAlign: pw.TextAlign.center, | ||||
|                         ), | ||||
|                       ], | ||||
|                     ], | ||||
|                   ), | ||||
|                 ); | ||||
|               }), | ||||
|             ); | ||||
|           }, | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // ========================================================================== | ||||
|   // GRANDES ÉTIQUETTES (QR + infos détaillées, 6 par page) | ||||
|   // ========================================================================== | ||||
|  | ||||
|   static Future<void> _generateLargeQRCodesPDF<T>( | ||||
|     pw.Document pdf, | ||||
|     List<T> items, | ||||
|     String Function(T) getId, | ||||
|     String Function(T)? getTitle, | ||||
|     List<String> Function(T)? getSubtitle, | ||||
|   ) async { | ||||
|     const qrSize = 100.0; | ||||
|     const itemsPerPage = 6; | ||||
|  | ||||
|     // Charger le logo une seule fois | ||||
|     await _ensureLogoLoaded(); | ||||
|  | ||||
|     // Générer les QR codes en bulk pour optimisation | ||||
|     final allQRImages = await QRCodeService.generateBulkQRCodes( | ||||
|       items.map((item) => getId(item)).toList(), | ||||
|       size: 300, // Réduit de 400 à 300 pour améliorer la performance | ||||
|       withLogo: false, | ||||
|       useCache: true, | ||||
|     ); | ||||
|  | ||||
|     for (int pageStart = 0; pageStart < items.length; pageStart += itemsPerPage) { | ||||
|       final pageItems = items.skip(pageStart).take(itemsPerPage).toList(); | ||||
|       final pageQRImages = allQRImages.skip(pageStart).take(itemsPerPage).toList(); | ||||
|  | ||||
|       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(pageItems.length, (index) { | ||||
|                 final item = pageItems[index]; | ||||
|                 return pw.Container( | ||||
|                   width: 260, | ||||
|                   height: 120, | ||||
|                   decoration: pw.BoxDecoration( | ||||
|                     border: pw.Border.all(color: PdfColors.grey400), | ||||
|                     borderRadius: pw.BorderRadius.circular(4), | ||||
|                   ), | ||||
|                   padding: const pw.EdgeInsets.all(8), | ||||
|                   child: pw.Row( | ||||
|                     crossAxisAlignment: pw.CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       // QR Code | ||||
|                       pw.Container( | ||||
|                         width: qrSize, | ||||
|                         height: qrSize, | ||||
|                         child: pw.Image( | ||||
|                           pw.MemoryImage(pageQRImages[index]), | ||||
|                           fit: pw.BoxFit.contain, | ||||
|                         ), | ||||
|                       ), | ||||
|                       pw.SizedBox(width: 8), | ||||
|                       // Informations | ||||
|                       pw.Expanded( | ||||
|                         child: pw.Column( | ||||
|                           crossAxisAlignment: pw.CrossAxisAlignment.start, | ||||
|                           mainAxisAlignment: pw.MainAxisAlignment.start, | ||||
|                           children: [ | ||||
|                             // Logo - CENTRÉ ET PLUS GRAND | ||||
|                             if (_cachedLogoBytes != null && _cachedLogoBytes!.isNotEmpty) | ||||
|                               pw.Center( | ||||
|                                 child: pw.Container( | ||||
|                                   height: 25, // Augmenté de 15 à 25 | ||||
|                                   margin: const pw.EdgeInsets.only(bottom: 6), | ||||
|                                   child: pw.Image( | ||||
|                                     pw.MemoryImage(_cachedLogoBytes!), | ||||
|                                     fit: pw.BoxFit.contain, | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ), | ||||
|                             // ID (toujours affiché sur plusieurs lignes si nécessaire) | ||||
|                             if (getTitle != null) ...[ | ||||
|                             pw.SizedBox(height: 2), | ||||
|                             pw.Text( | ||||
|                             _truncateText(getTitle(item), 20), | ||||
|                               style: pw.TextStyle( | ||||
|                                 fontSize: 10, | ||||
|                                 fontWeight: pw.FontWeight.bold, | ||||
|                               ), | ||||
|                               maxLines: 2, | ||||
|                             ), | ||||
|                             ], | ||||
|                               pw.SizedBox(height: 2), | ||||
|                               pw.Text( | ||||
|                                 getId(item), | ||||
|                                 style: const pw.TextStyle( | ||||
|                                   fontSize: 8, | ||||
|                                   color: PdfColors.grey700, | ||||
|                                 ), | ||||
|                                 maxLines: 1, | ||||
|                               ), | ||||
|                             if (getSubtitle != null) ...[ | ||||
|                               pw.SizedBox(height: 4), | ||||
|                               ...getSubtitle(item).take(5).map((line) { | ||||
|                                 return pw.Padding( | ||||
|                                   padding: const pw.EdgeInsets.only(bottom: 1), | ||||
|                                   child: pw.Text( | ||||
|                                     _truncateText(line, 25), | ||||
|                                     style: const pw.TextStyle( | ||||
|                                       fontSize: 6, | ||||
|                                       color: PdfColors.grey800, | ||||
|                                     ), | ||||
|                                   ), | ||||
|                                 ); | ||||
|                               }), | ||||
|                             ], | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ); | ||||
|               }), | ||||
|             ); | ||||
|           }, | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 ElPoyo
					ElPoyo