feat: Introduce PDFService for optimized PDF generation and caching in container and equipment management

This commit is contained in:
ElPoyo
2025-10-30 18:45:50 +01:00
parent 822d4443f9
commit 6abb8f1d14
8 changed files with 429 additions and 513 deletions

View File

@@ -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<ContainerManagementPage>
final TextEditingController _searchController = TextEditingController();
ContainerType? _selectedType;
EquipmentStatus? _selectedStatus;
List<ContainerModel>? _cachedContainers; // Cache pour éviter le rebuild
@override
void dispose() {
@@ -321,7 +322,13 @@ class _ContainerManagementPageState extends State<ContainerManagementPage>
return StreamBuilder<List<ContainerModel>>(
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<ContainerManagementPage>
);
}
final containers = snapshot.data ?? [];
final containers = _cachedContainers ?? snapshot.data ?? [];
if (containers.isEmpty) {
return Center(
@@ -635,7 +642,7 @@ class _ContainerManagementPageState extends State<ContainerManagementPage>
}
// Afficher le dialogue de sélection de format
final format = await showDialog<ContainerQRLabelFormat>(
final format = await showDialog<QRLabelFormat>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Format des étiquettes'),
@@ -646,19 +653,19 @@ class _ContainerManagementPageState extends State<ContainerManagementPage>
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<ContainerManagementPage>
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<ContainerModel>(
items: selectedContainers,
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) {

View File

@@ -25,6 +25,7 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
with SelectionModeMixin<EquipmentManagementPage> {
final TextEditingController _searchController = TextEditingController();
EquipmentCategory? _selectedCategory;
List<EquipmentModel>? _cachedEquipment;
@override
void dispose() {
@@ -420,7 +421,13 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
return StreamBuilder<List<EquipmentModel>>(
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<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(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -452,15 +462,15 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
);
}
// 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<EquipmentModel>.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]);
},
);
},

View File

@@ -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<EquipmentModel>(
items: equipmentList,
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