diff --git a/em2rp/.github/copilot-instructions.md b/em2rp/.github/copilot-instructions.md new file mode 100644 index 0000000..f441068 --- /dev/null +++ b/em2rp/.github/copilot-instructions.md @@ -0,0 +1,6 @@ +CLEAN CODE très important: Toujours écrire du code propre, lisible et bien structuré. Utiliser des noms de variables et de fonctions explicites, éviter les répétitions inutiles et suivre les meilleures pratiques de codage. +Penser a créer des fonctions réutilisables pour éviter la duplication de code. +Verifier la présence de composant existants ou librairie existante avant de créer du code maison. Reutiliser le plus possible le code. Ne pas héister a analyser fréquemment la codebase existante. +Créer des fichiers séparés pour chaque composant, classe ou module afin de faciliter la maintenance et la réutilisation. Il faut eviter de dépasser 600 lignes par fichier. +Si quelque chose n'est pas clair, poser des questions pour clarifier les exigences avant de commencer à coder. +Ne pas générer de fichier résumant le code généré. diff --git a/em2rp/lib/models/container_model.dart b/em2rp/lib/models/container_model.dart index 177dac8..eba9307 100644 --- a/em2rp/lib/models/container_model.dart +++ b/em2rp/lib/models/container_model.dart @@ -83,13 +83,13 @@ extension ContainerTypeExtension on ContainerType { case ContainerType.flightCase: return Icons.work; case ContainerType.pelicase: - return Icons.inventory_2; + return Icons.work_outline; case ContainerType.bag: return Icons.shopping_bag; case ContainerType.openCrate: - return Icons.inventory; + return Icons.inventory_2; case ContainerType.toolbox: - return Icons.build_circle; + return Icons.home_repair_service; } } diff --git a/em2rp/lib/services/container_service.dart b/em2rp/lib/services/container_service.dart index 15b34e3..fab4b08 100644 --- a/em2rp/lib/services/container_service.dart +++ b/em2rp/lib/services/container_service.dart @@ -183,7 +183,7 @@ class ContainerService { 'success': true, 'message': 'Équipement ajouté avec succès', 'warnings': otherContainers.isNotEmpty - ? 'Attention : cet équipement est également dans les containers suivants : ${otherContainers.join(", ")}' + ? 'Attention : cet équipement est également dans les boites suivants : ${otherContainers.join(", ")}' : null, }; } catch (e) { diff --git a/em2rp/lib/utils/dialog_utils.dart b/em2rp/lib/utils/dialog_utils.dart new file mode 100644 index 0000000..d83ac0d --- /dev/null +++ b/em2rp/lib/utils/dialog_utils.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +/// Utilitaires pour afficher des dialogues de confirmation +class DialogUtils { + /// Affiche un dialogue de confirmation de suppression + static Future showDeleteConfirmation({ + required BuildContext context, + required String title, + required String message, + String confirmButtonText = 'Supprimer', + String cancelButtonText = 'Annuler', + }) async { + final result = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(cancelButtonText), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + child: Text(confirmButtonText), + ), + ], + ), + ); + return result ?? false; + } + + /// Affiche un dialogue de confirmation générique + static Future showConfirmation({ + required BuildContext context, + required String title, + required String message, + String confirmButtonText = 'Confirmer', + String cancelButtonText = 'Annuler', + Color? confirmButtonColor, + }) async { + final result = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(cancelButtonText), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: confirmButtonColor != null + ? ElevatedButton.styleFrom( + backgroundColor: confirmButtonColor, + foregroundColor: Colors.white, + ) + : null, + child: Text(confirmButtonText), + ), + ], + ), + ); + return result ?? false; + } +} + diff --git a/em2rp/lib/views/container_detail_page.dart b/em2rp/lib/views/container_detail_page.dart index 8a89950..de5a640 100644 --- a/em2rp/lib/views/container_detail_page.dart +++ b/em2rp/lib/views/container_detail_page.dart @@ -68,7 +68,7 @@ class _ContainerDetailPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Détails du Container'), + title: const Text('Détails de la boite'), backgroundColor: AppColors.rouge, foregroundColor: Colors.white, actions: [ @@ -131,11 +131,7 @@ class _ContainerDetailPageState extends State { children: [ Row( children: [ - Icon( - _getTypeIcon(_container.type), - size: 60, - color: AppColors.rouge, - ), + _container.type.getIcon(size:60, color:AppColors.rouge), const SizedBox(width: 20), Expanded( child: Column( @@ -580,22 +576,6 @@ class _ContainerDetailPageState extends State { 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: diff --git a/em2rp/lib/views/container_form_page.dart b/em2rp/lib/views/container_form_page.dart index 7b46ad7..4335f74 100644 --- a/em2rp/lib/views/container_form_page.dart +++ b/em2rp/lib/views/container_form_page.dart @@ -90,7 +90,7 @@ class _ContainerFormPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(_isEditing ? 'Modifier Container' : 'Nouveau Container'), + title: Text(_isEditing ? 'Modifier boite' : 'Nouvelle boite'), backgroundColor: AppColors.rouge, foregroundColor: Colors.white, ), @@ -636,6 +636,14 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { final TextEditingController _searchController = TextEditingController(); EquipmentCategory? _filterCategory; String _searchQuery = ''; + late Set _tempSelectedIds; + + @override + void initState() { + super.initState(); + // Créer une copie temporaire des IDs sélectionnés + _tempSelectedIds = Set.from(widget.selectedIds); + } @override void dispose() { @@ -759,7 +767,7 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { const Icon(Icons.check_circle, color: AppColors.rouge), const SizedBox(width: 8), Text( - '${widget.selectedIds.length} équipement(s) sélectionné(s)', + '${_tempSelectedIds.length} équipement(s) sélectionné(s)', style: const TextStyle(fontWeight: FontWeight.bold), ), ], @@ -807,16 +815,16 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { itemCount: equipment.length, itemBuilder: (context, index) { final item = equipment[index]; - final isSelected = widget.selectedIds.contains(item.id); + final isSelected = _tempSelectedIds.contains(item.id); return CheckboxListTile( value: isSelected, onChanged: (selected) { setState(() { if (selected == true) { - widget.selectedIds.add(item.id); + _tempSelectedIds.add(item.id); } else { - widget.selectedIds.remove(item.id); + _tempSelectedIds.remove(item.id); } }); }, @@ -857,12 +865,20 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( - onPressed: () => Navigator.pop(context), + onPressed: () { + // Ne pas appliquer les modifications, juste fermer + Navigator.pop(context); + }, child: const Text('Annuler'), ), const SizedBox(width: 16), ElevatedButton( - onPressed: () => Navigator.pop(context), + onPressed: () { + // Appliquer les modifications à l'original + widget.selectedIds.clear(); + widget.selectedIds.addAll(_tempSelectedIds); + Navigator.pop(context); + }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.rouge, ), @@ -920,5 +936,4 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { return Icons.category; } } -} - +} \ No newline at end of file diff --git a/em2rp/lib/views/equipment_detail_page.dart b/em2rp/lib/views/equipment_detail_page.dart index 69e4410..0a7e435 100644 --- a/em2rp/lib/views/equipment_detail_page.dart +++ b/em2rp/lib/views/equipment_detail_page.dart @@ -10,7 +10,14 @@ import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/views/widgets/nav/custom_app_bar.dart'; import 'package:em2rp/views/equipment_form_page.dart'; import 'package:em2rp/views/widgets/equipment/equipment_parent_containers.dart'; -import 'package:intl/intl.dart'; +import 'package:em2rp/views/widgets/equipment/equipment_referencing_containers.dart'; +import 'package:em2rp/views/widgets/equipment/equipment_header_section.dart'; +import 'package:em2rp/views/widgets/equipment/equipment_main_info_section.dart'; +import 'package:em2rp/views/widgets/equipment/equipment_notes_section.dart'; +import 'package:em2rp/views/widgets/equipment/equipment_associated_events_section.dart'; +import 'package:em2rp/views/widgets/equipment/equipment_price_section.dart'; +import 'package:em2rp/views/widgets/equipment/equipment_maintenance_history_section.dart'; +import 'package:em2rp/views/widgets/equipment/equipment_dates_section.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:printing/printing.dart'; @@ -49,9 +56,11 @@ class _EquipmentDetailPageState extends State { } + @override Widget build(BuildContext context) { - final isMobile = MediaQuery.of(context).size.width < 800; + final screenWidth = MediaQuery.of(context).size.width; + final isDesktop = screenWidth >= 1200; final userProvider = Provider.of(context); final hasManagePermission = userProvider.hasPermission('manage_equipment'); @@ -79,539 +88,102 @@ class _EquipmentDetailPageState extends State { ], ), body: SingleChildScrollView( - padding: EdgeInsets.all(isMobile ? 16 : 24), + padding: EdgeInsets.all(screenWidth < 800 ? 16 : 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildHeader(), + // 1. Titre de la machine + EquipmentHeaderSection(equipment: widget.equipment), const SizedBox(height: 24), - _buildMainInfoSection(), + + // 2. Info principale + EquipmentMainInfoSection(equipment: widget.equipment), const SizedBox(height: 24), - if (hasManagePermission) ...[ - _buildPriceSection(), - const SizedBox(height: 24), - ], - if (widget.equipment.category == EquipmentCategory.consumable || - widget.equipment.category == EquipmentCategory.cable) ...[ - _buildQuantitySection(), + + // 3. Notes + if (widget.equipment.notes != null && widget.equipment.notes!.isNotEmpty) ...[ + EquipmentNotesSection(notes: widget.equipment.notes!), const SizedBox(height: 24), ], + + // 4. Événements associés + const EquipmentAssociatedEventsSection(), + const SizedBox(height: 24), + + // 5-7. Prix, Historique des maintenances, Dates en layout responsive + if (isDesktop) + _buildDesktopTwoColumnLayout(hasManagePermission) + else + _buildMobileLayout(hasManagePermission), + + const SizedBox(height: 24), + + // Containers parents (si applicable) if (widget.equipment.parentBoxIds.isNotEmpty) ...[ EquipmentParentContainers( parentBoxIds: widget.equipment.parentBoxIds, ), const SizedBox(height: 24), ], - _buildDatesSection(), - const SizedBox(height: 24), - if (widget.equipment.notes != null && widget.equipment.notes!.isNotEmpty) ...[ - _buildNotesSection(), + + // Containers associés + EquipmentReferencingContainers( + equipmentId: widget.equipment.id, + ), + ], + ), + ), + ); + } + + /// Layout 2 colonnes pour desktop + Widget _buildDesktopTwoColumnLayout(bool hasManagePermission) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Colonne gauche + Expanded( + child: Column( + children: [ + // Prix + EquipmentPriceSection(equipment: widget.equipment), const SizedBox(height: 24), - ], - _buildMaintenanceHistorySection(hasManagePermission), - const SizedBox(height: 24), - _buildAssociatedEventsSection(), - ], - ), - ), - ); - } - - Widget _buildHeader() { - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [AppColors.rouge, AppColors.rouge.withValues(alpha: 0.8)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: AppColors.rouge.withValues(alpha: 0.3), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - CircleAvatar( - backgroundColor: Colors.white, - radius: 30, - child: widget.equipment.category.getIcon( - size: 32, - color: AppColors.rouge, - ), + // Historique des maintenances + EquipmentMaintenanceHistorySection( + maintenances: _maintenances, + isLoading: _isLoadingMaintenances, + hasManagePermission: hasManagePermission, ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.equipment.id, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - const SizedBox(height: 4), - Text( - '${widget.equipment.brand ?? ''} ${widget.equipment.model ?? ''}'.trim().isNotEmpty - ? '${widget.equipment.brand ?? ''} ${widget.equipment.model ?? ''}'.trim() - : 'Marque/Modèle non défini', - style: const TextStyle( - fontSize: 16, - color: Colors.white70, - ), - ), - ], - ), - ), - if (widget.equipment.category != EquipmentCategory.consumable && - widget.equipment.category != EquipmentCategory.cable) - _buildStatusBadge(), ], ), - ], - ), - ); - } - - Widget _buildStatusBadge() { - final status = widget.equipment.status; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: status.color, - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 8), - Text( - status.label, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: status.color, - ), - ), - ], - ), - ); - } - - Widget _buildMainInfoSection() { - return Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon(Icons.info_outline, color: AppColors.rouge), - const SizedBox(width: 8), - Text( - 'Informations principales', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const Divider(height: 24), - _buildInfoRow('Catégorie', widget.equipment.category.label), - if (widget.equipment.brand != null && widget.equipment.brand!.isNotEmpty) - _buildInfoRow('Marque', widget.equipment.brand!), - if (widget.equipment.model != null && widget.equipment.model!.isNotEmpty) - _buildInfoRow('Modèle', widget.equipment.model!), - if (widget.equipment.category != EquipmentCategory.consumable && - widget.equipment.category != EquipmentCategory.cable) - _buildInfoRow('Statut', widget.equipment.status.label), - ], ), - ), - ); - } - - Widget _buildPriceSection() { - final hasPrices = widget.equipment.purchasePrice != null || widget.equipment.rentalPrice != null; - - if (!hasPrices) 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.euro, color: AppColors.rouge), - const SizedBox(width: 8), - Text( - 'Prix', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const Divider(height: 24), - if (widget.equipment.purchasePrice != null) - _buildInfoRow( - 'Prix d\'achat', - '${widget.equipment.purchasePrice!.toStringAsFixed(2)} €', - ), - if (widget.equipment.rentalPrice != null) - _buildInfoRow( - 'Prix de location', - '${widget.equipment.rentalPrice!.toStringAsFixed(2)} €/jour', - ), - ], + const SizedBox(width: 24), + // Colonne droite + Expanded( + child: EquipmentDatesSection(equipment: widget.equipment), ), - ), + ], ); } - Widget _buildQuantitySection() { - final availableQty = widget.equipment.availableQuantity ?? 0; - final totalQty = widget.equipment.totalQuantity ?? 0; - final criticalThreshold = widget.equipment.criticalThreshold ?? 0; - final isCritical = criticalThreshold > 0 && availableQty <= criticalThreshold; - - return Card( - elevation: 2, - color: isCritical ? Colors.red.shade50 : null, - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - isCritical ? Icons.warning : Icons.inventory, - color: isCritical ? Colors.red : AppColors.rouge, - ), - const SizedBox(width: 8), - Text( - 'Quantités', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - color: isCritical ? Colors.red : null, - ), - ), - if (isCritical) ...[ - const SizedBox(width: 12), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(12), - ), - child: const Text( - 'STOCK CRITIQUE', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ), - ], - ], - ), - const Divider(height: 24), - _buildInfoRow( - 'Quantité disponible', - availableQty.toString(), - valueColor: isCritical ? Colors.red : null, - valueWeight: isCritical ? FontWeight.bold : null, - ), - _buildInfoRow('Quantité totale', totalQty.toString()), - if (criticalThreshold > 0) - _buildInfoRow('Seuil critique', criticalThreshold.toString()), - ], + /// Layout simple colonne pour mobile/tablette + Widget _buildMobileLayout(bool hasManagePermission) { + return Column( + children: [ + EquipmentPriceSection(equipment: widget.equipment), + const SizedBox(height: 24), + EquipmentMaintenanceHistorySection( + maintenances: _maintenances, + isLoading: _isLoadingMaintenances, + hasManagePermission: hasManagePermission, ), - ), + const SizedBox(height: 24), + EquipmentDatesSection(equipment: widget.equipment), + ], ); } - Widget _buildDatesSection() { - return Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon(Icons.calendar_today, color: AppColors.rouge), - const SizedBox(width: 8), - Text( - 'Dates', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const Divider(height: 24), - if (widget.equipment.purchaseDate != null) - _buildInfoRow( - 'Date d\'achat', - DateFormat('dd/MM/yyyy').format(widget.equipment.purchaseDate!), - ), - if (widget.equipment.lastMaintenanceDate != null) - _buildInfoRow( - 'Dernière maintenance', - DateFormat('dd/MM/yyyy').format(widget.equipment.lastMaintenanceDate!), - ), - if (widget.equipment.nextMaintenanceDate != null) - _buildInfoRow( - 'Prochaine maintenance', - DateFormat('dd/MM/yyyy').format(widget.equipment.nextMaintenanceDate!), - valueColor: widget.equipment.nextMaintenanceDate!.isBefore(DateTime.now()) - ? Colors.red - : null, - ), - _buildInfoRow( - 'Créé le', - DateFormat('dd/MM/yyyy à HH:mm').format(widget.equipment.createdAt), - ), - _buildInfoRow( - 'Modifié le', - DateFormat('dd/MM/yyyy à HH:mm').format(widget.equipment.updatedAt), - ), - ], - ), - ), - ); - } - - Widget _buildNotesSection() { - return Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon(Icons.notes, color: AppColors.rouge), - const SizedBox(width: 8), - Text( - 'Notes', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const Divider(height: 24), - Text( - widget.equipment.notes!, - style: const TextStyle(fontSize: 14), - ), - ], - ), - ), - ); - } - - Widget _buildMaintenanceHistorySection(bool hasManagePermission) { - return Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon(Icons.build, color: AppColors.rouge), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Historique des maintenances', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - const Divider(height: 24), - if (_isLoadingMaintenances) - const Center(child: CircularProgressIndicator()) - else if (_maintenances.isEmpty) - const Padding( - padding: EdgeInsets.all(16.0), - child: Center( - child: Text( - 'Aucune maintenance enregistrée', - style: TextStyle(color: Colors.grey), - ), - ), - ) - else - ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: _maintenances.length, - separatorBuilder: (context, index) => const Divider(), - itemBuilder: (context, index) { - final maintenance = _maintenances[index]; - return _buildMaintenanceItem(maintenance, hasManagePermission); - }, - ), - ], - ), - ), - ); - } - - Widget _buildMaintenanceItem(MaintenanceModel maintenance, bool showCost) { - final isCompleted = maintenance.completedDate != null; - final typeInfo = _getMaintenanceTypeInfo(maintenance.type); - - return ListTile( - contentPadding: const EdgeInsets.symmetric(vertical: 8), - leading: CircleAvatar( - backgroundColor: isCompleted ? Colors.green.withValues(alpha: 0.2) : Colors.orange.withValues(alpha: 0.2), - child: Icon( - isCompleted ? Icons.check_circle : Icons.schedule, - color: isCompleted ? Colors.green : Colors.orange, - ), - ), - title: Text( - maintenance.name, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 4), - Row( - children: [ - Icon(typeInfo.$2, size: 16, color: Colors.grey[600]), - const SizedBox(width: 4), - Text(typeInfo.$1, style: TextStyle(color: Colors.grey[600], fontSize: 12)), - ], - ), - const SizedBox(height: 4), - Text( - isCompleted - ? 'Effectuée le ${DateFormat('dd/MM/yyyy').format(maintenance.completedDate!)}' - : 'Planifiée le ${DateFormat('dd/MM/yyyy').format(maintenance.scheduledDate)}', - style: TextStyle(fontSize: 12, color: Colors.grey[700]), - ), - if (showCost && maintenance.cost != null) ...[ - const SizedBox(height: 4), - Text( - 'Coût: ${maintenance.cost!.toStringAsFixed(2)} €', - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), - ), - ], - ], - ), - ); - } - - Widget _buildAssociatedEventsSection() { - return Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon(Icons.event, color: AppColors.rouge), - const SizedBox(width: 8), - Text( - 'Événements associés', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const Divider(height: 24), - const Padding( - padding: EdgeInsets.all(16.0), - child: Center( - child: Text( - 'Fonctionnalité à implémenter', - style: TextStyle(color: Colors.grey, fontStyle: FontStyle.italic), - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildInfoRow( - String label, - String value, { - Color? valueColor, - FontWeight? valueWeight, - }) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 180, - child: Text( - label, - style: TextStyle( - fontWeight: FontWeight.w500, - color: Colors.grey[700], - ), - ), - ), - Expanded( - child: Text( - value, - style: TextStyle( - color: valueColor, - fontWeight: valueWeight ?? FontWeight.w600, - ), - ), - ), - ], - ), - ); - } - void _showQRCode() { showDialog( context: context, @@ -805,16 +377,4 @@ class _EquipmentDetailPageState extends State { ), ); } - - (String, IconData) _getMaintenanceTypeInfo(MaintenanceType type) { - switch (type) { - case MaintenanceType.preventive: - return ('Préventive', Icons.schedule); - case MaintenanceType.corrective: - return ('Corrective', Icons.build); - case MaintenanceType.inspection: - return ('Inspection', Icons.search); - } - } -} - +} \ No newline at end of file diff --git a/em2rp/lib/views/equipment_form_page.dart b/em2rp/lib/views/equipment_form_page.dart index 3adf584..d1ad61c 100644 --- a/em2rp/lib/views/equipment_form_page.dart +++ b/em2rp/lib/views/equipment_form_page.dart @@ -169,7 +169,7 @@ class _EquipmentFormPageState extends State { if (value != null && value.isNotEmpty) { // Empêcher les ID commençant par BOX_ (réservé aux containers) if (value.toUpperCase().startsWith('BOX_')) { - return 'Les ID commençant par BOX_ sont réservés aux containers'; + return 'Les ID commençant par BOX_ sont réservés aux boites'; } } return null; diff --git a/em2rp/lib/views/widgets/common/custom_filter_chip.dart b/em2rp/lib/views/widgets/common/custom_filter_chip.dart new file mode 100644 index 0000000..a728788 --- /dev/null +++ b/em2rp/lib/views/widgets/common/custom_filter_chip.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/utils/colors.dart'; + +/// Widget réutilisable pour créer des chips de filtre avec icône +class CustomFilterChip extends StatelessWidget { + final String label; + final Widget? icon; + final bool isSelected; + final VoidCallback onSelected; + final Color selectedColor; + final Color unselectedTextColor; + + const CustomFilterChip({ + super.key, + required this.label, + this.icon, + required this.isSelected, + required this.onSelected, + this.selectedColor = AppColors.rouge, + this.unselectedTextColor = AppColors.rouge, + }); + + @override + Widget build(BuildContext context) { + final color = isSelected ? Colors.white : unselectedTextColor; + + return ChoiceChip( + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[ + icon!, + const SizedBox(width: 8), + ], + Text(label), + ], + ), + selected: isSelected, + onSelected: (selected) { + if (selected) { + onSelected(); + } + }, + selectedColor: selectedColor, + labelStyle: TextStyle( + color: color, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + ); + } +} + diff --git a/em2rp/lib/views/widgets/common/empty_state.dart b/em2rp/lib/views/widgets/common/empty_state.dart new file mode 100644 index 0000000..61daa82 --- /dev/null +++ b/em2rp/lib/views/widgets/common/empty_state.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +/// Widget réutilisable pour afficher un état vide (aucun élément) +class EmptyState extends StatelessWidget { + final IconData icon; + final String title; + final String? subtitle; + final double iconSize; + + const EmptyState({ + super.key, + required this.icon, + required this.title, + this.subtitle, + this.iconSize = 64, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: iconSize, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + title, + style: TextStyle(fontSize: 18, color: Colors.grey[600]), + ), + if (subtitle != null) ...[ + const SizedBox(height: 8), + Text( + subtitle!, + style: TextStyle(fontSize: 14, color: Colors.grey[500]), + ), + ], + ], + ), + ); + } +} + diff --git a/em2rp/lib/views/widgets/common/info_chip.dart b/em2rp/lib/views/widgets/common/info_chip.dart new file mode 100644 index 0000000..33db183 --- /dev/null +++ b/em2rp/lib/views/widgets/common/info_chip.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +/// Widget réutilisable pour afficher une puce d'information avec icône et texte +class InfoChip extends StatelessWidget { + final String label; + final Widget icon; + final Color? backgroundColor; + final Color? textColor; + final double fontSize; + final EdgeInsets padding; + + const InfoChip({ + super.key, + required this.label, + required this.icon, + this.backgroundColor, + this.textColor, + this.fontSize = 12, + this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: padding, + decoration: BoxDecoration( + color: backgroundColor ?? Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + icon, + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + fontSize: fontSize, + color: textColor ?? Colors.grey.shade700, + ), + ), + ], + ), + ); + } +} + diff --git a/em2rp/lib/views/widgets/common/search_bar_widget.dart b/em2rp/lib/views/widgets/common/search_bar_widget.dart new file mode 100644 index 0000000..c5591ce --- /dev/null +++ b/em2rp/lib/views/widgets/common/search_bar_widget.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/utils/colors.dart'; + +/// Widget réutilisable pour une barre de recherche +class SearchBarWidget extends StatelessWidget { + final TextEditingController controller; + final String hintText; + final ValueChanged onChanged; + final VoidCallback? onClear; + final EdgeInsets padding; + final bool withShadow; + + const SearchBarWidget({ + super.key, + required this.controller, + required this.hintText, + required this.onChanged, + this.onClear, + this.padding = const EdgeInsets.all(16), + this.withShadow = true, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: padding, + decoration: withShadow + ? BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ) + : null, + child: TextField( + controller: controller, + decoration: InputDecoration( + hintText: hintText, + prefixIcon: const Icon(Icons.search, color: AppColors.rouge), + suffixIcon: controller.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + controller.clear(); + if (onClear != null) { + onClear!(); + } else { + onChanged(''); + } + }, + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onChanged: onChanged, + ), + ); + } +} + diff --git a/em2rp/lib/views/widgets/common/selection_app_bar.dart b/em2rp/lib/views/widgets/common/selection_app_bar.dart new file mode 100644 index 0000000..7adac47 --- /dev/null +++ b/em2rp/lib/views/widgets/common/selection_app_bar.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/utils/colors.dart'; + +/// Widget réutilisable pour un AppBar en mode sélection +class SelectionAppBar extends StatelessWidget implements PreferredSizeWidget { + final int selectedCount; + final VoidCallback onClose; + final VoidCallback? onDelete; + final VoidCallback? onGenerateQR; + final List? additionalActions; + + const SelectionAppBar({ + super.key, + required this.selectedCount, + required this.onClose, + this.onDelete, + this.onGenerateQR, + this.additionalActions, + }); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + Widget build(BuildContext context) { + final hasSelection = selectedCount > 0; + + return AppBar( + backgroundColor: AppColors.rouge, + leading: IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: onClose, + ), + title: Text( + '$selectedCount sélectionné(s)', + style: const TextStyle(color: Colors.white), + ), + actions: [ + if (hasSelection) ...[ + if (onGenerateQR != null) + IconButton( + icon: const Icon(Icons.qr_code, color: Colors.white), + tooltip: 'Générer QR Codes', + onPressed: onGenerateQR, + ), + if (onDelete != null) + IconButton( + icon: const Icon(Icons.delete, color: Colors.white), + tooltip: 'Supprimer', + onPressed: onDelete, + ), + if (additionalActions != null) ...additionalActions!, + ], + ], + ); + } +} + diff --git a/em2rp/lib/views/widgets/common/status_badge.dart b/em2rp/lib/views/widgets/common/status_badge.dart new file mode 100644 index 0000000..ed41ce7 --- /dev/null +++ b/em2rp/lib/views/widgets/common/status_badge.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/models/equipment_model.dart'; + +/// Widget réutilisable pour afficher un badge de statut d'équipement ou container +class StatusBadge extends StatelessWidget { + final EquipmentStatus status; + final double fontSize; + final EdgeInsets padding; + + const StatusBadge({ + super.key, + required this.status, + this.fontSize = 12, + this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: padding, + decoration: BoxDecoration( + color: status.color.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: status.color), + ), + child: Text( + status.label, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + color: status.color, + ), + ), + ); + } +} + diff --git a/em2rp/lib/views/widgets/equipment/equipment_associated_events_section.dart b/em2rp/lib/views/widgets/equipment/equipment_associated_events_section.dart new file mode 100644 index 0000000..54fbf28 --- /dev/null +++ b/em2rp/lib/views/widgets/equipment/equipment_associated_events_section.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/utils/colors.dart'; + +/// Widget pour afficher les événements associés +class EquipmentAssociatedEventsSection extends StatelessWidget { + const EquipmentAssociatedEventsSection({super.key}); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.event, color: AppColors.rouge), + const SizedBox(width: 8), + Text( + 'Événements associés', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Divider(height: 24), + const Padding( + padding: EdgeInsets.all(16.0), + child: Center( + child: Text( + 'Fonctionnalité à implémenter', + style: TextStyle(color: Colors.grey, fontStyle: FontStyle.italic), + ), + ), + ), + ], + ), + ), + ); + } +} + diff --git a/em2rp/lib/views/widgets/equipment/equipment_dates_section.dart b/em2rp/lib/views/widgets/equipment/equipment_dates_section.dart new file mode 100644 index 0000000..f5cd109 --- /dev/null +++ b/em2rp/lib/views/widgets/equipment/equipment_dates_section.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:em2rp/models/equipment_model.dart'; +import 'package:em2rp/utils/colors.dart'; + +/// Widget pour afficher les dates +class EquipmentDatesSection extends StatelessWidget { + final EquipmentModel equipment; + + const EquipmentDatesSection({ + super.key, + required this.equipment, + }); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.calendar_today, color: AppColors.rouge), + const SizedBox(width: 8), + Text( + 'Dates', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Divider(height: 24), + if (equipment.purchaseDate != null) + _buildInfoRow( + 'Date d\'achat', + DateFormat('dd/MM/yyyy').format(equipment.purchaseDate!), + ), + if (equipment.lastMaintenanceDate != null) + _buildInfoRow( + 'Dernière maintenance', + DateFormat('dd/MM/yyyy').format(equipment.lastMaintenanceDate!), + ), + if (equipment.nextMaintenanceDate != null) + _buildInfoRow( + 'Prochaine maintenance', + DateFormat('dd/MM/yyyy').format(equipment.nextMaintenanceDate!), + valueColor: equipment.nextMaintenanceDate!.isBefore(DateTime.now()) + ? Colors.red + : null, + ), + _buildInfoRow( + 'Créé le', + DateFormat('dd/MM/yyyy à HH:mm').format(equipment.createdAt), + ), + _buildInfoRow( + 'Modifié le', + DateFormat('dd/MM/yyyy à HH:mm').format(equipment.updatedAt), + ), + ], + ), + ), + ); + } + + Widget _buildInfoRow( + String label, + String value, { + Color? valueColor, + }) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 180, + child: Text( + label, + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.grey[700], + ), + ), + ), + Expanded( + child: Text( + value, + style: TextStyle( + color: valueColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } +} + diff --git a/em2rp/lib/views/widgets/equipment/equipment_header_section.dart b/em2rp/lib/views/widgets/equipment/equipment_header_section.dart new file mode 100644 index 0000000..2b487f5 --- /dev/null +++ b/em2rp/lib/views/widgets/equipment/equipment_header_section.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/models/equipment_model.dart'; +import 'package:em2rp/utils/colors.dart'; + +/// Widget pour afficher l'en-tête (titre et infos principales) +class EquipmentHeaderSection extends StatelessWidget { + final EquipmentModel equipment; + + const EquipmentHeaderSection({ + super.key, + required this.equipment, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [AppColors.rouge, AppColors.rouge.withValues(alpha: 0.8)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppColors.rouge.withValues(alpha: 0.3), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar( + backgroundColor: Colors.white, + radius: 30, + child: equipment.category.getIcon( + size: 32, + color: AppColors.rouge, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + equipment.id, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 4), + Text( + '${equipment.brand ?? ''} ${equipment.model ?? ''}'.trim().isNotEmpty + ? '${equipment.brand ?? ''} ${equipment.model ?? ''}'.trim() + : 'Marque/Modèle non défini', + style: const TextStyle( + fontSize: 16, + color: Colors.white70, + ), + ), + ], + ), + ), + if (equipment.category != EquipmentCategory.consumable && + equipment.category != EquipmentCategory.cable) + _buildStatusBadge(), + ], + ), + ], + ), + ); + } + + Widget _buildStatusBadge() { + final status = equipment.status; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: status.color, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Text( + status.label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: status.color, + ), + ), + ], + ), + ); + } +} + diff --git a/em2rp/lib/views/widgets/equipment/equipment_main_info_section.dart b/em2rp/lib/views/widgets/equipment/equipment_main_info_section.dart new file mode 100644 index 0000000..f12dc3c --- /dev/null +++ b/em2rp/lib/views/widgets/equipment/equipment_main_info_section.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/models/equipment_model.dart'; +import 'package:em2rp/utils/colors.dart'; + +/// Widget pour afficher les informations principales +class EquipmentMainInfoSection extends StatelessWidget { + final EquipmentModel equipment; + + const EquipmentMainInfoSection({ + super.key, + required this.equipment, + }); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.info_outline, color: AppColors.rouge), + const SizedBox(width: 8), + Text( + 'Informations principales', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Divider(height: 24), + _buildInfoRow('Catégorie', equipment.category.label), + if (equipment.brand != null && equipment.brand!.isNotEmpty) + _buildInfoRow('Marque', equipment.brand!), + if (equipment.model != null && equipment.model!.isNotEmpty) + _buildInfoRow('Modèle', equipment.model!), + if (equipment.category != EquipmentCategory.consumable && + equipment.category != EquipmentCategory.cable) + _buildInfoRow('Statut', equipment.status.label), + ], + ), + ), + ); + } + + Widget _buildInfoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 120, + child: Text( + label, + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.grey[700], + ), + ), + ), + Expanded( + child: Text( + value, + style: const TextStyle( + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } +} + diff --git a/em2rp/lib/views/widgets/equipment/equipment_maintenance_history_section.dart b/em2rp/lib/views/widgets/equipment/equipment_maintenance_history_section.dart new file mode 100644 index 0000000..1501e76 --- /dev/null +++ b/em2rp/lib/views/widgets/equipment/equipment_maintenance_history_section.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/models/maintenance_model.dart'; +import 'package:intl/intl.dart'; +import 'package:em2rp/utils/colors.dart'; + +/// Widget pour afficher l'historique des maintenances +class EquipmentMaintenanceHistorySection extends StatelessWidget { + final List maintenances; + final bool isLoading; + final bool hasManagePermission; + + const EquipmentMaintenanceHistorySection({ + super.key, + required this.maintenances, + required this.isLoading, + required this.hasManagePermission, + }); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.build, color: AppColors.rouge), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Historique des maintenances', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const Divider(height: 24), + if (isLoading) + const Center(child: CircularProgressIndicator()) + else if (maintenances.isEmpty) + const Padding( + padding: EdgeInsets.all(16.0), + child: Center( + child: Text( + 'Aucune maintenance enregistrée', + style: TextStyle(color: Colors.grey), + ), + ), + ) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: maintenances.length, + separatorBuilder: (context, index) => const Divider(), + itemBuilder: (context, index) { + return _buildMaintenanceItem(maintenances[index], hasManagePermission); + }, + ), + ], + ), + ), + ); + } + + Widget _buildMaintenanceItem(MaintenanceModel maintenance, bool showCost) { + final isCompleted = maintenance.completedDate != null; + final typeInfo = _getMaintenanceTypeInfo(maintenance.type); + + return ListTile( + contentPadding: const EdgeInsets.symmetric(vertical: 8), + leading: CircleAvatar( + backgroundColor: isCompleted ? Colors.green.withValues(alpha: 0.2) : Colors.orange.withValues(alpha: 0.2), + child: Icon( + isCompleted ? Icons.check_circle : Icons.schedule, + color: isCompleted ? Colors.green : Colors.orange, + ), + ), + title: Text( + maintenance.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Row( + children: [ + Icon(typeInfo.$2, size: 16, color: Colors.grey[600]), + const SizedBox(width: 4), + Text(typeInfo.$1, style: TextStyle(color: Colors.grey[600], fontSize: 12)), + ], + ), + const SizedBox(height: 4), + Text( + isCompleted + ? 'Effectuée le ${DateFormat('dd/MM/yyyy').format(maintenance.completedDate!)}' + : 'Planifiée le ${DateFormat('dd/MM/yyyy').format(maintenance.scheduledDate)}', + style: TextStyle(fontSize: 12, color: Colors.grey[700]), + ), + if (showCost && maintenance.cost != null) ...[ + const SizedBox(height: 4), + Text( + 'Coût: ${maintenance.cost!.toStringAsFixed(2)} €', + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + ), + ], + ], + ), + ); + } + + (String, IconData) _getMaintenanceTypeInfo(MaintenanceType type) { + switch (type) { + case MaintenanceType.preventive: + return ('Préventive', Icons.schedule); + case MaintenanceType.corrective: + return ('Corrective', Icons.build); + case MaintenanceType.inspection: + return ('Inspection', Icons.search); + } + } +} + diff --git a/em2rp/lib/views/widgets/equipment/equipment_notes_section.dart b/em2rp/lib/views/widgets/equipment/equipment_notes_section.dart new file mode 100644 index 0000000..769580b --- /dev/null +++ b/em2rp/lib/views/widgets/equipment/equipment_notes_section.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/utils/colors.dart'; + +/// Widget pour afficher les notes +class EquipmentNotesSection extends StatelessWidget { + final String notes; + + const EquipmentNotesSection({ + super.key, + required this.notes, + }); + + @override + Widget build(BuildContext context) { + if (notes.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.notes, color: AppColors.rouge), + const SizedBox(width: 8), + Text( + 'Notes', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Divider(height: 24), + Text( + notes, + style: const TextStyle(fontSize: 14), + ), + ], + ), + ), + ); + } +} + diff --git a/em2rp/lib/views/widgets/equipment/equipment_price_section.dart b/em2rp/lib/views/widgets/equipment/equipment_price_section.dart new file mode 100644 index 0000000..a3094a5 --- /dev/null +++ b/em2rp/lib/views/widgets/equipment/equipment_price_section.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/models/equipment_model.dart'; +import 'package:em2rp/utils/colors.dart'; + +/// Widget pour afficher les prix +class EquipmentPriceSection extends StatelessWidget { + final EquipmentModel equipment; + + const EquipmentPriceSection({ + super.key, + required this.equipment, + }); + + @override + Widget build(BuildContext context) { + final hasPrices = equipment.purchasePrice != null || equipment.rentalPrice != null; + + if (!hasPrices) 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.euro, color: AppColors.rouge), + const SizedBox(width: 8), + Text( + 'Prix', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Divider(height: 24), + if (equipment.purchasePrice != null) + _buildInfoRow( + 'Prix d\'achat', + '${equipment.purchasePrice!.toStringAsFixed(2)} €', + ), + if (equipment.rentalPrice != null) + _buildInfoRow( + 'Prix de location', + '${equipment.rentalPrice!.toStringAsFixed(2)} €/jour', + ), + ], + ), + ), + ); + } + + Widget _buildInfoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 120, + child: Text( + label, + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.grey[700], + ), + ), + ), + Expanded( + child: Text( + value, + style: const TextStyle( + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } +} + diff --git a/em2rp/lib/views/widgets/equipment/equipment_referencing_containers.dart b/em2rp/lib/views/widgets/equipment/equipment_referencing_containers.dart new file mode 100644 index 0000000..f580ae0 --- /dev/null +++ b/em2rp/lib/views/widgets/equipment/equipment_referencing_containers.dart @@ -0,0 +1,288 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/models/container_model.dart'; +import 'package:em2rp/models/equipment_model.dart'; +import 'package:em2rp/services/container_service.dart'; +import 'package:em2rp/utils/colors.dart'; +import 'package:em2rp/views/container_detail_page.dart'; + +/// Widget pour afficher les containers qui référencent un équipement +class EquipmentReferencingContainers extends StatefulWidget { + final String equipmentId; + + const EquipmentReferencingContainers({ + super.key, + required this.equipmentId, + }); + + @override + State createState() => _EquipmentReferencingContainersState(); +} + +class _EquipmentReferencingContainersState extends State { + final ContainerService _containerService = ContainerService(); + List _referencingContainers = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadReferencingContainers(); + } + + Future _loadReferencingContainers() async { + try { + final containers = await _containerService.findContainersWithEquipment(widget.equipmentId); + setState(() { + _referencingContainers = containers; + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + if (_referencingContainers.isEmpty && !_isLoading) { + 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), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Containers contenant cet équipement', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const Divider(height: 24), + if (_isLoading) + const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator(), + ), + ) + else if (_referencingContainers.isEmpty) + const Padding( + padding: EdgeInsets.all(16.0), + child: Center( + child: Text( + 'Cet équipement n\'est dans aucun container', + style: TextStyle(color: Colors.grey), + ), + ), + ) + else + _buildContainersGrid(), + ], + ), + ), + ); + } + + Widget _buildContainersGrid() { + final screenWidth = MediaQuery.of(context).size.width; + final isMobile = screenWidth < 800; + final isTablet = screenWidth < 1200; + + final crossAxisCount = isMobile ? 1 : (isTablet ? 2 : 3); + + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: 12, + mainAxisSpacing: 8, + childAspectRatio: 7.5, + ), + itemCount: _referencingContainers.length, + itemBuilder: (context, index) { + final container = _referencingContainers[index]; + return _buildContainerCard(container); + }, + ); + } + + Widget _buildContainerCard(ContainerModel container) { + return Card( + elevation: 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: InkWell( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ContainerDetailPage(container: container), + ), + ); + }, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Row( + children: [ + // Icône du type de container + container.type.getIcon(size: 28, color: AppColors.rouge), + const SizedBox(width: 10), + // Infos textuelles + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + container.id, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + height: 1.0, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + if (container.notes != null && container.notes!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + container.notes!, + style: TextStyle( + fontSize: 10, + color: Colors.grey[600], + height: 1.0, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ) + else + Text( + container.name, + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + height: 1.0, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ), + ), + const SizedBox(width: 8), + // Badges compacts + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _buildStatusBadge(_getStatusLabel(container.status), _getStatusColor(container.status)), + if (container.itemCount > 0) + Padding( + padding: const EdgeInsets.only(top: 2), + child: _buildCountBadge(container.itemCount), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildStatusBadge(String label, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: color.withValues(alpha: 0.4), width: 0.5), + ), + child: Text( + label, + style: TextStyle( + fontSize: 9, + color: color, + fontWeight: FontWeight.bold, + height: 1.0, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ); + } + + Widget _buildCountBadge(int count) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.withValues(alpha: 0.4), width: 0.5), + ), + child: Text( + '$count article${count > 1 ? 's' : ''}', + style: const TextStyle( + fontSize: 9, + color: Colors.green, + fontWeight: FontWeight.bold, + height: 1.0, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ); + } + + String _getStatusLabel(EquipmentStatus status) { + switch (status) { + case EquipmentStatus.available: + return 'Disponible'; + case EquipmentStatus.inUse: + return 'En prestation'; + case EquipmentStatus.rented: + return 'Loué'; + case EquipmentStatus.lost: + return 'Perdu'; + case EquipmentStatus.outOfService: + return 'HS'; + case EquipmentStatus.maintenance: + return 'En maintenance'; + } + } + + Color _getStatusColor(EquipmentStatus status) { + switch (status) { + case EquipmentStatus.available: + return Colors.green; + case EquipmentStatus.inUse: + return Colors.blue; + case EquipmentStatus.rented: + return Colors.orange; + case EquipmentStatus.lost: + return Colors.red; + case EquipmentStatus.outOfService: + return Colors.red; + case EquipmentStatus.maintenance: + return Colors.yellow; + } + } +} + diff --git a/em2rp/lib/views/widgets/equipment/quantity_display.dart b/em2rp/lib/views/widgets/equipment/quantity_display.dart new file mode 100644 index 0000000..729ed47 --- /dev/null +++ b/em2rp/lib/views/widgets/equipment/quantity_display.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:em2rp/models/equipment_model.dart'; + +/// Widget pour afficher la quantité disponible/totale d'un équipement consommable +class QuantityDisplay extends StatelessWidget { + final EquipmentModel equipment; + + const QuantityDisplay({ + super.key, + required this.equipment, + }); + + @override + Widget build(BuildContext context) { + final availableQty = equipment.availableQuantity ?? 0; + final totalQty = equipment.totalQuantity ?? 0; + final criticalThreshold = equipment.criticalThreshold ?? 0; + final isCritical = + criticalThreshold > 0 && availableQty <= criticalThreshold; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: isCritical + ? Colors.red.withOpacity(0.15) + : Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isCritical ? Colors.red : Colors.grey.shade400, + width: isCritical ? 2 : 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isCritical ? Icons.warning : Icons.inventory, + size: 16, + color: isCritical ? Colors.red : Colors.grey[700], + ), + const SizedBox(width: 6), + Text( + 'Disponible: $availableQty / $totalQty', + style: TextStyle( + fontSize: 13, + fontWeight: isCritical ? FontWeight.bold : FontWeight.normal, + color: isCritical ? Colors.red : Colors.grey[700], + ), + ), + if (isCritical) ...[ + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(10), + ), + child: const Text( + 'CRITIQUE', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + ], + ), + ); + } +} + diff --git a/em2rp/lib/views/widgets/equipment/restock_dialog.dart b/em2rp/lib/views/widgets/equipment/restock_dialog.dart new file mode 100644 index 0000000..88118af --- /dev/null +++ b/em2rp/lib/views/widgets/equipment/restock_dialog.dart @@ -0,0 +1,223 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:em2rp/models/equipment_model.dart'; +import 'package:em2rp/providers/equipment_provider.dart'; +import 'package:em2rp/utils/colors.dart'; + +/// Dialogue pour le restock d'un équipement consommable +class RestockDialog extends StatefulWidget { + final EquipmentModel equipment; + + const RestockDialog({ + super.key, + required this.equipment, + }); + + @override + State createState() => _RestockDialogState(); + + /// Méthode statique pour afficher le dialogue + static Future show(BuildContext context, EquipmentModel equipment) { + return showDialog( + context: context, + builder: (context) => RestockDialog(equipment: equipment), + ); + } +} + +class _RestockDialogState extends State { + final TextEditingController _quantityController = TextEditingController(); + bool _addToTotal = false; + + @override + void dispose() { + _quantityController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Row( + children: [ + const Icon(Icons.add_shopping_cart, color: AppColors.rouge), + const SizedBox(width: 12), + Expanded( + child: Text('Restock - ${widget.equipment.name}'), + ), + ], + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Quantités actuelles + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Quantités actuelles', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.grey[700], + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Disponible:'), + Text( + '${widget.equipment.availableQuantity ?? 0}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Total:'), + Text( + '${widget.equipment.totalQuantity ?? 0}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + // Champ de saisie + TextField( + controller: _quantityController, + decoration: const InputDecoration( + labelText: 'Quantité à ajouter/retirer', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.inventory), + hintText: 'Ex: 10 ou -5', + helperText: 'Nombre positif pour ajouter, négatif pour retirer', + ), + keyboardType: const TextInputType.numberWithOptions(signed: true), + autofocus: true, + ), + const SizedBox(height: 16), + // Checkbox pour ajouter au total + StatefulBuilder( + builder: (context, setState) { + return CheckboxListTile( + title: const Text('Ajouter à la quantité totale'), + subtitle: const Text('Mettre à jour aussi la quantité totale'), + value: _addToTotal, + contentPadding: EdgeInsets.zero, + onChanged: (bool? value) { + setState(() { + _addToTotal = value ?? false; + }); + // Update parent state as well + this.setState(() { + _addToTotal = value ?? false; + }); + }, + ); + }, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () => _handleRestock(context), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.rouge, + foregroundColor: Colors.white, + ), + child: const Text('Valider'), + ), + ], + ); + } + + Future _handleRestock(BuildContext context) async { + final quantityText = _quantityController.text.trim(); + + if (quantityText.isEmpty) { + _showError(context, 'Veuillez entrer une quantité'); + return; + } + + final quantity = int.tryParse(quantityText); + if (quantity == null) { + _showError(context, 'Quantité invalide'); + return; + } + + Navigator.pop(context); + + try { + final currentAvailable = widget.equipment.availableQuantity ?? 0; + final currentTotal = widget.equipment.totalQuantity ?? 0; + + final newAvailable = currentAvailable + quantity; + final newTotal = _addToTotal ? currentTotal + quantity : currentTotal; + + if (newAvailable < 0) { + _showError(context, 'La quantité disponible ne peut pas être négative'); + return; + } + + if (newTotal < 0) { + _showError(context, 'La quantité totale ne peut pas être négative'); + return; + } + + final updatedData = { + 'availableQuantity': newAvailable, + 'totalQuantity': newTotal, + 'updatedAt': DateTime.now().toIso8601String(), + }; + + if (context.mounted) { + await context.read().updateEquipment( + widget.equipment.id, + updatedData, + ); + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + quantity > 0 + ? 'Ajout de $quantity unité(s) effectué' + : 'Retrait de ${quantity.abs()} unité(s) effectué', + ), + backgroundColor: Colors.green, + ), + ); + } + } + } catch (e) { + if (context.mounted) { + _showError(context, 'Erreur: $e'); + } + } + } + + void _showError(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } +} + diff --git a/em2rp/lib/views/widgets/nav/main_drawer.dart b/em2rp/lib/views/widgets/nav/main_drawer.dart index 4158387..7b861a5 100644 --- a/em2rp/lib/views/widgets/nav/main_drawer.dart +++ b/em2rp/lib/views/widgets/nav/main_drawer.dart @@ -112,19 +112,6 @@ class MainDrawer extends StatelessWidget { }, ), ), - 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,