From 6abb8f1d143d8964dcbac65f8d8a0eeb3712eacc Mon Sep 17 00:00:00 2001 From: ElPoyo Date: Thu, 30 Oct 2025 18:45:50 +0100 Subject: [PATCH] feat: Introduce PDFService for optimized PDF generation and caching in container and equipment management --- .../container_pdf_generator_service.dart | 52 --- em2rp/lib/services/pdf_generator_service.dart | 76 ---- em2rp/lib/services/pdf_service.dart | 336 +++++++++++++++++ em2rp/lib/services/qr_code_service.dart | 37 +- .../unified_pdf_generator_service.dart | 354 ------------------ .../lib/views/container_management_page.dart | 40 +- .../lib/views/equipment_management_page.dart | 24 +- .../qr_code_format_selector_dialog.dart | 23 +- 8 files changed, 429 insertions(+), 513 deletions(-) delete mode 100644 em2rp/lib/services/container_pdf_generator_service.dart delete mode 100644 em2rp/lib/services/pdf_generator_service.dart create mode 100644 em2rp/lib/services/pdf_service.dart delete mode 100644 em2rp/lib/services/unified_pdf_generator_service.dart diff --git a/em2rp/lib/services/container_pdf_generator_service.dart b/em2rp/lib/services/container_pdf_generator_service.dart deleted file mode 100644 index 22c9151..0000000 --- a/em2rp/lib/services/container_pdf_generator_service.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'dart:typed_data'; -import 'package:em2rp/models/container_model.dart'; -import 'package:em2rp/models/equipment_model.dart'; -import 'package:em2rp/services/unified_pdf_generator_service.dart'; - -export 'package:em2rp/services/unified_pdf_generator_service.dart' show QRLabelFormat; - -/// Formats d'étiquettes disponibles pour containers (legacy - utilise QRLabelFormat maintenant) -@Deprecated('Utiliser QRLabelFormat directement') -typedef ContainerQRLabelFormat = QRLabelFormat; - -/// Service pour la génération de PDFs avec QR codes pour containers -/// WRAPPER LEGACY - Utilise maintenant UnifiedPDFGeneratorService -@Deprecated('Utiliser UnifiedPDFGeneratorService directement') -class ContainerPDFGeneratorService { - /// Génère un PDF avec des QR codes selon le format choisi - static Future generateQRCodesPDF({ - required List containerList, - required Map> containerEquipmentMap, - required QRLabelFormat format, - }) async { - // Pour les grandes étiquettes, inclure les équipements - if (format == QRLabelFormat.large) { - return UnifiedPDFGeneratorService.generateAdvancedQRCodesPDF( - items: containerList, - getId: (c) => c.id, - getTitle: (c) => c.name, - getSubtitle: (c) { - final equipment = containerEquipmentMap[c.id] ?? []; - final lines = [ - 'Contenu (${equipment.length}):', - ...equipment.take(5).map((eq) => '- ${eq.id}'), - if (equipment.length > 5) '... +${equipment.length - 5}', - ]; - return lines; - }, - format: format, - ); - } - - // Pour les petites et moyennes étiquettes, juste ID + nom - return UnifiedPDFGeneratorService.generateAdvancedQRCodesPDF( - items: containerList, - getId: (c) => c.id, - getTitle: (c) => c.name, - getSubtitle: null, - format: format, - ); - } -} - - diff --git a/em2rp/lib/services/pdf_generator_service.dart b/em2rp/lib/services/pdf_generator_service.dart deleted file mode 100644 index 8e0e44a..0000000 --- a/em2rp/lib/services/pdf_generator_service.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'dart:typed_data'; -import 'package:em2rp/models/equipment_model.dart'; -import 'package:em2rp/services/unified_pdf_generator_service.dart'; - -// Export QRLabelFormat pour rétrocompatibilité -export 'package:em2rp/services/unified_pdf_generator_service.dart' show QRLabelFormat; - -/// Service pour la génération de PDFs avec QR codes pour équipements -/// WRAPPER LEGACY - Utilise maintenant UnifiedPDFGeneratorService -@Deprecated('Utiliser UnifiedPDFGeneratorService directement') -class PDFGeneratorService { - /// Génère un PDF avec des QR codes selon le format choisi - static Future generateQRCodesPDF({ - required List equipmentList, - required QRLabelFormat format, - }) async { - // Pour les grandes étiquettes, ajouter les détails - if (format == QRLabelFormat.large) { - return UnifiedPDFGeneratorService.generateAdvancedQRCodesPDF( - items: equipmentList, - getId: (eq) => eq.id, - getTitle: (eq) => '${eq.brand ?? ''} ${eq.model ?? ''}'.trim(), - getSubtitle: (eq) { - final details = []; - - // Marque - if (eq.brand != null && eq.brand!.isNotEmpty) { - details.add('Marque: ${eq.brand}'); - } - - // Modèle - if (eq.model != null && eq.model!.isNotEmpty) { - details.add('Modèle: ${eq.model}'); - } - - // Catégorie - details.add('Catégorie: ${_getCategoryLabel(eq.category)}'); - - return details; - }, - format: format, - ); - } - - // Pour petites et moyennes étiquettes, juste ID + marque/modèle - return UnifiedPDFGeneratorService.generateAdvancedQRCodesPDF( - items: equipmentList, - getId: (eq) => eq.id, - getTitle: (eq) => '${eq.brand ?? ''} ${eq.model ?? ''}'.trim(), - getSubtitle: null, - format: format, - ); - } - - static String _getCategoryLabel(EquipmentCategory category) { - switch (category) { - case EquipmentCategory.lighting: - return 'Lumière'; - case EquipmentCategory.sound: - return 'Son'; - case EquipmentCategory.video: - return 'Vidéo'; - case EquipmentCategory.effect: - return 'Effets'; - case EquipmentCategory.structure: - return 'Structure'; - case EquipmentCategory.cable: - return 'Câble'; - case EquipmentCategory.consumable: - return 'Consommable'; - case EquipmentCategory.other: - return 'Autre'; - } - } -} - diff --git a/em2rp/lib/services/pdf_service.dart b/em2rp/lib/services/pdf_service.dart new file mode 100644 index 0000000..465135b --- /dev/null +++ b/em2rp/lib/services/pdf_service.dart @@ -0,0 +1,336 @@ +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 } + +/// Configuration pour la génération de PDF +class PDFGeneratorConfig { + final double qrCodeSize; + final int itemsPerPage; + final bool withProgress; + + const PDFGeneratorConfig({ + required this.qrCodeSize, + required this.itemsPerPage, + this.withProgress = false, + }); + + static const small = PDFGeneratorConfig( + qrCodeSize: 150, + itemsPerPage: 20, + ); + + static const medium = PDFGeneratorConfig( + qrCodeSize: 250, + itemsPerPage: 6, + ); + + static const large = PDFGeneratorConfig( + qrCodeSize: 300, + itemsPerPage: 10, + ); + + static PDFGeneratorConfig fromFormat(QRLabelFormat format) { + switch (format) { + case QRLabelFormat.small: + return small; + case QRLabelFormat.medium: + return medium; + case QRLabelFormat.large: + return large; + } + } +} + +/// Service UNIQUE et optimisé pour la génération de PDFs avec QR codes +/// Remplace PDFGeneratorService, ContainerPDFGeneratorService et UnifiedPDFGeneratorService +class PDFService { + static Uint8List? _cachedLogoBytes; + static bool _logoLoadAttempted = false; + + /// Charge le logo en cache une seule fois + static Future _ensureLogoLoaded() async { + if (_logoLoadAttempted) return; + _logoLoadAttempted = true; + + try { + final logoData = await rootBundle.load('assets/logos/LowQRectangleLogoBlack.png'); + _cachedLogoBytes = logoData.buffer.asUint8List(); + } catch (e) { + _cachedLogoBytes = Uint8List(0); + } + } + + /// Tronque un texte + static String _truncate(String text, int max) { + return text.length <= max ? text : '${text.substring(0, max - 3)}...'; + } + + /// Génère un PDF avec QR codes pour n'importe quel type d'objet + /// + /// [items] : Liste des objets à générer + /// [format] : Format des étiquettes (small, medium, large) + /// [getId] : Fonction pour obtenir l'ID unique + /// [getTitle] : Fonction pour obtenir le titre (optionnel) + /// [getDetails] : Fonction pour obtenir les détails (optionnel, seulement pour large) + /// [onProgress] : Callback de progression (optionnel) + static Future generatePDF({ + required List items, + required QRLabelFormat format, + required String Function(T) getId, + String Function(T)? getTitle, + List Function(T)? getDetails, + Function(int current, int total)? onProgress, + }) async { + if (items.isEmpty) { + throw Exception('La liste d\'items est vide'); + } + + final config = PDFGeneratorConfig.fromFormat(format); + final pdf = pw.Document(); + + // Pré-charger le logo pour format large + if (format == QRLabelFormat.large) { + await _ensureLogoLoaded(); + } + + // Générer tous les QR codes en bulk avec progression + final allQRImages = await QRCodeService.generateBulkQRCodes( + items.map((item) => getId(item)).toList(), + size: config.qrCodeSize, + withLogo: false, + useCache: true, + onProgress: onProgress, + ); + + // Générer les pages + switch (format) { + case QRLabelFormat.small: + _addSmallLabels(pdf, items, getId, allQRImages, config); + break; + case QRLabelFormat.medium: + _addMediumLabels(pdf, items, getId, getTitle, allQRImages, config); + break; + case QRLabelFormat.large: + _addLargeLabels(pdf, items, getId, getTitle, getDetails, allQRImages, config); + break; + } + + return pdf.save(); + } + + // ======================================================================== + // PETITS LABELS (2x2 cm, 20 par page) + // ======================================================================== + static void _addSmallLabels( + pw.Document pdf, + List items, + String Function(T) getId, + List qrImages, + PDFGeneratorConfig config, + ) { + const qrSize = 56.69; // 2cm + + for (int pageStart = 0; pageStart < items.length; pageStart += config.itemsPerPage) { + final pageItems = items.skip(pageStart).take(config.itemsPerPage).toList(); + final pageQRs = qrImages.skip(pageStart).take(config.itemsPerPage).toList(); + + pdf.addPage( + pw.Page( + pageFormat: PdfPageFormat.a4, + margin: const pw.EdgeInsets.all(20), + build: (_) => pw.Wrap( + spacing: 10, + runSpacing: 10, + children: List.generate(pageItems.length, (i) { + return pw.Container( + width: qrSize, + height: qrSize + 20, + child: pw.Column( + mainAxisAlignment: pw.MainAxisAlignment.center, + children: [ + pw.Image(pw.MemoryImage(pageQRs[i])), + pw.SizedBox(height: 2), + pw.Text( + getId(pageItems[i]), + style: pw.TextStyle(fontSize: 6, fontWeight: pw.FontWeight.bold), + textAlign: pw.TextAlign.center, + ), + ], + ), + ); + }), + ), + ), + ); + } + } + + // ======================================================================== + // LABELS MOYENS (4x4 cm, 6 par page) + // ======================================================================== + static void _addMediumLabels( + pw.Document pdf, + List items, + String Function(T) getId, + String Function(T)? getTitle, + List qrImages, + PDFGeneratorConfig config, + ) { + const qrSize = 113.39; // 4cm + + for (int pageStart = 0; pageStart < items.length; pageStart += config.itemsPerPage) { + final pageItems = items.skip(pageStart).take(config.itemsPerPage).toList(); + final pageQRs = qrImages.skip(pageStart).take(config.itemsPerPage).toList(); + + pdf.addPage( + pw.Page( + pageFormat: PdfPageFormat.a4, + margin: const pw.EdgeInsets.all(20), + build: (_) => pw.Wrap( + spacing: 20, + runSpacing: 20, + children: List.generate(pageItems.length, (i) { + return pw.Container( + width: qrSize, + height: qrSize + 30, + child: pw.Column( + mainAxisAlignment: pw.MainAxisAlignment.center, + children: [ + pw.Image(pw.MemoryImage(pageQRs[i])), + pw.SizedBox(height: 4), + pw.Text( + getId(pageItems[i]), + style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold), + textAlign: pw.TextAlign.center, + ), + if (getTitle != null) ...[ + pw.SizedBox(height: 2), + pw.Text( + _truncate(getTitle(pageItems[i]), 25), + style: const pw.TextStyle(fontSize: 8, color: PdfColors.grey700), + textAlign: pw.TextAlign.center, + ), + ], + ], + ), + ); + }), + ), + ), + ); + } + } + + // ======================================================================== + // GRANDS LABELS (avec détails, 10 par page) + // ======================================================================== + static void _addLargeLabels( + pw.Document pdf, + List items, + String Function(T) getId, + String Function(T)? getTitle, + List Function(T)? getDetails, + List qrImages, + PDFGeneratorConfig config, + ) { + const qrSize = 100.0; + + for (int pageStart = 0; pageStart < items.length; pageStart += config.itemsPerPage) { + final pageItems = items.skip(pageStart).take(config.itemsPerPage).toList(); + final pageQRs = qrImages.skip(pageStart).take(config.itemsPerPage).toList(); + + pdf.addPage( + pw.Page( + pageFormat: PdfPageFormat.a4, + margin: const pw.EdgeInsets.all(20), + build: (_) => pw.Wrap( + spacing: 10, + runSpacing: 10, + children: List.generate(pageItems.length, (i) { + final item = pageItems[i]; + 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(pageQRs[i])), + ), + pw.SizedBox(width: 8), + // Détails + pw.Expanded( + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // Logo + if (_cachedLogoBytes != null && _cachedLogoBytes!.isNotEmpty) + pw.Center( + child: pw.Container( + height: 25, + margin: const pw.EdgeInsets.only(bottom: 6), + child: pw.Image(pw.MemoryImage(_cachedLogoBytes!)), + ), + ), + // Titre + if (getTitle != null) ...[ + pw.SizedBox(height: 2), + pw.Text( + _truncate(getTitle(item), 20), + style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold), + maxLines: 2, + ), + ], + // ID + pw.SizedBox(height: 2), + pw.Text( + getId(item), + style: const pw.TextStyle(fontSize: 8, color: PdfColors.grey700), + maxLines: 1, + ), + // Détails supplémentaires + if (getDetails != null) ...[ + pw.SizedBox(height: 4), + ...getDetails(item).take(5).map((line) { + return pw.Padding( + padding: const pw.EdgeInsets.only(bottom: 1), + child: pw.Text( + _truncate(line, 25), + style: const pw.TextStyle(fontSize: 6, color: PdfColors.grey800), + ), + ); + }), + ], + ], + ), + ), + ], + ), + ); + }), + ), + ), + ); + } + } + + /// Nettoie le cache (logo) + static void clearCache() { + _cachedLogoBytes = null; + _logoLoadAttempted = false; + } +} + diff --git a/em2rp/lib/services/qr_code_service.dart b/em2rp/lib/services/qr_code_service.dart index 4dddde9..9519a2d 100644 --- a/em2rp/lib/services/qr_code_service.dart +++ b/em2rp/lib/services/qr_code_service.dart @@ -111,13 +111,16 @@ class QRCodeService { return _cachedLogoImage!; } - /// Génère plusieurs QR codes en parallèle (optimisé) + /// Génère plusieurs QR codes en parallèle par batches optimisés (réduit les lags) static Future> generateBulkQRCodes( List dataList, { double size = 512, bool withLogo = false, bool useCache = true, + Function(int, int)? onProgress, // Callback de progression }) async { + if (dataList.isEmpty) return []; + // Si tout est en cache, retourner immédiatement if (useCache) { final allCached = dataList.every((data) { @@ -133,28 +136,46 @@ class QRCodeService { } } - // Batching adaptatif optimisé selon la taille et le nombre + // Batching adaptatif ultra-optimisé int batchSize; - if (size <= 200) { - batchSize = 100; // Petits QR : lots de 100 + if (dataList.length <= 10) { + // Pour petites quantités, tout en une fois + batchSize = dataList.length; + } else if (size <= 200) { + batchSize = 20; // Petits QR : lots de 20 (réduit de 100) } else if (size <= 300) { - batchSize = 50; // Moyens QR : lots de 50 - } else if (size <= 500) { - batchSize = 20; // Grands QR : lots de 20 + batchSize = 10; // Moyens QR : lots de 10 (réduit de 50) + } else if (size <= 400) { + batchSize = 5; // Grands QR : lots de 5 (réduit de 20) } else { - batchSize = 10; // Très grands : lots de 10 + batchSize = 3; // Très grands : lots de 3 (réduit de 10) } final List results = []; + int processed = 0; for (int i = 0; i < dataList.length; i += batchSize) { final batch = dataList.skip(i).take(batchSize).toList(); + + // Générer le batch final batchResults = await Future.wait( batch.map((data) => withLogo ? generateQRCodeWithLogo(data, size: size, useCache: useCache) : generateQRCode(data, size: size, useCache: useCache)), ); + results.addAll(batchResults); + processed += batch.length; + + // Callback de progression + if (onProgress != null) { + onProgress(processed, dataList.length); + } + + // Pause micro pour éviter de bloquer l'UI (seulement pour gros batches) + if (batchSize >= 5 && i + batchSize < dataList.length) { + await Future.delayed(const Duration(milliseconds: 10)); + } } return results; diff --git a/em2rp/lib/services/unified_pdf_generator_service.dart b/em2rp/lib/services/unified_pdf_generator_service.dart deleted file mode 100644 index e8136b5..0000000 --- a/em2rp/lib/services/unified_pdf_generator_service.dart +++ /dev/null @@ -1,354 +0,0 @@ -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 _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 generateSimpleQRCodesPDF({ - required List 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 generateAdvancedQRCodesPDF({ - required List items, - required String Function(T) getId, - required String Function(T) getTitle, - required List 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 _generateSmallQRCodesPDF( - pw.Document pdf, - List 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 _generateMediumQRCodesPDF( - pw.Document pdf, - List 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 _generateLargeQRCodesPDF( - pw.Document pdf, - List items, - String Function(T) getId, - String Function(T)? getTitle, - List 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, - ), - ), - ); - }), - ], - ], - ), - ), - ], - ), - ); - }), - ); - }, - ), - ); - } - } -} - diff --git a/em2rp/lib/views/container_management_page.dart b/em2rp/lib/views/container_management_page.dart index 1eda72f..2d831b8 100644 --- a/em2rp/lib/views/container_management_page.dart +++ b/em2rp/lib/views/container_management_page.dart @@ -7,7 +7,7 @@ import 'package:em2rp/views/widgets/nav/custom_app_bar.dart'; import 'package:em2rp/providers/container_provider.dart'; import 'package:em2rp/models/container_model.dart'; import 'package:em2rp/models/equipment_model.dart'; -import 'package:em2rp/services/container_pdf_generator_service.dart'; +import 'package:em2rp/services/pdf_service.dart'; import 'package:em2rp/views/widgets/common/qr_code_dialog.dart'; import 'package:em2rp/mixins/selection_mode_mixin.dart'; import 'package:printing/printing.dart'; @@ -26,6 +26,7 @@ class _ContainerManagementPageState extends State final TextEditingController _searchController = TextEditingController(); ContainerType? _selectedType; EquipmentStatus? _selectedStatus; + List? _cachedContainers; // Cache pour éviter le rebuild @override void dispose() { @@ -321,7 +322,13 @@ class _ContainerManagementPageState extends State return StreamBuilder>( stream: provider.containersStream, builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { + // Utiliser les données en cache si disponibles pendant le rebuild + if (snapshot.hasData) { + _cachedContainers = snapshot.data; + } + + // Afficher le loader seulement au premier chargement + if (snapshot.connectionState == ConnectionState.waiting && _cachedContainers == null) { return const Center(child: CircularProgressIndicator()); } @@ -331,7 +338,7 @@ class _ContainerManagementPageState extends State ); } - final containers = snapshot.data ?? []; + final containers = _cachedContainers ?? snapshot.data ?? []; if (containers.isEmpty) { return Center( @@ -635,7 +642,7 @@ class _ContainerManagementPageState extends State } // Afficher le dialogue de sélection de format - final format = await showDialog( + final format = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Format des étiquettes'), @@ -646,19 +653,19 @@ class _ContainerManagementPageState extends State leading: const Icon(Icons.qr_code_2), title: const Text('Petits QR codes'), subtitle: const Text('2×2 cm - QR code + ID (20 par page)'), - onTap: () => Navigator.pop(context, ContainerQRLabelFormat.small), + onTap: () => Navigator.pop(context, QRLabelFormat.small), ), ListTile( leading: const Icon(Icons.qr_code), title: const Text('QR codes moyens'), subtitle: const Text('4×4 cm - QR code + ID (6 par page)'), - onTap: () => Navigator.pop(context, ContainerQRLabelFormat.medium), + onTap: () => Navigator.pop(context, QRLabelFormat.medium), ), ListTile( leading: const Icon(Icons.label), title: const Text('Grandes étiquettes'), - subtitle: const Text('QR code + ID + Type + Contenu (6 par page)'), - onTap: () => Navigator.pop(context, ContainerQRLabelFormat.large), + subtitle: const Text('QR code + ID + Type + Contenu (10 par page)'), + onTap: () => Navigator.pop(context, QRLabelFormat.large), ), ], ), @@ -673,12 +680,21 @@ class _ContainerManagementPageState extends State if (format == null || !mounted) return; - // Générer et afficher le PDF + // Générer et afficher le PDF avec la nouvelle API optimisée try { - final pdfBytes = await ContainerPDFGeneratorService.generateQRCodesPDF( - containerList: selectedContainers, - containerEquipmentMap: containerEquipmentMap, + final pdfBytes = await PDFService.generatePDF( + items: selectedContainers, format: format, + getId: (c) => c.id, + getTitle: (c) => c.name, + getDetails: format == QRLabelFormat.large ? (ContainerModel c) { + final equipment = containerEquipmentMap[c.id] ?? []; + return [ + 'Contenu (${equipment.length}):', + ...equipment.take(5).map((eq) => '- ${eq.id}'), + if (equipment.length > 5) '... +${equipment.length - 5}', + ]; + } : null, ); if (mounted) { diff --git a/em2rp/lib/views/equipment_management_page.dart b/em2rp/lib/views/equipment_management_page.dart index 0888af5..91b2c6c 100644 --- a/em2rp/lib/views/equipment_management_page.dart +++ b/em2rp/lib/views/equipment_management_page.dart @@ -25,6 +25,7 @@ class _EquipmentManagementPageState extends State with SelectionModeMixin { final TextEditingController _searchController = TextEditingController(); EquipmentCategory? _selectedCategory; + List? _cachedEquipment; @override void dispose() { @@ -420,7 +421,13 @@ class _EquipmentManagementPageState extends State return StreamBuilder>( stream: provider.equipmentStream, builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { + // Mettre en cache les données quand elles arrivent + if (snapshot.hasData) { + _cachedEquipment = snapshot.data; + } + + // Afficher le loader seulement si on n'a pas encore de cache + if (snapshot.connectionState == ConnectionState.waiting && _cachedEquipment == null) { return const Center(child: CircularProgressIndicator()); } @@ -430,7 +437,10 @@ class _EquipmentManagementPageState extends State ); } - if (!snapshot.hasData || snapshot.data!.isEmpty) { + // Utiliser le cache si disponible, sinon les nouvelles données + final equipment = _cachedEquipment ?? snapshot.data ?? []; + + if (equipment.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -452,15 +462,15 @@ class _EquipmentManagementPageState extends State ); } - // Tri par nom - final equipment = snapshot.data!; - equipment.sort((a, b) => a.name.compareTo(b.name)); + // Créer une copie pour le tri + final sortedEquipment = List.from(equipment); + sortedEquipment.sort((a, b) => a.name.compareTo(b.name)); return ListView.builder( padding: const EdgeInsets.all(16), - itemCount: equipment.length, + itemCount: sortedEquipment.length, itemBuilder: (context, index) { - return _buildEquipmentCard(equipment[index]); + return _buildEquipmentCard(sortedEquipment[index]); }, ); }, diff --git a/em2rp/lib/views/widgets/common/qr_code_format_selector_dialog.dart b/em2rp/lib/views/widgets/common/qr_code_format_selector_dialog.dart index 7044f34..31c6077 100644 --- a/em2rp/lib/views/widgets/common/qr_code_format_selector_dialog.dart +++ b/em2rp/lib/views/widgets/common/qr_code_format_selector_dialog.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/models/equipment_model.dart'; -import 'package:em2rp/services/pdf_generator_service.dart'; +import 'package:em2rp/services/pdf_service.dart'; import 'package:printing/printing.dart'; /// Widget réutilisable pour sélectionner le format de génération de QR codes multiples @@ -157,10 +157,25 @@ class QRCodeFormatSelectorDialog extends StatelessWidget { ); try { - // Génération du PDF - final pdfBytes = await PDFGeneratorService.generateQRCodesPDF( - equipmentList: equipmentList, + // Génération du PDF avec progression + final pdfBytes = await PDFService.generatePDF( + items: equipmentList, format: format, + getId: (eq) => eq.id, + getTitle: (eq) => '${eq.brand ?? ''} ${eq.model ?? ''}'.trim(), + getDetails: format == QRLabelFormat.large ? (EquipmentModel eq) { + final details = []; + final brand = eq.brand; + if (brand != null && brand.isNotEmpty) { + details.add('Marque: $brand'); + } + final model = eq.model; + if (model != null && model.isNotEmpty) { + details.add('Modèle: $model'); + } + details.add('Catégorie: ${eq.category.label}'); + return details; + } : null, ); // Fermer le dialogue de chargement