Files
EM2_ERP/em2rp/lib/views/container_detail_page.dart
ElPoyo df6d54a007 Refactor: Centralisation des labels et icônes pour les enums
Centralise la gestion des libellés, couleurs et icônes pour `EquipmentStatus`, `EquipmentCategory`, et `ContainerType` en utilisant des extensions Dart.

- Ajout de nouvelles icônes SVG pour `flight-case`, `truss` et `tape`.
- Refactorisation des vues pour utiliser les nouvelles extensions, supprimant ainsi la logique d'affichage dupliquée.
- Mise à jour des `ChoiceChip` et des listes de filtres pour afficher les icônes à côté des labels.
2025-10-29 18:43:24 +01:00

794 lines
22 KiB
Dart
Raw 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 du Container'),
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: [
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(
'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';
}
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;
}
}
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')),
);
}
}
}
}
}