feat: Gestion complète des containers et refactorisation du matériel

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.).
This commit is contained in:
ElPoyo
2025-10-29 10:57:42 +01:00
parent ae3a1b7227
commit 3fab69cb00
31 changed files with 6540 additions and 656 deletions

View File

@@ -0,0 +1,193 @@
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/services/qr_code_service.dart';
import 'package:printing/printing.dart';
/// Widget réutilisable pour afficher un QR code avec option de téléchargement
/// Utilisable pour équipements, containers, et autres entités
class QRCodeDialog<T> extends StatelessWidget {
final T item;
final String Function(T) getId;
final String Function(T) getTitle;
final List<Widget> Function(T)? buildSubtitle;
const QRCodeDialog({
super.key,
required this.item,
required this.getId,
required this.getTitle,
this.buildSubtitle,
});
@override
Widget build(BuildContext context) {
final id = getId(item);
final title = getTitle(item);
return Dialog(
child: Container(
padding: const EdgeInsets.all(24),
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// En-tête
Row(
children: [
const Icon(Icons.qr_code, color: AppColors.rouge, size: 32),
const SizedBox(width: 12),
Expanded(
child: Text(
'QR Code - $id',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
const SizedBox(height: 24),
// QR Code
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: QrImageView(
data: id,
version: QrVersions.auto,
size: 250,
backgroundColor: Colors.white,
),
),
const SizedBox(height: 16),
// Informations
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
id,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
title,
style: TextStyle(color: Colors.grey[700]),
),
if (buildSubtitle != null) ...[
const SizedBox(height: 4),
...buildSubtitle!(item),
],
],
),
),
const SizedBox(height: 24),
// Bouton télécharger
ElevatedButton.icon(
onPressed: () => _downloadQRCode(context, id),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.rouge,
minimumSize: const Size(double.infinity, 48),
),
icon: const Icon(Icons.download, color: Colors.white),
label: const Text(
'Télécharger l\'image',
style: TextStyle(color: Colors.white),
),
),
],
),
),
);
}
Future<void> _downloadQRCode(BuildContext context, String id) async {
try {
// Générer l'image QR code en haute résolution
final qrImage = await QRCodeService.generateQRCode(
id,
size: 1024,
useCache: false,
);
// Utiliser la bibliothèque printing pour sauvegarder l'image
await Printing.sharePdf(
bytes: qrImage,
filename: 'QRCode_$id.png',
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Image QR Code téléchargée avec succès'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors du téléchargement: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
/// Factory pour équipement
static QRCodeDialog forEquipment(dynamic equipment) {
return QRCodeDialog(
item: equipment,
getId: (eq) => eq.id,
getTitle: (eq) => '${eq.brand ?? ''} ${eq.model ?? ''}'.trim(),
);
}
/// Factory pour container
static QRCodeDialog forContainer(dynamic container) {
return QRCodeDialog(
item: container,
getId: (c) => c.id,
getTitle: (c) => c.name,
buildSubtitle: (c) {
return [
Text(
_getContainerTypeLabel(c.type),
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
];
},
);
}
static String _getContainerTypeLabel(dynamic type) {
// Simple fallback - à améliorer avec import du model
return type.toString().split('.').last;
}
}

View File

@@ -0,0 +1,258 @@
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:printing/printing.dart';
/// Widget réutilisable pour sélectionner le format de génération de QR codes multiples
class QRCodeFormatSelectorDialog extends StatelessWidget {
final List<EquipmentModel> equipmentList;
const QRCodeFormatSelectorDialog({
super.key,
required this.equipmentList,
});
@override
Widget build(BuildContext context) {
return Dialog(
child: Container(
padding: const EdgeInsets.all(24),
constraints: const BoxConstraints(maxWidth: 600, maxHeight: 700),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// En-tête
Row(
children: [
const Icon(Icons.qr_code_2, color: AppColors.rouge, size: 32),
const SizedBox(width: 12),
Expanded(
child: Text(
'Générer ${equipmentList.length} QR Codes',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
const SizedBox(height: 24),
const Text(
'Choisissez un format d\'étiquette :',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 16),
// Liste des équipements
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
),
child: ListView.separated(
shrinkWrap: true,
itemCount: equipmentList.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final equipment = equipmentList[index];
return ListTile(
dense: true,
leading: const Icon(Icons.qr_code, size: 20),
title: Text(
equipment.id,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
'${equipment.brand ?? ''} ${equipment.model ?? ''}'.trim(),
style: const TextStyle(fontSize: 12),
),
);
},
),
),
),
const SizedBox(height: 24),
// Boutons de format
_FormatButton(
icon: Icons.qr_code,
title: 'Petits QR Codes',
subtitle: 'QR codes compacts (2x2 cm)',
onPressed: () {
Navigator.pop(context);
_generatePDF(context, equipmentList, QRLabelFormat.small);
},
),
const SizedBox(height: 12),
_FormatButton(
icon: Icons.qr_code_2,
title: 'QR Moyens',
subtitle: 'QR codes taille moyenne (4x4 cm)',
onPressed: () {
Navigator.pop(context);
_generatePDF(context, equipmentList, QRLabelFormat.medium);
},
),
const SizedBox(height: 12),
_FormatButton(
icon: Icons.label,
title: 'Grandes étiquettes',
subtitle: 'QR + ID + Marque/Modèle (10x5 cm)',
onPressed: () {
Navigator.pop(context);
_generatePDF(context, equipmentList, QRLabelFormat.large);
},
),
],
),
),
);
}
Future<void> _generatePDF(
BuildContext context,
List<EquipmentModel> equipmentList,
QRLabelFormat format,
) async {
// Afficher le dialogue de chargement
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => Dialog(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(AppColors.rouge),
),
const SizedBox(height: 20),
const Text(
'Génération du PDF en cours...',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Génération de ${equipmentList.length} QR code(s)',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
),
),
);
try {
// Génération du PDF
final pdfBytes = await PDFGeneratorService.generateQRCodesPDF(
equipmentList: equipmentList,
format: format,
);
// Fermer le dialogue de chargement
if (context.mounted) {
Navigator.pop(context);
}
// Afficher le PDF
await Printing.layoutPdf(
onLayout: (format) async => pdfBytes,
name: 'QRCodes_${DateTime.now().millisecondsSinceEpoch}.pdf',
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('PDF généré avec succès'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
// Fermer le dialogue de chargement en cas d'erreur
if (context.mounted) {
Navigator.pop(context);
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur lors de la génération du PDF: $e')),
);
}
}
}
}
/// Bouton de sélection de format
class _FormatButton extends StatelessWidget {
final IconData icon;
final String title;
final String subtitle;
final VoidCallback onPressed;
const _FormatButton({
required this.icon,
required this.title,
required this.subtitle,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(icon, color: AppColors.rouge, size: 32),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
color: Colors.grey[600],
fontSize: 13,
),
),
],
),
),
const Icon(Icons.arrow_forward_ios, size: 16),
],
),
),
);
}
}