Files
EM2_ERP/em2rp/lib/views/container_management_page.dart

801 lines
25 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/utils/permission_gate.dart';
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
import 'package:em2rp/providers/container_provider.dart';
import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/services/pdf_service.dart';
import 'package:em2rp/views/widgets/common/qr_code_dialog.dart';
import 'package:em2rp/mixins/selection_mode_mixin.dart';
import 'package:printing/printing.dart';
import 'package:pdf/pdf.dart';
class ContainerManagementPage extends StatefulWidget {
const ContainerManagementPage({super.key});
@override
State<ContainerManagementPage> createState() =>
_ContainerManagementPageState();
}
class _ContainerManagementPageState extends State<ContainerManagementPage>
with SelectionModeMixin<ContainerManagementPage> {
final TextEditingController _searchController = TextEditingController();
ContainerType? _selectedType;
EquipmentStatus? _selectedStatus;
List<ContainerModel>? _cachedContainers; // Cache pour éviter le rebuild
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 800;
return PermissionGate(
requiredPermissions: const ['view_equipment'],
fallback: Scaffold(
appBar: const CustomAppBar(title: 'Accès refusé'),
drawer: const MainDrawer(currentPage: '/container_management'),
body: const Center(
child: Padding(
padding: EdgeInsets.all(24.0),
child: Text(
'Vous n\'avez pas les permissions nécessaires pour accéder à la gestion des containers.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
),
),
),
child: Scaffold(
appBar: isSelectionMode
? AppBar(
backgroundColor: AppColors.rouge,
leading: IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: toggleSelectionMode,
),
title: Text(
'$selectedCount sélectionné(s)',
style: const TextStyle(color: Colors.white),
),
actions: [
if (hasSelection) ...[
IconButton(
icon: const Icon(Icons.qr_code, color: Colors.white),
tooltip: 'Générer QR Codes',
onPressed: _generateQRCodesForSelected,
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.white),
tooltip: 'Supprimer',
onPressed: _deleteSelectedContainers,
),
],
],
)
: const CustomAppBar(title: 'Gestion des Containers'),
drawer: const MainDrawer(currentPage: '/container_management'),
floatingActionButton: !isSelectionMode
? FloatingActionButton.extended(
onPressed: () => _navigateToForm(context),
backgroundColor: AppColors.rouge,
icon: const Icon(Icons.add, color: Colors.white),
label: const Text(
'Nouveau Container',
style: TextStyle(color: Colors.white),
),
)
: null,
body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(),
),
);
}
Widget _buildMobileLayout() {
return Column(
children: [
_buildSearchBar(),
_buildMobileFilters(),
Expanded(child: _buildContainerList()),
],
);
}
Widget _buildDesktopLayout() {
return Row(
children: [
SizedBox(
width: 250,
child: _buildSidebar(),
),
const VerticalDivider(width: 1, thickness: 1),
Expanded(
child: Column(
children: [
_buildSearchBar(),
Expanded(child: _buildContainerList()),
],
),
),
],
);
}
Widget _buildSearchBar() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher un container...',
prefixIcon: const Icon(Icons.search, color: AppColors.rouge),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey.shade300),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
context.read<ContainerProvider>().setSearchQuery(value);
},
),
),
const SizedBox(width: 12),
if (!isSelectionMode)
IconButton(
icon: const Icon(Icons.checklist, color: AppColors.rouge),
tooltip: 'Mode sélection',
onPressed: toggleSelectionMode,
),
],
),
);
}
Widget _buildMobileFilters() {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
color: Colors.grey.shade50,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
_buildTypeChip(null, 'Tous'),
const SizedBox(width: 8),
...ContainerType.values.map((type) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: _buildTypeChip(type, type.label),
);
}),
],
),
),
);
}
Widget _buildTypeChip(ContainerType? type, String label) {
final isSelected = _selectedType == type;
final color = isSelected ? Colors.white : AppColors.noir;
return ChoiceChip(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (type != null) ...[
type.getIcon(size: 16, color: color),
const SizedBox(width: 8),
],
Text(label),
],
),
selected: isSelected,
onSelected: (selected) {
setState(() {
_selectedType = selected ? type : null;
context.read<ContainerProvider>().setSelectedType(_selectedType);
});
},
selectedColor: AppColors.rouge,
labelStyle: TextStyle(
color: color,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
);
}
Widget _buildSidebar() {
return Container(
color: Colors.grey.shade50,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
'Filtres',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: AppColors.noir,
),
),
const SizedBox(height: 16),
// Filtre par type
Text(
'Type de container',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: AppColors.noir,
),
),
const SizedBox(height: 8),
_buildFilterOption(null, 'Tous les types'),
...ContainerType.values.map((type) {
return _buildFilterOption(type, type.label);
}),
const Divider(height: 32),
// Filtre par statut
Text(
'Statut',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: AppColors.noir,
),
),
const SizedBox(height: 8),
_buildStatusFilter(null, 'Tous les statuts'),
_buildStatusFilter(EquipmentStatus.available, 'Disponible'),
_buildStatusFilter(EquipmentStatus.inUse, 'En prestation'),
_buildStatusFilter(EquipmentStatus.maintenance, 'En maintenance'),
_buildStatusFilter(EquipmentStatus.outOfService, 'Hors service'),
],
),
);
}
Widget _buildFilterOption(ContainerType? type, String label) {
final isSelected = _selectedType == type;
return RadioListTile<ContainerType?>(
title: Text(label),
value: type,
groupValue: _selectedType,
activeColor: AppColors.rouge,
dense: true,
contentPadding: EdgeInsets.zero,
onChanged: (value) {
setState(() {
_selectedType = value;
context.read<ContainerProvider>().setSelectedType(_selectedType);
});
},
);
}
Widget _buildStatusFilter(EquipmentStatus? status, String label) {
final isSelected = _selectedStatus == status;
return RadioListTile<EquipmentStatus?>(
title: Text(label),
value: status,
groupValue: _selectedStatus,
activeColor: AppColors.rouge,
dense: true,
contentPadding: EdgeInsets.zero,
onChanged: (value) {
setState(() {
_selectedStatus = value;
context.read<ContainerProvider>().setSelectedStatus(_selectedStatus);
});
},
);
}
Widget _buildContainerList() {
return Consumer<ContainerProvider>(
builder: (context, provider, child) {
return StreamBuilder<List<ContainerModel>>(
stream: provider.containersStream,
builder: (context, snapshot) {
// Utiliser les données en cache si disponibles pendant le rebuild
if (snapshot.hasData) {
_cachedContainers = snapshot.data;
}
// Afficher le loader seulement au premier chargement
if (snapshot.connectionState == ConnectionState.waiting && _cachedContainers == null) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Text('Erreur: ${snapshot.error}'),
);
}
final containers = _cachedContainers ?? snapshot.data ?? [];
if (containers.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inventory_2_outlined,
size: 80,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Aucun container trouvé',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
),
),
],
),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: containers.length,
itemBuilder: (context, index) {
final container = containers[index];
return _buildContainerCard(container);
},
);
},
);
},
);
}
Widget _buildContainerCard(ContainerModel container) {
final isSelected = isItemSelected(container.id);
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: isSelected
? const BorderSide(color: AppColors.rouge, width: 2)
: BorderSide.none,
),
child: InkWell(
onTap: () {
if (isSelectionMode) {
toggleItemSelection(container.id);
} else {
_viewContainerDetails(container);
}
},
onLongPress: () {
if (!isSelectionMode) {
toggleSelectionMode();
toggleItemSelection(container.id);
}
},
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
if (isSelectionMode)
Padding(
padding: const EdgeInsets.only(right: 16),
child: Checkbox(
value: isSelected,
onChanged: (value) {
toggleItemSelection(container.id);
},
activeColor: AppColors.rouge,
),
),
// Icône du type de container
container.type.getIcon(
size: 40,
color: AppColors.rouge,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
container.id,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
container.name,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 4),
Row(
children: [
_buildInfoChip(
container.type.label,
Icons.category,
),
const SizedBox(width: 8),
_buildInfoChip(
'${container.itemCount} items',
Icons.inventory,
),
],
),
],
),
),
const SizedBox(width: 16),
// Badge de statut
_buildStatusBadge(container.status),
if (!isSelectionMode) ...[
const SizedBox(width: 8),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) => _handleMenuAction(value, container),
itemBuilder: (context) => [
const PopupMenuItem(
value: 'view',
child: Row(
children: [
Icon(Icons.visibility, size: 20),
SizedBox(width: 8),
Text('Voir détails'),
],
),
),
const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit, size: 20),
SizedBox(width: 8),
Text('Modifier'),
],
),
),
const PopupMenuItem(
value: 'qr',
child: Row(
children: [
Icon(Icons.qr_code, size: 20),
SizedBox(width: 8),
Text('QR Code'),
],
),
),
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)),
],
),
),
],
),
],
],
),
),
),
);
}
Widget _buildInfoChip(String label, IconData icon) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: Colors.grey.shade700),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade700,
),
),
],
),
);
}
Widget _buildStatusBadge(EquipmentStatus status) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: status.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: status.color),
),
child: Text(
status.label,
style: TextStyle(
color: status.color,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
);
}
void _handleMenuAction(String action, ContainerModel container) {
switch (action) {
case 'view':
_viewContainerDetails(container);
break;
case 'edit':
_editContainer(container);
break;
case 'qr':
showDialog(
context: context,
builder: (context) => QRCodeDialog.forContainer(container),
);
break;
case 'delete':
_deleteContainer(container);
break;
}
}
void _navigateToForm(BuildContext context) async {
final result = await Navigator.pushNamed(context, '/container_form');
if (result == true) {
// Rafraîchir la liste
}
}
void _viewContainerDetails(ContainerModel container) async {
await Navigator.pushNamed(
context,
'/container_detail',
arguments: container,
);
}
void _editContainer(ContainerModel container) async {
await Navigator.pushNamed(
context,
'/container_form',
arguments: container,
);
}
Future<void> _generateQRCodesForSelected() async {
if (!hasSelection) return;
// Récupérer les containers sélectionnés
final containerProvider = context.read<ContainerProvider>();
final List<ContainerModel> selectedContainers = [];
final Map<String, List<EquipmentModel>> containerEquipmentMap = {};
for (final id in selectedIds) {
final container = await containerProvider.getContainerById(id);
if (container != null) {
selectedContainers.add(container);
// Charger les équipements pour ce container
final equipment = await containerProvider.getContainerEquipment(id);
containerEquipmentMap[id] = equipment;
}
}
if (selectedContainers.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Aucun container trouvé')),
);
}
return;
}
// Afficher le dialogue de sélection de format
final format = await showDialog<QRLabelFormat>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Format des étiquettes'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.qr_code_2),
title: const Text('Petits QR codes'),
subtitle: const Text('2×2 cm - QR code + ID (20 par page)'),
onTap: () => Navigator.pop(context, QRLabelFormat.small),
),
ListTile(
leading: const Icon(Icons.qr_code),
title: const Text('QR codes moyens'),
subtitle: const Text('4×4 cm - QR code + ID (6 par page)'),
onTap: () => Navigator.pop(context, QRLabelFormat.medium),
),
ListTile(
leading: const Icon(Icons.label),
title: const Text('Grandes étiquettes'),
subtitle: const Text('QR code + ID + Type + Contenu (10 par page)'),
onTap: () => Navigator.pop(context, QRLabelFormat.large),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
],
),
);
if (format == null || !mounted) return;
// Générer et afficher le PDF avec la nouvelle API optimisée
try {
final pdfBytes = await PDFService.generatePDF<ContainerModel>(
items: selectedContainers,
format: format,
getId: (c) => c.id,
getTitle: (c) => c.name,
getDetails: format == QRLabelFormat.large ? (ContainerModel c) {
final equipment = containerEquipmentMap[c.id] ?? <EquipmentModel>[];
return [
'Contenu (${equipment.length}):',
...equipment.take(5).map((eq) => '- ${eq.id}'),
if (equipment.length > 5) '... +${equipment.length - 5}',
];
} : null,
);
if (mounted) {
await Printing.layoutPdf(
onLayout: (PdfPageFormat format) async => pdfBytes,
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur lors de la génération: $e')),
);
}
}
}
Future<void> _deleteContainer(ContainerModel container) 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) {
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 lors de la suppression: $e')),
);
}
}
}
}
Future<void> _deleteSelectedContainers() 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 $selectedCount container(s) ?\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 {
final provider = context.read<ContainerProvider>();
for (final id in selectedIds) {
await provider.deleteContainer(id);
}
disableSelectionMode();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Containers supprimés avec succès')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur lors de la suppression: $e')),
);
}
}
}
}
}