588 lines
18 KiB
Dart
588 lines
18 KiB
Dart
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/providers/local_user_provider.dart';
|
|
import 'package:em2rp/models/container_model.dart';
|
|
import 'package:em2rp/models/equipment_model.dart';
|
|
import 'package:em2rp/views/widgets/common/qr_code_format_selector_dialog.dart';
|
|
import 'package:em2rp/mixins/selection_mode_mixin.dart';
|
|
import 'package:em2rp/views/widgets/management/management_search_bar.dart';
|
|
import 'package:em2rp/views/widgets/management/management_card.dart';
|
|
import 'package:em2rp/views/widgets/management/management_list.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,
|
|
),
|
|
],
|
|
],
|
|
)
|
|
: AppBar(
|
|
title: const Text('Gestion des Containers'),
|
|
backgroundColor: AppColors.rouge,
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
tooltip: 'Retour à la gestion des équipements',
|
|
onPressed: () => Navigator.pushReplacementNamed(context, '/equipment_management'),
|
|
),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.logout, color: Colors.white),
|
|
onPressed: () async {
|
|
final shouldLogout = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Déconnexion'),
|
|
content: const Text('Voulez-vous vraiment vous déconnecter ?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Annuler'),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
child: const Text('Déconnexion'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
if (shouldLogout == true && context.mounted) {
|
|
await context.read<LocalUserProvider>().signOut();
|
|
if (context.mounted) {
|
|
Navigator.pushReplacementNamed(context, '/login');
|
|
}
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
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 ManagementSearchBar(
|
|
controller: _searchController,
|
|
hintText: 'Rechercher un container...',
|
|
onChanged: (value) {
|
|
context.read<ContainerProvider>().setSearchQuery(value);
|
|
},
|
|
onSelectionModeToggle: isSelectionMode ? null : toggleSelectionMode,
|
|
showSelectionModeButton: !isSelectionMode,
|
|
);
|
|
}
|
|
|
|
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 ManagementList<ContainerModel>(
|
|
stream: provider.containersStream,
|
|
cachedItems: _cachedContainers,
|
|
emptyMessage: 'Aucun container trouvé',
|
|
emptyIcon: Icons.inventory_2_outlined,
|
|
onDataReceived: (items) {
|
|
_cachedContainers = items;
|
|
},
|
|
itemBuilder: (container) => _buildContainerCard(container),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildContainerCard(ContainerModel container) {
|
|
final isSelected = isItemSelected(container.id);
|
|
|
|
return ManagementCard<ContainerModel>(
|
|
item: container,
|
|
getId: (c) => c.id,
|
|
getTitle: (c) => c.id,
|
|
getSubtitle: (c) => c.name,
|
|
getIcon: (c) => c.type.getIcon(
|
|
size: 40,
|
|
color: AppColors.rouge,
|
|
),
|
|
getInfoChips: (c) => [
|
|
InfoChip(
|
|
label: c.type.label,
|
|
icon: Icons.category,
|
|
),
|
|
InfoChip(
|
|
label: '${c.itemCount} items',
|
|
icon: Icons.inventory,
|
|
),
|
|
],
|
|
getStatusBadge: (c) => StatusBadge(
|
|
label: c.status.label,
|
|
color: c.status.color,
|
|
),
|
|
actions: const [
|
|
CardAction(
|
|
id: 'view',
|
|
label: 'Voir détails',
|
|
icon: Icons.visibility,
|
|
),
|
|
CardAction(
|
|
id: 'edit',
|
|
label: 'Modifier',
|
|
icon: Icons.edit,
|
|
),
|
|
CardAction(
|
|
id: 'qr',
|
|
label: 'QR Code',
|
|
icon: Icons.qr_code,
|
|
),
|
|
CardAction(
|
|
id: 'delete',
|
|
label: 'Supprimer',
|
|
icon: Icons.delete,
|
|
color: Colors.red,
|
|
),
|
|
],
|
|
onActionSelected: _handleMenuAction,
|
|
onTap: () {
|
|
if (isSelectionMode) {
|
|
toggleItemSelection(container.id);
|
|
} else {
|
|
_viewContainerDetails(container);
|
|
}
|
|
},
|
|
onLongPress: () {
|
|
if (!isSelectionMode) {
|
|
toggleSelectionMode();
|
|
toggleItemSelection(container.id);
|
|
}
|
|
},
|
|
isSelectionMode: isSelectionMode,
|
|
isSelected: isSelected,
|
|
onSelectionChanged: (value) {
|
|
toggleItemSelection(container.id);
|
|
},
|
|
);
|
|
}
|
|
|
|
|
|
void _handleMenuAction(String action, ContainerModel container) {
|
|
switch (action) {
|
|
case 'view':
|
|
_viewContainerDetails(container);
|
|
break;
|
|
case 'edit':
|
|
_editContainer(container);
|
|
break;
|
|
case 'qr':
|
|
// Non utilisé - les QR codes multiples sont générés via _generateQRCodesForSelected
|
|
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 avec le widget générique
|
|
if (mounted) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => QRCodeFormatSelectorDialog<ContainerModel>(
|
|
itemList: selectedContainers,
|
|
getId: (c) => c.id,
|
|
getTitle: (c) => c.name,
|
|
getDetails: (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}',
|
|
];
|
|
},
|
|
dialogTitle: 'Générer ${selectedContainers.length} QR Code(s)',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|