feat: Introduce PDFService for optimized PDF generation and caching in container and equipment management
This commit is contained in:
@@ -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<Uint8List> generateQRCodesPDF({
|
|
||||||
required List<ContainerModel> containerList,
|
|
||||||
required Map<String, List<EquipmentModel>> 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 = <String>[
|
|
||||||
'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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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<Uint8List> generateQRCodesPDF({
|
|
||||||
required List<EquipmentModel> 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 = <String>[];
|
|
||||||
|
|
||||||
// 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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
336
em2rp/lib/services/pdf_service.dart
Normal file
336
em2rp/lib/services/pdf_service.dart
Normal file
@@ -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<void> _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<Uint8List> generatePDF<T>({
|
||||||
|
required List<T> items,
|
||||||
|
required QRLabelFormat format,
|
||||||
|
required String Function(T) getId,
|
||||||
|
String Function(T)? getTitle,
|
||||||
|
List<String> 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<T>(
|
||||||
|
pw.Document pdf,
|
||||||
|
List<T> items,
|
||||||
|
String Function(T) getId,
|
||||||
|
List<Uint8List> 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<T>(
|
||||||
|
pw.Document pdf,
|
||||||
|
List<T> items,
|
||||||
|
String Function(T) getId,
|
||||||
|
String Function(T)? getTitle,
|
||||||
|
List<Uint8List> 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<T>(
|
||||||
|
pw.Document pdf,
|
||||||
|
List<T> items,
|
||||||
|
String Function(T) getId,
|
||||||
|
String Function(T)? getTitle,
|
||||||
|
List<String> Function(T)? getDetails,
|
||||||
|
List<Uint8List> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -111,13 +111,16 @@ class QRCodeService {
|
|||||||
return _cachedLogoImage!;
|
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<List<Uint8List>> generateBulkQRCodes(
|
static Future<List<Uint8List>> generateBulkQRCodes(
|
||||||
List<String> dataList, {
|
List<String> dataList, {
|
||||||
double size = 512,
|
double size = 512,
|
||||||
bool withLogo = false,
|
bool withLogo = false,
|
||||||
bool useCache = true,
|
bool useCache = true,
|
||||||
|
Function(int, int)? onProgress, // Callback de progression
|
||||||
}) async {
|
}) async {
|
||||||
|
if (dataList.isEmpty) return [];
|
||||||
|
|
||||||
// Si tout est en cache, retourner immédiatement
|
// Si tout est en cache, retourner immédiatement
|
||||||
if (useCache) {
|
if (useCache) {
|
||||||
final allCached = dataList.every((data) {
|
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;
|
int batchSize;
|
||||||
if (size <= 200) {
|
if (dataList.length <= 10) {
|
||||||
batchSize = 100; // Petits QR : lots de 100
|
// 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) {
|
} else if (size <= 300) {
|
||||||
batchSize = 50; // Moyens QR : lots de 50
|
batchSize = 10; // Moyens QR : lots de 10 (réduit de 50)
|
||||||
} else if (size <= 500) {
|
} else if (size <= 400) {
|
||||||
batchSize = 20; // Grands QR : lots de 20
|
batchSize = 5; // Grands QR : lots de 5 (réduit de 20)
|
||||||
} else {
|
} else {
|
||||||
batchSize = 10; // Très grands : lots de 10
|
batchSize = 3; // Très grands : lots de 3 (réduit de 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Uint8List> results = [];
|
final List<Uint8List> results = [];
|
||||||
|
int processed = 0;
|
||||||
|
|
||||||
for (int i = 0; i < dataList.length; i += batchSize) {
|
for (int i = 0; i < dataList.length; i += batchSize) {
|
||||||
final batch = dataList.skip(i).take(batchSize).toList();
|
final batch = dataList.skip(i).take(batchSize).toList();
|
||||||
|
|
||||||
|
// Générer le batch
|
||||||
final batchResults = await Future.wait(
|
final batchResults = await Future.wait(
|
||||||
batch.map((data) => withLogo
|
batch.map((data) => withLogo
|
||||||
? generateQRCodeWithLogo(data, size: size, useCache: useCache)
|
? generateQRCodeWithLogo(data, size: size, useCache: useCache)
|
||||||
: generateQRCode(data, size: size, useCache: useCache)),
|
: generateQRCode(data, size: size, useCache: useCache)),
|
||||||
);
|
);
|
||||||
|
|
||||||
results.addAll(batchResults);
|
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;
|
return results;
|
||||||
|
|||||||
@@ -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<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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
|||||||
import 'package:em2rp/providers/container_provider.dart';
|
import 'package:em2rp/providers/container_provider.dart';
|
||||||
import 'package:em2rp/models/container_model.dart';
|
import 'package:em2rp/models/container_model.dart';
|
||||||
import 'package:em2rp/models/equipment_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/views/widgets/common/qr_code_dialog.dart';
|
||||||
import 'package:em2rp/mixins/selection_mode_mixin.dart';
|
import 'package:em2rp/mixins/selection_mode_mixin.dart';
|
||||||
import 'package:printing/printing.dart';
|
import 'package:printing/printing.dart';
|
||||||
@@ -26,6 +26,7 @@ class _ContainerManagementPageState extends State<ContainerManagementPage>
|
|||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
ContainerType? _selectedType;
|
ContainerType? _selectedType;
|
||||||
EquipmentStatus? _selectedStatus;
|
EquipmentStatus? _selectedStatus;
|
||||||
|
List<ContainerModel>? _cachedContainers; // Cache pour éviter le rebuild
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -321,7 +322,13 @@ class _ContainerManagementPageState extends State<ContainerManagementPage>
|
|||||||
return StreamBuilder<List<ContainerModel>>(
|
return StreamBuilder<List<ContainerModel>>(
|
||||||
stream: provider.containersStream,
|
stream: provider.containersStream,
|
||||||
builder: (context, snapshot) {
|
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());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +338,7 @@ class _ContainerManagementPageState extends State<ContainerManagementPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final containers = snapshot.data ?? [];
|
final containers = _cachedContainers ?? snapshot.data ?? [];
|
||||||
|
|
||||||
if (containers.isEmpty) {
|
if (containers.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
@@ -635,7 +642,7 @@ class _ContainerManagementPageState extends State<ContainerManagementPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Afficher le dialogue de sélection de format
|
// Afficher le dialogue de sélection de format
|
||||||
final format = await showDialog<ContainerQRLabelFormat>(
|
final format = await showDialog<QRLabelFormat>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('Format des étiquettes'),
|
title: const Text('Format des étiquettes'),
|
||||||
@@ -646,19 +653,19 @@ class _ContainerManagementPageState extends State<ContainerManagementPage>
|
|||||||
leading: const Icon(Icons.qr_code_2),
|
leading: const Icon(Icons.qr_code_2),
|
||||||
title: const Text('Petits QR codes'),
|
title: const Text('Petits QR codes'),
|
||||||
subtitle: const Text('2×2 cm - QR code + ID (20 par page)'),
|
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(
|
ListTile(
|
||||||
leading: const Icon(Icons.qr_code),
|
leading: const Icon(Icons.qr_code),
|
||||||
title: const Text('QR codes moyens'),
|
title: const Text('QR codes moyens'),
|
||||||
subtitle: const Text('4×4 cm - QR code + ID (6 par page)'),
|
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(
|
ListTile(
|
||||||
leading: const Icon(Icons.label),
|
leading: const Icon(Icons.label),
|
||||||
title: const Text('Grandes étiquettes'),
|
title: const Text('Grandes étiquettes'),
|
||||||
subtitle: const Text('QR code + ID + Type + Contenu (6 par page)'),
|
subtitle: const Text('QR code + ID + Type + Contenu (10 par page)'),
|
||||||
onTap: () => Navigator.pop(context, ContainerQRLabelFormat.large),
|
onTap: () => Navigator.pop(context, QRLabelFormat.large),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -673,12 +680,21 @@ class _ContainerManagementPageState extends State<ContainerManagementPage>
|
|||||||
|
|
||||||
if (format == null || !mounted) return;
|
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 {
|
try {
|
||||||
final pdfBytes = await ContainerPDFGeneratorService.generateQRCodesPDF(
|
final pdfBytes = await PDFService.generatePDF<ContainerModel>(
|
||||||
containerList: selectedContainers,
|
items: selectedContainers,
|
||||||
containerEquipmentMap: containerEquipmentMap,
|
|
||||||
format: format,
|
format: format,
|
||||||
|
getId: (c) => c.id,
|
||||||
|
getTitle: (c) => c.name,
|
||||||
|
getDetails: format == QRLabelFormat.large ? (ContainerModel c) {
|
||||||
|
final equipment = containerEquipmentMap[c.id] ?? <EquipmentModel>[];
|
||||||
|
return [
|
||||||
|
'Contenu (${equipment.length}):',
|
||||||
|
...equipment.take(5).map((eq) => '- ${eq.id}'),
|
||||||
|
if (equipment.length > 5) '... +${equipment.length - 5}',
|
||||||
|
];
|
||||||
|
} : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
with SelectionModeMixin<EquipmentManagementPage> {
|
with SelectionModeMixin<EquipmentManagementPage> {
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
EquipmentCategory? _selectedCategory;
|
EquipmentCategory? _selectedCategory;
|
||||||
|
List<EquipmentModel>? _cachedEquipment;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -420,7 +421,13 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
return StreamBuilder<List<EquipmentModel>>(
|
return StreamBuilder<List<EquipmentModel>>(
|
||||||
stream: provider.equipmentStream,
|
stream: provider.equipmentStream,
|
||||||
builder: (context, snapshot) {
|
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());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,7 +437,10 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -452,15 +462,15 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tri par nom
|
// Créer une copie pour le tri
|
||||||
final equipment = snapshot.data!;
|
final sortedEquipment = List<EquipmentModel>.from(equipment);
|
||||||
equipment.sort((a, b) => a.name.compareTo(b.name));
|
sortedEquipment.sort((a, b) => a.name.compareTo(b.name));
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: equipment.length,
|
itemCount: sortedEquipment.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return _buildEquipmentCard(equipment[index]);
|
return _buildEquipmentCard(sortedEquipment[index]);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:em2rp/utils/colors.dart';
|
import 'package:em2rp/utils/colors.dart';
|
||||||
import 'package:em2rp/models/equipment_model.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';
|
import 'package:printing/printing.dart';
|
||||||
|
|
||||||
/// Widget réutilisable pour sélectionner le format de génération de QR codes multiples
|
/// 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 {
|
try {
|
||||||
// Génération du PDF
|
// Génération du PDF avec progression
|
||||||
final pdfBytes = await PDFGeneratorService.generateQRCodesPDF(
|
final pdfBytes = await PDFService.generatePDF<EquipmentModel>(
|
||||||
equipmentList: equipmentList,
|
items: equipmentList,
|
||||||
format: format,
|
format: format,
|
||||||
|
getId: (eq) => eq.id,
|
||||||
|
getTitle: (eq) => '${eq.brand ?? ''} ${eq.model ?? ''}'.trim(),
|
||||||
|
getDetails: format == QRLabelFormat.large ? (EquipmentModel eq) {
|
||||||
|
final details = <String>[];
|
||||||
|
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
|
// Fermer le dialogue de chargement
|
||||||
|
|||||||
Reference in New Issue
Block a user