diff --git a/em2rp/.gitignore b/em2rp/.gitignore index 1bcb3e4..cd098bd 100644 --- a/em2rp/.gitignore +++ b/em2rp/.gitignore @@ -44,4 +44,4 @@ app.*.map.json # Environment configuration with credentials lib/config/env.dev.dart - +functions/.env diff --git a/em2rp/lib/config/app_version.dart b/em2rp/lib/config/app_version.dart index c9fe19c..db988fe 100644 --- a/em2rp/lib/config/app_version.dart +++ b/em2rp/lib/config/app_version.dart @@ -1,6 +1,6 @@ /// Configuration de la version de l'application class AppVersion { - static const String version = '1.0.3'; + static const String version = '1.0.4'; /// Retourne la version complète de l'application static String get fullVersion => 'v$version'; diff --git a/em2rp/lib/services/pdf_service.dart b/em2rp/lib/services/pdf_service.dart index 4aacea4..0515b12 100644 --- a/em2rp/lib/services/pdf_service.dart +++ b/em2rp/lib/services/pdf_service.dart @@ -24,14 +24,16 @@ class PDFGeneratorConfig { itemsPerPage: 50, ); + // 4 colonnes x 10 lignes = 40 étiquettes static const medium = PDFGeneratorConfig( - qrCodeSize: 250, - itemsPerPage: 20, + qrCodeSize: 150, // Réduit légèrement pour entrer dans 25.4mm de haut + itemsPerPage: 40, ); + // 2 colonnes x 5 lignes = 10 étiquettes static const large = PDFGeneratorConfig( qrCodeSize: 300, - itemsPerPage: 12, + itemsPerPage: 10, ); static PDFGeneratorConfig fromFormat(QRLabelFormat format) { @@ -47,7 +49,6 @@ class PDFGeneratorConfig { } /// 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; @@ -71,13 +72,6 @@ class PDFService { } /// 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, @@ -93,8 +87,8 @@ class PDFService { final config = PDFGeneratorConfig.fromFormat(format); final pdf = pw.Document(); - // Pré-charger le logo pour format large - if (format == QRLabelFormat.large) { + // Pré-charger le logo pour formats medium et large + if (format == QRLabelFormat.medium || format == QRLabelFormat.large) { await _ensureLogoLoaded(); } @@ -124,16 +118,16 @@ class PDFService { } // ======================================================================== - // PETITS LABELS (2x2 cm, 20 par page) + // PETITS LABELS (Original: 2x2 cm approx) // ======================================================================== static void _addSmallLabels( - pw.Document pdf, - List items, - String Function(T) getId, - List qrImages, - PDFGeneratorConfig config, - ) { - const qrSize = 56.69; // 2cm + 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(); @@ -169,19 +163,30 @@ class PDFService { ); } } - - // ======================================================================== - // LABELS MOYENS (4x4 cm, 6 par page) +// ======================================================================== + // LABELS MOYENS (49 x 26 mm | 4 colonnes, 10 lignes) // ======================================================================== 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 + pw.Document pdf, + List items, + String Function(T) getId, + String Function(T)? getTitle, + List qrImages, + PDFGeneratorConfig config, + ) { + // 1. Dimensions exactes des étiquettes + const double labelWidth = 50 * PdfPageFormat.mm; + const double labelHeight = 26.0 * PdfPageFormat.mm; + + // 2. Calcul du centrage manuel + // Marge théorique = (210mm - (49*4)) / 2 = 7mm + // CORRECTION : On enlève 1.5mm pour réduire la marge de gauche (décalage vers la gauche) + const double horizontalCorrection = PdfPageFormat.mm; + + final double leftMargin = ((PdfPageFormat.a4.width - (labelWidth * 4)) / 2) + horizontalCorrection; + + // Centrage vertical standard + final double topMargin = (PdfPageFormat.a4.height - (labelHeight * 10)) / 2 -0.75; for (int pageStart = 0; pageStart < items.length; pageStart += config.itemsPerPage) { final pageItems = items.skip(pageStart).take(config.itemsPerPage).toList(); @@ -190,130 +195,56 @@ class PDFService { 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, - ), - ], - ], - ), - ); - }), + // 3. Application des marges calculées (plus de pw.Center) + margin: pw.EdgeInsets.only( + left: leftMargin, + top: topMargin, + right: 0, + bottom: 0 ), - ), - ); - } - } - - // ======================================================================== - // 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, + spacing: 0, + runSpacing: 0, 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), + width: labelWidth, + height: labelHeight, + padding: const pw.EdgeInsets.all(2), child: pw.Row( - crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - // QR Code + // QR Code à gauche pw.Container( - width: qrSize, - height: qrSize, + width: labelHeight - 4, + height: labelHeight - 4, child: pw.Image(pw.MemoryImage(pageQRs[i])), ), - pw.SizedBox(width: 8), - // Détails + pw.SizedBox(width: 4), + // Texte à droite pw.Expanded( child: pw.Column( + mainAxisAlignment: pw.MainAxisAlignment.center, 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!)), - ), + pw.Container( + height: 12, + alignment: pw.Alignment.centerLeft, + margin: const pw.EdgeInsets.only(bottom: 2), + 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), + getId(pageItems[i]), + style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold), 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), - ), - ); - }), - ], + if (getTitle != null) + pw.Text( + _truncate(getTitle(pageItems[i]), 18), + style: const pw.TextStyle(fontSize: 6, color: PdfColors.grey700), + maxLines: 2, + overflow: pw.TextOverflow.clip, + ), ], ), ), @@ -327,10 +258,120 @@ class PDFService { } } + // ======================================================================== + // GRANDS LABELS (105 x 57 mm | 2 colonnes, 5 lignes) + // ======================================================================== + static void _addLargeLabels( + pw.Document pdf, + List items, + String Function(T) getId, + String Function(T)? getTitle, + List Function(T)? getDetails, + List qrImages, + PDFGeneratorConfig config, + ) { + // UTILISATION DE LA LARGEUR A4 DIVISÉE PAR 2 + // Cela garantit que 2 colonnes rentrent pile poil (210mm / 2 = 105mm) + final double labelWidth = PdfPageFormat.a4.width / 2; + const double labelHeight = 57.0 * PdfPageFormat.mm; + const int cols = 2; + const int rows = 5; + + final double totalGridWidth = labelWidth * cols; + final double totalGridHeight = labelHeight * rows; + + const double innerQrSize = 45.0 * PdfPageFormat.mm; + + 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: pw.EdgeInsets.zero, + build: (_) => pw.Center( + child: pw.Container( + width: totalGridWidth, + height: totalGridHeight, + child: pw.Wrap( + spacing: 0, // Très important : 0 espace entre les colonnes + runSpacing: 0, // 0 espace entre les lignes + children: List.generate(pageItems.length, (i) { + final item = pageItems[i]; + return pw.Container( + width: labelWidth, + height: labelHeight, + padding: const pw.EdgeInsets.all(6), + // Suppression de la décoration (bordure) + child: pw.Row( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + // QR Code + pw.Container( + width: innerQrSize, + height: innerQrSize, + child: pw.Image(pw.MemoryImage(pageQRs[i])), + ), + pw.SizedBox(width: 8), + // Détails + pw.Expanded( + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + mainAxisAlignment: pw.MainAxisAlignment.center, + children: [ + // Logo + if (_cachedLogoBytes != null && _cachedLogoBytes!.isNotEmpty) + pw.Container( + height: 20, + alignment: pw.Alignment.centerLeft, + margin: const pw.EdgeInsets.only(bottom: 4), + child: pw.Image(pw.MemoryImage(_cachedLogoBytes!)), + ), + // Titre + if (getTitle != null) ...[ + pw.Text( + _truncate(getTitle(item), 40), + style: pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold), + maxLines: 2, + ), + ], + // ID + pw.SizedBox(height: 2), + pw.Text( + getId(item), + style: const pw.TextStyle(fontSize: 9, color: PdfColors.grey700), + ), + // Détails supplémentaires + if (getDetails != null) ...[ + pw.SizedBox(height: 4), + ...getDetails(item).take(4).map((line) { + return pw.Padding( + padding: const pw.EdgeInsets.only(bottom: 1), + child: pw.Text( + _truncate(line, 35), + style: const pw.TextStyle(fontSize: 7, color: PdfColors.grey800), + ), + ); + }), + ], + ], + ), + ), + ], + ), + ); + }), + ), + ), + ), + ), + ); + } + } /// Nettoie le cache (logo) static void clearCache() { _cachedLogoBytes = null; _logoLoadAttempted = false; } -} - +} \ No newline at end of file