Refactor de la page de détails de l'équipement et ajouts de widgets communs

Refactor de la page `equipment_detail_page` en la décomposant en plusieurs widgets de section réutilisables pour une meilleure lisibilité et maintenance :
- `EquipmentHeaderSection` : En-tête avec titre et informations principales.
- `EquipmentMainInfoSection` : Informations sur la catégorie, la marque, le modèle et le statut.
- `EquipmentNotesSection` : Affichage des notes.
- `EquipmentDatesSection` : Gestion de l'affichage des dates (achat, maintenance, création, etc.).
- `EquipmentPriceSection` : Section dédiée aux prix.
- `EquipmentMaintenanceHistorySection` : Historique des maintenances.
- `EquipmentAssociatedEventsSection` : Placeholder pour les événements à venir.
- `EquipmentReferencingContainers` : Affiche les boites (containers) qui contiennent cet équipement.

Ajout de plusieurs widgets communs et utilitaires :
- Widgets UI : `SearchBarWidget`, `SelectionAppBar`, `CustomFilterChip`, `EmptyState`, `InfoChip`, `StatusBadge`, `QuantityDisplay`.
- Dialogues : `RestockDialog` pour les consommables et `DialogUtils` pour les confirmations génériques.

Autres modifications :
- Mise à jour de la terminologie "Container" en "Boite" dans l'interface utilisateur.
- Amélioration de la sélection d'équipements dans le formulaire des boites.
- Ajout d'instructions pour Copilot (`copilot-instructions.md`).
- Mise à jour de certaines icônes pour les types de boites.
This commit is contained in:
ElPoyo
2025-10-30 17:40:28 +01:00
parent df6d54a007
commit 822d4443f9
25 changed files with 1687 additions and 570 deletions

View File

@@ -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,
),
);
}
}

View File

@@ -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]),
),
],
],
),
);
}
}

View File

@@ -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,
),
),
],
),
);
}
}

View File

@@ -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<String> 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,
),
);
}
}

View File

@@ -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<Widget>? 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!,
],
],
);
}
}

View File

@@ -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,
),
),
);
}
}