Ajout de la gestion des containers (création, édition, suppression, affichage des détails).
Introduction d'un système de génération de QR codes unifié et d'un mode de sélection multiple.
**Features:**
- **Gestion des Containers :**
- Nouvelle page de gestion des containers (`container_management_page.dart`) avec recherche et filtres.
- Formulaire de création/édition de containers (`container_form_page.dart`) avec génération d'ID automatique.
- Page de détails d'un container (`container_detail_page.dart`) affichant son contenu et ses caractéristiques.
- Ajout des routes et du provider (`ContainerProvider`) nécessaires.
- **Modèle de Données :**
- Ajout du `ContainerModel` pour représenter les boîtes, flight cases, etc.
- Le modèle `EquipmentModel` a été enrichi avec des caractéristiques physiques (poids, dimensions).
- **QR Codes :**
- Nouveau service unifié (`UnifiedPDFGeneratorService`) pour générer des PDFs de QR codes pour n'importe quelle entité.
- Services `PDFGeneratorService` et `ContainerPDFGeneratorService` transformés en wrappers pour maintenir la compatibilité.
- Amélioration de la performance de la génération de QR codes en masse.
- **Interface Utilisateur (UI/UX) :**
- Nouvelle page de détails pour le matériel (`equipment_detail_page.dart`).
- Ajout d'un `SelectionModeMixin` pour gérer la sélection multiple dans les pages de gestion.
- Dialogues réutilisables pour l'affichage de QR codes (`QRCodeDialog`) et la sélection de format d'impression (`QRCodeFormatSelectorDialog`).
- Ajout d'un bouton "Gérer les boîtes" sur la page de gestion du matériel.
**Refactorisation :**
- L' `IdGenerator` a été déplacé dans le répertoire `utils` et étendu pour gérer les containers.
- Mise à jour de nombreuses dépendances `pubspec.yaml` vers des versions plus récentes.
- Séparation de la logique d'affichage des containers et du matériel dans des widgets dédiés (`ContainerHeaderCard`, `EquipmentParentContainers`, etc.).
355 lines
13 KiB
Dart
355 lines
13 KiB
Dart
import 'dart:typed_data';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:pdf/pdf.dart';
|
|
import 'package:pdf/widgets.dart' as pw;
|
|
import 'package:em2rp/services/qr_code_service.dart';
|
|
|
|
/// Formats d'étiquettes QR disponibles
|
|
enum QRLabelFormat { small, medium, large }
|
|
|
|
/// Interface pour les items qui peuvent avoir un QR code
|
|
abstract class QRCodeItem {
|
|
String get id;
|
|
String get displayName;
|
|
}
|
|
|
|
/// Service unifié pour la génération de PDFs avec QR codes
|
|
/// Fonctionne avec n'importe quel type d'objet ayant un ID
|
|
class UnifiedPDFGeneratorService {
|
|
static Uint8List? _cachedLogoBytes;
|
|
|
|
/// Tronque un texte s'il dépasse une longueur maximale
|
|
static String _truncateText(String text, int maxLength) {
|
|
if (text.length <= maxLength) {
|
|
return text;
|
|
}
|
|
return '${text.substring(0, maxLength - 3)}...';
|
|
}
|
|
|
|
/// Charge le logo en cache (optimisation)
|
|
static Future<void> _ensureLogoLoaded() async {
|
|
if (_cachedLogoBytes == null) {
|
|
try {
|
|
final logoData = await rootBundle.load('assets/logos/LowQRectangleLogoBlack.png');
|
|
_cachedLogoBytes = logoData.buffer.asUint8List();
|
|
} catch (e) {
|
|
// Logo non disponible, on continue sans
|
|
_cachedLogoBytes = Uint8List(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Génère un PDF avec des QR codes simples (juste ID + QR)
|
|
static Future<Uint8List> generateSimpleQRCodesPDF<T>({
|
|
required List<T> items,
|
|
required String Function(T) getId,
|
|
required QRLabelFormat format,
|
|
String Function(T)? getDisplayName,
|
|
}) async {
|
|
final pdf = pw.Document();
|
|
|
|
switch (format) {
|
|
case QRLabelFormat.small:
|
|
await _generateSmallQRCodesPDF(pdf, items, getId, getDisplayName);
|
|
break;
|
|
case QRLabelFormat.medium:
|
|
await _generateMediumQRCodesPDF(pdf, items, getId, getDisplayName);
|
|
break;
|
|
case QRLabelFormat.large:
|
|
await _generateLargeQRCodesPDF(pdf, items, getId, getDisplayName, null);
|
|
break;
|
|
}
|
|
|
|
return pdf.save();
|
|
}
|
|
|
|
/// Génère un PDF avec des QR codes avancés (avec informations supplémentaires)
|
|
static Future<Uint8List> generateAdvancedQRCodesPDF<T>({
|
|
required List<T> items,
|
|
required String Function(T) getId,
|
|
required String Function(T) getTitle,
|
|
required List<String> Function(T)? getSubtitle,
|
|
required QRLabelFormat format,
|
|
}) async {
|
|
final pdf = pw.Document();
|
|
|
|
switch (format) {
|
|
case QRLabelFormat.small:
|
|
await _generateSmallQRCodesPDF(pdf, items, getId, getTitle);
|
|
break;
|
|
case QRLabelFormat.medium:
|
|
await _generateMediumQRCodesPDF(pdf, items, getId, getTitle);
|
|
break;
|
|
case QRLabelFormat.large:
|
|
await _generateLargeQRCodesPDF(pdf, items, getId, getTitle, getSubtitle);
|
|
break;
|
|
}
|
|
|
|
return pdf.save();
|
|
}
|
|
|
|
// ==========================================================================
|
|
// PETITS QR CODES (2x2 cm, 20 par page)
|
|
// ==========================================================================
|
|
|
|
static Future<void> _generateSmallQRCodesPDF<T>(
|
|
pw.Document pdf,
|
|
List<T> items,
|
|
String Function(T) getId,
|
|
String Function(T)? getDisplayName,
|
|
) async {
|
|
const qrSize = 56.69; // 2cm en points
|
|
const itemsPerPage = 20;
|
|
|
|
// Générer tous les QR codes en une fois (optimisé avec résolution réduite)
|
|
final allQRImages = await QRCodeService.generateBulkQRCodes(
|
|
items.map((item) => getId(item)).toList(),
|
|
size: 150, // Réduit de 200 à 150 pour performance optimale
|
|
withLogo: false,
|
|
useCache: true,
|
|
);
|
|
|
|
for (int pageStart = 0; pageStart < items.length; pageStart += itemsPerPage) {
|
|
final pageItems = items.skip(pageStart).take(itemsPerPage).toList();
|
|
final pageQRImages = allQRImages.skip(pageStart).take(itemsPerPage).toList();
|
|
|
|
pdf.addPage(
|
|
pw.Page(
|
|
pageFormat: PdfPageFormat.a4,
|
|
margin: const pw.EdgeInsets.all(20),
|
|
build: (context) {
|
|
return pw.Wrap(
|
|
spacing: 10,
|
|
runSpacing: 10,
|
|
children: List.generate(pageItems.length, (index) {
|
|
return pw.Container(
|
|
width: qrSize,
|
|
height: qrSize + 20,
|
|
child: pw.Column(
|
|
mainAxisAlignment: pw.MainAxisAlignment.center,
|
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
|
children: [
|
|
pw.Image(pw.MemoryImage(pageQRImages[index])),
|
|
pw.SizedBox(height: 2),
|
|
pw.Text(
|
|
getId(pageItems[index]),
|
|
style: pw.TextStyle(
|
|
fontSize: 6,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
textAlign: pw.TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// QR CODES MOYENS (4x4 cm, 6 par page)
|
|
// ==========================================================================
|
|
|
|
static Future<void> _generateMediumQRCodesPDF<T>(
|
|
pw.Document pdf,
|
|
List<T> items,
|
|
String Function(T) getId,
|
|
String Function(T)? getDisplayName,
|
|
) async {
|
|
const qrSize = 113.39; // 4cm en points
|
|
const itemsPerPage = 6;
|
|
|
|
// Optimisé avec résolution réduite
|
|
final allQRImages = await QRCodeService.generateBulkQRCodes(
|
|
items.map((item) => getId(item)).toList(),
|
|
size: 250, // Réduit de 400 à 250 pour performance optimale
|
|
withLogo: false,
|
|
useCache: true,
|
|
);
|
|
|
|
for (int pageStart = 0; pageStart < items.length; pageStart += itemsPerPage) {
|
|
final pageItems = items.skip(pageStart).take(itemsPerPage).toList();
|
|
final pageQRImages = allQRImages.skip(pageStart).take(itemsPerPage).toList();
|
|
|
|
pdf.addPage(
|
|
pw.Page(
|
|
pageFormat: PdfPageFormat.a4,
|
|
margin: const pw.EdgeInsets.all(20),
|
|
build: (context) {
|
|
return pw.Wrap(
|
|
spacing: 20,
|
|
runSpacing: 20,
|
|
children: List.generate(pageItems.length, (index) {
|
|
return pw.Container(
|
|
width: qrSize,
|
|
height: qrSize + 30,
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
|
mainAxisAlignment: pw.MainAxisAlignment.center,
|
|
children: [
|
|
pw.Image(pw.MemoryImage(pageQRImages[index])),
|
|
pw.SizedBox(height: 4),
|
|
pw.Text(
|
|
getId(pageItems[index]),
|
|
style: pw.TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
textAlign: pw.TextAlign.center,
|
|
),
|
|
if (getDisplayName != null) ...[
|
|
pw.SizedBox(height: 2),
|
|
pw.Text(
|
|
_truncateText(getDisplayName(pageItems[index]), 25),
|
|
style: const pw.TextStyle(
|
|
fontSize: 8,
|
|
color: PdfColors.grey700,
|
|
),
|
|
textAlign: pw.TextAlign.center,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// GRANDES ÉTIQUETTES (QR + infos détaillées, 6 par page)
|
|
// ==========================================================================
|
|
|
|
static Future<void> _generateLargeQRCodesPDF<T>(
|
|
pw.Document pdf,
|
|
List<T> items,
|
|
String Function(T) getId,
|
|
String Function(T)? getTitle,
|
|
List<String> Function(T)? getSubtitle,
|
|
) async {
|
|
const qrSize = 100.0;
|
|
const itemsPerPage = 6;
|
|
|
|
// Charger le logo une seule fois
|
|
await _ensureLogoLoaded();
|
|
|
|
// Générer les QR codes en bulk pour optimisation
|
|
final allQRImages = await QRCodeService.generateBulkQRCodes(
|
|
items.map((item) => getId(item)).toList(),
|
|
size: 300, // Réduit de 400 à 300 pour améliorer la performance
|
|
withLogo: false,
|
|
useCache: true,
|
|
);
|
|
|
|
for (int pageStart = 0; pageStart < items.length; pageStart += itemsPerPage) {
|
|
final pageItems = items.skip(pageStart).take(itemsPerPage).toList();
|
|
final pageQRImages = allQRImages.skip(pageStart).take(itemsPerPage).toList();
|
|
|
|
pdf.addPage(
|
|
pw.Page(
|
|
pageFormat: PdfPageFormat.a4,
|
|
margin: const pw.EdgeInsets.all(20),
|
|
build: (context) {
|
|
return pw.Wrap(
|
|
spacing: 10,
|
|
runSpacing: 10,
|
|
children: List.generate(pageItems.length, (index) {
|
|
final item = pageItems[index];
|
|
return pw.Container(
|
|
width: 260,
|
|
height: 120,
|
|
decoration: pw.BoxDecoration(
|
|
border: pw.Border.all(color: PdfColors.grey400),
|
|
borderRadius: pw.BorderRadius.circular(4),
|
|
),
|
|
padding: const pw.EdgeInsets.all(8),
|
|
child: pw.Row(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
// QR Code
|
|
pw.Container(
|
|
width: qrSize,
|
|
height: qrSize,
|
|
child: pw.Image(
|
|
pw.MemoryImage(pageQRImages[index]),
|
|
fit: pw.BoxFit.contain,
|
|
),
|
|
),
|
|
pw.SizedBox(width: 8),
|
|
// Informations
|
|
pw.Expanded(
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
mainAxisAlignment: pw.MainAxisAlignment.start,
|
|
children: [
|
|
// Logo - CENTRÉ ET PLUS GRAND
|
|
if (_cachedLogoBytes != null && _cachedLogoBytes!.isNotEmpty)
|
|
pw.Center(
|
|
child: pw.Container(
|
|
height: 25, // Augmenté de 15 à 25
|
|
margin: const pw.EdgeInsets.only(bottom: 6),
|
|
child: pw.Image(
|
|
pw.MemoryImage(_cachedLogoBytes!),
|
|
fit: pw.BoxFit.contain,
|
|
),
|
|
),
|
|
),
|
|
// ID (toujours affiché sur plusieurs lignes si nécessaire)
|
|
if (getTitle != null) ...[
|
|
pw.SizedBox(height: 2),
|
|
pw.Text(
|
|
_truncateText(getTitle(item), 20),
|
|
style: pw.TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
maxLines: 2,
|
|
),
|
|
],
|
|
pw.SizedBox(height: 2),
|
|
pw.Text(
|
|
getId(item),
|
|
style: const pw.TextStyle(
|
|
fontSize: 8,
|
|
color: PdfColors.grey700,
|
|
),
|
|
maxLines: 1,
|
|
),
|
|
if (getSubtitle != null) ...[
|
|
pw.SizedBox(height: 4),
|
|
...getSubtitle(item).take(5).map((line) {
|
|
return pw.Padding(
|
|
padding: const pw.EdgeInsets.only(bottom: 1),
|
|
child: pw.Text(
|
|
_truncateText(line, 25),
|
|
style: const pw.TextStyle(
|
|
fontSize: 6,
|
|
color: PdfColors.grey800,
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|