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:
		
							
								
								
									
										193
									
								
								em2rp/lib/views/widgets/common/qr_code_dialog.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								em2rp/lib/views/widgets/common/qr_code_dialog.dart
									
									
									
									
									
										Normal 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; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -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), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										110
									
								
								em2rp/lib/views/widgets/containers/container_equipment_tile.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								em2rp/lib/views/widgets/containers/container_equipment_tile.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:em2rp/utils/colors.dart'; | ||||
| import 'package:em2rp/models/equipment_model.dart'; | ||||
|  | ||||
| /// Widget pour afficher un équipement dans la liste d'un container | ||||
| class ContainerEquipmentTile extends StatelessWidget { | ||||
|   final EquipmentModel equipment; | ||||
|   final VoidCallback onView; | ||||
|   final VoidCallback onRemove; | ||||
|  | ||||
|   const ContainerEquipmentTile({ | ||||
|     super.key, | ||||
|     required this.equipment, | ||||
|     required this.onView, | ||||
|     required this.onRemove, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return ListTile( | ||||
|       contentPadding: const EdgeInsets.symmetric(vertical: 8), | ||||
|       leading: CircleAvatar( | ||||
|         backgroundColor: AppColors.rouge.withOpacity(0.1), | ||||
|         child: const Icon(Icons.inventory_2, color: AppColors.rouge), | ||||
|       ), | ||||
|       title: Text( | ||||
|         equipment.id, | ||||
|         style: const TextStyle(fontWeight: FontWeight.bold), | ||||
|       ), | ||||
|       subtitle: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           if (equipment.brand != null || equipment.model != null) | ||||
|             Text('${equipment.brand ?? ''} ${equipment.model ?? ''}'), | ||||
|           const SizedBox(height: 4), | ||||
|           Row( | ||||
|             children: [ | ||||
|               _buildSmallBadge( | ||||
|                 _getCategoryLabel(equipment.category), | ||||
|                 Colors.blue, | ||||
|               ), | ||||
|               const SizedBox(width: 8), | ||||
|               if (equipment.weight != null) | ||||
|                 _buildSmallBadge( | ||||
|                   '${equipment.weight} kg', | ||||
|                   Colors.grey, | ||||
|                 ), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       trailing: Row( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: [ | ||||
|           IconButton( | ||||
|             icon: const Icon(Icons.visibility, size: 20), | ||||
|             tooltip: 'Voir détails', | ||||
|             onPressed: onView, | ||||
|           ), | ||||
|           IconButton( | ||||
|             icon: const Icon(Icons.remove_circle, color: Colors.red, size: 20), | ||||
|             tooltip: 'Retirer', | ||||
|             onPressed: onRemove, | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildSmallBadge(String label, Color color) { | ||||
|     return Container( | ||||
|       padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|       decoration: BoxDecoration( | ||||
|         color: color.withOpacity(0.1), | ||||
|         borderRadius: BorderRadius.circular(12), | ||||
|         border: Border.all(color: color.withOpacity(0.3)), | ||||
|       ), | ||||
|       child: Text( | ||||
|         label, | ||||
|         style: TextStyle( | ||||
|           fontSize: 11, | ||||
|           color: color, | ||||
|           fontWeight: FontWeight.bold, | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   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.consumable: | ||||
|         return 'Consommable'; | ||||
|       case EquipmentCategory.cable: | ||||
|         return 'Câble'; | ||||
|       case EquipmentCategory.other: | ||||
|         return 'Autre'; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										197
									
								
								em2rp/lib/views/widgets/containers/container_header_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								em2rp/lib/views/widgets/containers/container_header_card.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,197 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:em2rp/utils/colors.dart'; | ||||
| import 'package:em2rp/models/container_model.dart'; | ||||
| import 'package:em2rp/models/equipment_model.dart'; | ||||
|  | ||||
| /// Widget pour afficher la carte d'en-tête d'un container | ||||
| class ContainerHeaderCard extends StatelessWidget { | ||||
|   final ContainerModel container; | ||||
|   final List<EquipmentModel> equipmentList; | ||||
|  | ||||
|   const ContainerHeaderCard({ | ||||
|     super.key, | ||||
|     required this.container, | ||||
|     required this.equipmentList, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Card( | ||||
|       elevation: 2, | ||||
|       child: Padding( | ||||
|         padding: const EdgeInsets.all(20), | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             Row( | ||||
|               children: [ | ||||
|                 Icon( | ||||
|                   _getTypeIcon(container.type), | ||||
|                   size: 60, | ||||
|                   color: AppColors.rouge, | ||||
|                 ), | ||||
|                 const SizedBox(width: 20), | ||||
|                 Expanded( | ||||
|                   child: Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       Text( | ||||
|                         container.id, | ||||
|                         style: const TextStyle( | ||||
|                           fontSize: 24, | ||||
|                           fontWeight: FontWeight.bold, | ||||
|                         ), | ||||
|                       ), | ||||
|                       const SizedBox(height: 4), | ||||
|                       Text( | ||||
|                         container.name, | ||||
|                         style: TextStyle( | ||||
|                           fontSize: 16, | ||||
|                           color: Colors.grey.shade700, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|             const Divider(height: 32), | ||||
|             Row( | ||||
|               children: [ | ||||
|                 Expanded( | ||||
|                   child: _buildInfoItem( | ||||
|                     context, | ||||
|                     'Type', | ||||
|                     containerTypeLabel(container.type), | ||||
|                     Icons.category, | ||||
|                   ), | ||||
|                 ), | ||||
|                 Expanded( | ||||
|                   child: _buildInfoItem( | ||||
|                     context, | ||||
|                     'Statut', | ||||
|                     _getStatusLabel(container.status), | ||||
|                     Icons.info, | ||||
|                     statusColor: _getStatusColor(container.status), | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|             const SizedBox(height: 12), | ||||
|             Row( | ||||
|               children: [ | ||||
|                 Expanded( | ||||
|                   child: _buildInfoItem( | ||||
|                     context, | ||||
|                     'Équipements', | ||||
|                     '${container.itemCount}', | ||||
|                     Icons.inventory, | ||||
|                   ), | ||||
|                 ), | ||||
|                 Expanded( | ||||
|                   child: _buildInfoItem( | ||||
|                     context, | ||||
|                     'Poids total', | ||||
|                     _calculateTotalWeight(), | ||||
|                     Icons.scale, | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildInfoItem( | ||||
|     BuildContext context, | ||||
|     String label, | ||||
|     String value, | ||||
|     IconData icon, { | ||||
|     Color? statusColor, | ||||
|   }) { | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         Row( | ||||
|           children: [ | ||||
|             Icon(icon, size: 16, color: Colors.grey.shade600), | ||||
|             const SizedBox(width: 6), | ||||
|             Text( | ||||
|               label, | ||||
|               style: TextStyle( | ||||
|                 fontSize: 12, | ||||
|                 color: Colors.grey.shade600, | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|         const SizedBox(height: 4), | ||||
|         Text( | ||||
|           value, | ||||
|           style: TextStyle( | ||||
|             fontSize: 16, | ||||
|             fontWeight: FontWeight.bold, | ||||
|             color: statusColor, | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   String _calculateTotalWeight() { | ||||
|     if (equipmentList.isEmpty && container.weight == null) { | ||||
|       return '-'; | ||||
|     } | ||||
|  | ||||
|     final totalWeight = container.calculateTotalWeight(equipmentList); | ||||
|     return '${totalWeight.toStringAsFixed(1)} kg'; | ||||
|   } | ||||
|  | ||||
|   IconData _getTypeIcon(ContainerType type) { | ||||
|     switch (type) { | ||||
|       case ContainerType.flightCase: | ||||
|         return Icons.work; | ||||
|       case ContainerType.pelicase: | ||||
|         return Icons.work_outline; | ||||
|       case ContainerType.bag: | ||||
|         return Icons.shopping_bag; | ||||
|       case ContainerType.openCrate: | ||||
|         return Icons.inventory_2; | ||||
|       case ContainerType.toolbox: | ||||
|         return Icons.handyman; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   String _getStatusLabel(EquipmentStatus status) { | ||||
|     switch (status) { | ||||
|       case EquipmentStatus.available: | ||||
|         return 'Disponible'; | ||||
|       case EquipmentStatus.inUse: | ||||
|         return 'En prestation'; | ||||
|       case EquipmentStatus.maintenance: | ||||
|         return 'Maintenance'; | ||||
|       case EquipmentStatus.outOfService: | ||||
|         return 'Hors service'; | ||||
|       default: | ||||
|         return 'Autre'; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Color _getStatusColor(EquipmentStatus status) { | ||||
|     switch (status) { | ||||
|       case EquipmentStatus.available: | ||||
|         return Colors.green; | ||||
|       case EquipmentStatus.inUse: | ||||
|         return Colors.orange; | ||||
|       case EquipmentStatus.maintenance: | ||||
|         return Colors.blue; | ||||
|       case EquipmentStatus.outOfService: | ||||
|         return Colors.red; | ||||
|       default: | ||||
|         return Colors.grey; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,91 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:em2rp/utils/colors.dart'; | ||||
| import 'package:em2rp/models/container_model.dart'; | ||||
|  | ||||
| /// Widget pour afficher les caractéristiques physiques d'un container | ||||
| class ContainerPhysicalCharacteristics extends StatelessWidget { | ||||
|   final ContainerModel container; | ||||
|  | ||||
|   const ContainerPhysicalCharacteristics({ | ||||
|     super.key, | ||||
|     required this.container, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final hasDimensions = container.length != null || | ||||
|         container.width != null || | ||||
|         container.height != null; | ||||
|     final hasWeight = container.weight != null; | ||||
|     final hasVolume = container.volume != null; | ||||
|  | ||||
|     if (!hasDimensions && !hasWeight) { | ||||
|       return const SizedBox.shrink(); | ||||
|     } | ||||
|  | ||||
|     return Card( | ||||
|       elevation: 2, | ||||
|       child: Padding( | ||||
|         padding: const EdgeInsets.all(20), | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             const Text( | ||||
|               'Caractéristiques physiques', | ||||
|               style: TextStyle( | ||||
|                 fontSize: 18, | ||||
|                 fontWeight: FontWeight.bold, | ||||
|               ), | ||||
|             ), | ||||
|             const Divider(height: 24), | ||||
|             if (hasWeight) | ||||
|               _buildCharacteristicRow( | ||||
|                 'Poids à vide', | ||||
|                 '${container.weight} kg', | ||||
|                 Icons.scale, | ||||
|               ), | ||||
|             if (hasDimensions) ...[ | ||||
|               if (hasWeight) const SizedBox(height: 12), | ||||
|               _buildCharacteristicRow( | ||||
|                 'Dimensions (L×l×H)', | ||||
|                 '${container.length ?? '?'} × ${container.width ?? '?'} × ${container.height ?? '?'} cm', | ||||
|                 Icons.straighten, | ||||
|               ), | ||||
|             ], | ||||
|             if (hasVolume) ...[ | ||||
|               const SizedBox(height: 12), | ||||
|               _buildCharacteristicRow( | ||||
|                 'Volume', | ||||
|                 '${container.volume!.toStringAsFixed(3)} m³', | ||||
|                 Icons.view_in_ar, | ||||
|               ), | ||||
|             ], | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildCharacteristicRow(String label, String value, IconData icon) { | ||||
|     return Row( | ||||
|       children: [ | ||||
|         Icon(icon, size: 20, color: AppColors.rouge), | ||||
|         const SizedBox(width: 12), | ||||
|         Expanded( | ||||
|           child: Text( | ||||
|             label, | ||||
|             style: const TextStyle(fontSize: 14), | ||||
|           ), | ||||
|         ), | ||||
|         Text( | ||||
|           value, | ||||
|           style: const TextStyle( | ||||
|             fontSize: 14, | ||||
|             fontWeight: FontWeight.bold, | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,177 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:em2rp/utils/colors.dart'; | ||||
| import 'package:em2rp/models/container_model.dart'; | ||||
| import 'package:em2rp/providers/container_provider.dart'; | ||||
|  | ||||
| /// Widget pour afficher les containers parents d'un équipement | ||||
| class EquipmentParentContainers extends StatefulWidget { | ||||
|   final List<String> parentBoxIds; | ||||
|  | ||||
|   const EquipmentParentContainers({ | ||||
|     super.key, | ||||
|     required this.parentBoxIds, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<EquipmentParentContainers> createState() => _EquipmentParentContainersState(); | ||||
| } | ||||
|  | ||||
| class _EquipmentParentContainersState extends State<EquipmentParentContainers> { | ||||
|   List<ContainerModel> _containers = []; | ||||
|   bool _isLoading = true; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _loadContainers(); | ||||
|   } | ||||
|  | ||||
|   Future<void> _loadContainers() async { | ||||
|     if (widget.parentBoxIds.isEmpty) { | ||||
|       setState(() { | ||||
|         _isLoading = false; | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     setState(() { | ||||
|       _isLoading = true; | ||||
|     }); | ||||
|  | ||||
|     try { | ||||
|       final containerProvider = context.read<ContainerProvider>(); | ||||
|       final List<ContainerModel> containers = []; | ||||
|  | ||||
|       for (final boxId in widget.parentBoxIds) { | ||||
|         final container = await containerProvider.getContainerById(boxId); | ||||
|         if (container != null) { | ||||
|           containers.add(container); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       setState(() { | ||||
|         _containers = containers; | ||||
|         _isLoading = false; | ||||
|       }); | ||||
|     } catch (e) { | ||||
|       setState(() { | ||||
|         _isLoading = false; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (widget.parentBoxIds.isEmpty) { | ||||
|       return const SizedBox.shrink(); | ||||
|     } | ||||
|  | ||||
|     return Card( | ||||
|       elevation: 2, | ||||
|       child: Padding( | ||||
|         padding: const EdgeInsets.all(20), | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             Row( | ||||
|               children: [ | ||||
|                 const Icon(Icons.inventory_2, color: AppColors.rouge, size: 20), | ||||
|                 const SizedBox(width: 8), | ||||
|                 const Text( | ||||
|                   'Containers', | ||||
|                   style: TextStyle( | ||||
|                     fontSize: 18, | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|             const Divider(height: 24), | ||||
|             if (_isLoading) | ||||
|               const Center( | ||||
|                 child: Padding( | ||||
|                   padding: EdgeInsets.all(16.0), | ||||
|                   child: CircularProgressIndicator(), | ||||
|                 ), | ||||
|               ) | ||||
|             else if (_containers.isEmpty) | ||||
|               const Center( | ||||
|                 child: Padding( | ||||
|                   padding: EdgeInsets.all(16.0), | ||||
|                   child: Text( | ||||
|                     'Cet équipement n\'est dans aucun container', | ||||
|                     style: TextStyle(color: Colors.grey), | ||||
|                   ), | ||||
|                 ), | ||||
|               ) | ||||
|             else | ||||
|               ListView.separated( | ||||
|                 shrinkWrap: true, | ||||
|                 physics: const NeverScrollableScrollPhysics(), | ||||
|                 itemCount: _containers.length, | ||||
|                 separatorBuilder: (context, index) => const Divider(height: 1), | ||||
|                 itemBuilder: (context, index) { | ||||
|                   final container = _containers[index]; | ||||
|                   return _buildContainerTile(container); | ||||
|                 }, | ||||
|               ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildContainerTile(ContainerModel container) { | ||||
|     return ListTile( | ||||
|       contentPadding: const EdgeInsets.symmetric(vertical: 8), | ||||
|       leading: Icon( | ||||
|         _getTypeIcon(container.type), | ||||
|         color: AppColors.rouge, | ||||
|         size: 32, | ||||
|       ), | ||||
|       title: Text( | ||||
|         container.id, | ||||
|         style: const TextStyle(fontWeight: FontWeight.bold), | ||||
|       ), | ||||
|       subtitle: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Text(container.name), | ||||
|           const SizedBox(height: 4), | ||||
|           Text( | ||||
|             containerTypeLabel(container.type), | ||||
|             style: TextStyle( | ||||
|               fontSize: 12, | ||||
|               color: Colors.grey.shade600, | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       trailing: const Icon(Icons.chevron_right), | ||||
|       onTap: () { | ||||
|         Navigator.pushNamed( | ||||
|           context, | ||||
|           '/container_detail', | ||||
|           arguments: container, | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   IconData _getTypeIcon(ContainerType type) { | ||||
|     switch (type) { | ||||
|       case ContainerType.flightCase: | ||||
|         return Icons.work; | ||||
|       case ContainerType.pelicase: | ||||
|         return Icons.work_outline; | ||||
|       case ContainerType.bag: | ||||
|         return Icons.shopping_bag; | ||||
|       case ContainerType.openCrate: | ||||
|         return Icons.inventory_2; | ||||
|       case ContainerType.toolbox: | ||||
|         return Icons.handyman; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -94,6 +94,37 @@ class MainDrawer extends StatelessWidget { | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|               PermissionGate( | ||||
|                 requiredPermissions: const ['view_equipment'], | ||||
|                 child: ListTile( | ||||
|                   leading: const Icon(Icons.inventory), | ||||
|                   title: const Text('Gestion du Matériel'), | ||||
|                   selected: currentPage == '/equipment_management', | ||||
|                   selectedColor: AppColors.rouge, | ||||
|                   onTap: () { | ||||
|                     Navigator.pop(context); | ||||
|                     Navigator.pushReplacement( | ||||
|                       context, | ||||
|                       MaterialPageRoute( | ||||
|                           builder: (context) => | ||||
|                           const EquipmentManagementPage()), | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|               ), | ||||
|               PermissionGate( | ||||
|                 requiredPermissions: const ['view_equipment'], | ||||
|                 child: ListTile( | ||||
|                   leading: const Icon(Icons.inventory_2), | ||||
|                   title: const Text('Containers'), | ||||
|                   selected: currentPage == '/container_management', | ||||
|                   selectedColor: AppColors.rouge, | ||||
|                   onTap: () { | ||||
|                     Navigator.pop(context); | ||||
|                     Navigator.pushNamed(context, '/container_management'); | ||||
|                   }, | ||||
|                 ), | ||||
|               ), | ||||
|               ExpansionTileTheme( | ||||
|                 data: const ExpansionTileThemeData( | ||||
|                   iconColor: AppColors.noir, | ||||
| @@ -152,24 +183,6 @@ class MainDrawer extends StatelessWidget { | ||||
|                         }, | ||||
|                       ), | ||||
|                     ), | ||||
|                     PermissionGate( | ||||
|                       requiredPermissions: const ['view_equipment'], | ||||
|                       child: ListTile( | ||||
|                         leading: const Icon(Icons.inventory), | ||||
|                         title: const Text('Gestion du Matériel'), | ||||
|                         selected: currentPage == '/equipment_management', | ||||
|                         selectedColor: AppColors.rouge, | ||||
|                         onTap: () { | ||||
|                           Navigator.pop(context); | ||||
|                           Navigator.pushReplacement( | ||||
|                             context, | ||||
|                             MaterialPageRoute( | ||||
|                                 builder: (context) => | ||||
|                                     const EquipmentManagementPage()), | ||||
|                           ); | ||||
|                         }, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 ElPoyo
					ElPoyo