Files
EM2_ERP/em2rp/lib/views/container_detail_page.dart
ElPoyo 822d4443f9 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.
2025-10-30 17:40:28 +01:00

774 lines
22 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/models/equipment_model.dart';
import 'package:em2rp/providers/container_provider.dart';
import 'package:em2rp/views/equipment_detail_page.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:intl/intl.dart';
class ContainerDetailPage extends StatefulWidget {
final ContainerModel container;
const ContainerDetailPage({super.key, required this.container});
@override
State<ContainerDetailPage> createState() => _ContainerDetailPageState();
}
class _ContainerDetailPageState extends State<ContainerDetailPage> {
late ContainerModel _container;
List<EquipmentModel> _equipmentList = [];
bool _isLoadingEquipment = true;
@override
void initState() {
super.initState();
_container = widget.container;
_loadEquipment();
}
Future<void> _loadEquipment() async {
setState(() {
_isLoadingEquipment = true;
});
try {
final containerProvider = context.read<ContainerProvider>();
final equipment = await containerProvider.getContainerEquipment(_container.id);
setState(() {
_equipmentList = equipment;
_isLoadingEquipment = false;
});
} catch (e) {
setState(() {
_isLoadingEquipment = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur lors du chargement: $e')),
);
}
}
}
Future<void> _refreshContainer() async {
final containerProvider = context.read<ContainerProvider>();
final updated = await containerProvider.getContainerById(_container.id);
if (updated != null) {
setState(() {
_container = updated;
});
await _loadEquipment();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Détails de la boite'),
backgroundColor: AppColors.rouge,
foregroundColor: Colors.white,
actions: [
IconButton(
icon: const Icon(Icons.edit),
tooltip: 'Modifier',
onPressed: _editContainer,
),
IconButton(
icon: const Icon(Icons.qr_code),
tooltip: 'QR Code',
onPressed: _showQRCode,
),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: _handleMenuAction,
itemBuilder: (context) => [
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, color: Colors.red, size: 20),
SizedBox(width: 8),
Text('Supprimer', style: TextStyle(color: Colors.red)),
],
),
),
],
),
],
),
body: RefreshIndicator(
onRefresh: _refreshContainer,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildHeaderCard(),
const SizedBox(height: 16),
_buildPhysicalCharacteristics(),
const SizedBox(height: 16),
_buildEquipmentSection(),
const SizedBox(height: 16),
if (_container.notes != null && _container.notes!.isNotEmpty)
_buildNotesSection(),
const SizedBox(height: 16),
_buildHistorySection(),
],
),
),
);
}
Widget _buildHeaderCard() {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_container.type.getIcon(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(
'Type',
_container.type.label,
Icons.category,
),
),
Expanded(
child: _buildInfoItem(
'Statut',
_getStatusLabel(_container.status),
Icons.info,
statusColor: _getStatusColor(_container.status),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildInfoItem(
'Équipements',
'${_container.itemCount}',
Icons.inventory,
),
),
Expanded(
child: _buildInfoItem(
'Poids total',
_calculateTotalWeight(),
Icons.scale,
),
),
],
),
],
),
),
);
}
Widget _buildPhysicalCharacteristics() {
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)}',
Icons.view_in_ar,
),
],
],
),
),
);
}
Widget _buildEquipmentSection() {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Contenu du container',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const Divider(height: 24),
if (_isLoadingEquipment)
const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
),
)
else if (_equipmentList.isEmpty)
Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
children: [
Icon(
Icons.inventory_2_outlined,
size: 60,
color: Colors.grey.shade400,
),
const SizedBox(height: 12),
Text(
'Aucun équipement dans ce container',
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade600,
),
),
],
),
),
)
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _equipmentList.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final equipment = _equipmentList[index];
return _buildEquipmentTile(equipment);
},
),
],
),
),
);
}
Widget _buildEquipmentTile(EquipmentModel equipment) {
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: () => _viewEquipment(equipment),
),
IconButton(
icon: const Icon(Icons.remove_circle, color: Colors.red, size: 20),
tooltip: 'Retirer',
onPressed: () => _removeEquipment(equipment),
),
],
),
);
}
Widget _buildNotesSection() {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Notes',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const Divider(height: 24),
Text(
_container.notes!,
style: const TextStyle(fontSize: 14),
),
],
),
),
);
}
Widget _buildHistorySection() {
if (_container.history.isEmpty) {
return const SizedBox.shrink();
}
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Historique',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const Divider(height: 24),
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _container.history.length > 10
? 10
: _container.history.length,
separatorBuilder: (context, index) => const Divider(height: 16),
itemBuilder: (context, index) {
final entry = _container.history[
_container.history.length - 1 - index]; // Plus récent en premier
return _buildHistoryEntry(entry);
},
),
],
),
),
);
}
Widget _buildHistoryEntry(ContainerHistoryEntry entry) {
IconData icon;
Color color;
String description;
switch (entry.action) {
case 'equipment_added':
icon = Icons.add_circle;
color = Colors.green;
description = 'Équipement ajouté: ${entry.equipmentId ?? "?"}';
break;
case 'equipment_removed':
icon = Icons.remove_circle;
color = Colors.red;
description = 'Équipement retiré: ${entry.equipmentId ?? "?"}';
break;
case 'status_change':
icon = Icons.sync;
color = Colors.blue;
description =
'Statut changé: ${entry.previousValue}${entry.newValue}';
break;
default:
icon = Icons.info;
color = Colors.grey;
description = entry.action;
}
return Row(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
description,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 2),
Text(
DateFormat('dd/MM/yyyy HH:mm').format(entry.timestamp),
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
],
);
}
Widget _buildInfoItem(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,
),
),
],
);
}
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,
),
),
],
);
}
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 _calculateTotalWeight() {
if (_equipmentList.isEmpty && _container.weight == null) {
return '-';
}
final totalWeight = _container.calculateTotalWeight(_equipmentList);
return '${totalWeight.toStringAsFixed(1)} kg';
}
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;
}
}
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';
}
}
void _handleMenuAction(String action) {
if (action == 'delete') {
_deleteContainer();
}
}
void _editContainer() {
Navigator.pushNamed(
context,
'/container_form',
arguments: _container,
).then((_) => _refreshContainer());
}
void _showQRCode() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('QR Code - ${_container.name}'),
content: SizedBox(
width: 250,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
QrImageView(
data: _container.id,
version: QrVersions.auto,
size: 200,
),
const SizedBox(height: 16),
Text(
_container.id,
style: const TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Fermer'),
),
],
),
);
}
void _viewEquipment(EquipmentModel equipment) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EquipmentDetailPage(equipment: equipment),
),
);
}
Future<void> _removeEquipment(EquipmentModel equipment) async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Retirer l\'équipement'),
content: Text(
'Êtes-vous sûr de vouloir retirer "${equipment.id}" de ce container ?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Retirer'),
),
],
),
);
if (confirm == true && mounted) {
try {
await context.read<ContainerProvider>().removeEquipmentFromContainer(
containerId: _container.id,
equipmentId: equipment.id,
);
await _refreshContainer();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Équipement retiré avec succès')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e')),
);
}
}
}
}
Future<void> _deleteContainer() async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmer la suppression'),
content: Text(
'Êtes-vous sûr de vouloir supprimer le container "${_container.name}" ?\n\n'
'Cette action est irréversible.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Supprimer'),
),
],
),
);
if (confirm == true && mounted) {
try {
await context.read<ContainerProvider>().deleteContainer(_container.id);
if (mounted) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Container supprimé avec succès')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e')),
);
}
}
}
}
}