 3fab69cb00
			
		
	
	3fab69cb00
	
	
	
		
			
			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.).
		
	
		
			
				
	
	
		
			355 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			355 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| 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,
 | |
|                                     ),
 | |
|                                   ),
 | |
|                                 );
 | |
|                               }),
 | |
|                             ],
 | |
|                           ],
 | |
|                         ),
 | |
|                       ),
 | |
|                     ],
 | |
|                   ),
 | |
|                 );
 | |
|               }),
 | |
|             );
 | |
|           },
 | |
|         ),
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 |