Files
EM2_ERP/em2rp/lib/views/equipment_management_page.dart
ElPoyo 3fab69cb00 feat: Gestion complète des containers et refactorisation du matériel
Ajout de la gestion des containers (création, édition, suppression, affichage des détails).
Introduction d'un système de génération de QR codes unifié et d'un mode de sélection multiple.

**Features:**
- **Gestion des Containers :**
    - Nouvelle page de gestion des containers (`container_management_page.dart`) avec recherche et filtres.
    - Formulaire de création/édition de containers (`container_form_page.dart`) avec génération d'ID automatique.
    - Page de détails d'un container (`container_detail_page.dart`) affichant son contenu et ses caractéristiques.
    - Ajout des routes et du provider (`ContainerProvider`) nécessaires.
- **Modèle de Données :**
    - Ajout du `ContainerModel` pour représenter les boîtes, flight cases, etc.
    - Le modèle `EquipmentModel` a été enrichi avec des caractéristiques physiques (poids, dimensions).
- **QR Codes :**
    - Nouveau service unifié (`UnifiedPDFGeneratorService`) pour générer des PDFs de QR codes pour n'importe quelle entité.
    - Services `PDFGeneratorService` et `ContainerPDFGeneratorService` transformés en wrappers pour maintenir la compatibilité.
    - Amélioration de la performance de la génération de QR codes en masse.
- **Interface Utilisateur (UI/UX) :**
    - Nouvelle page de détails pour le matériel (`equipment_detail_page.dart`).
    - Ajout d'un `SelectionModeMixin` pour gérer la sélection multiple dans les pages de gestion.
    - Dialogues réutilisables pour l'affichage de QR codes (`QRCodeDialog`) et la sélection de format d'impression (`QRCodeFormatSelectorDialog`).
    - Ajout d'un bouton "Gérer les boîtes" sur la page de gestion du matériel.

**Refactorisation :**
- L' `IdGenerator` a été déplacé dans le répertoire `utils` et étendu pour gérer les containers.
- Mise à jour de nombreuses dépendances `pubspec.yaml` vers des versions plus récentes.
- Séparation de la logique d'affichage des containers et du matériel dans des widgets dédiés (`ContainerHeaderCard`, `EquipmentParentContainers`, etc.).
2025-10-29 10:57:42 +01:00

1065 lines
37 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/equipment_provider.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/views/equipment_form_page.dart';
import 'package:em2rp/views/equipment_detail_page.dart';
import 'package:em2rp/views/widgets/common/qr_code_dialog.dart';
import 'package:em2rp/views/widgets/common/qr_code_format_selector_dialog.dart';
import 'package:em2rp/mixins/selection_mode_mixin.dart';
class EquipmentManagementPage extends StatefulWidget {
const EquipmentManagementPage({super.key});
@override
State<EquipmentManagementPage> createState() =>
_EquipmentManagementPageState();
}
class _EquipmentManagementPageState extends State<EquipmentManagementPage>
with SelectionModeMixin<EquipmentManagementPage> {
final TextEditingController _searchController = TextEditingController();
EquipmentCategory? _selectedCategory;
@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: '/equipment_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 du matériel.',
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: _deleteSelectedEquipment,
),
],
],
)
: CustomAppBar(
title: 'Gestion du matériel',
actions: [
IconButton(
icon: const Icon(Icons.checklist),
tooltip: 'Mode sélection',
onPressed: toggleSelectionMode,
),
],
),
drawer: const MainDrawer(currentPage: '/equipment_management'),
body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(),
floatingActionButton: isSelectionMode ? null : _buildFAB(),
),
);
}
Widget _buildFAB() {
return PermissionGate(
requiredPermissions: const ['manage_equipment'],
child: FloatingActionButton.extended(
onPressed: _createNewEquipment,
backgroundColor: AppColors.rouge,
icon: const Icon(Icons.add),
label: const Text('Ajouter un équipement'),
),
);
}
Widget _buildMobileLayout() {
return Column(
children: [
// Barre de recherche et bouton boîtes
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher par nom, modèle ou ID...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
context.read<EquipmentProvider>().setSearchQuery('');
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
onChanged: (value) {
context.read<EquipmentProvider>().setSearchQuery(value);
},
),
),
const SizedBox(width: 8),
// Bouton Gérer les boîtes
IconButton.filled(
onPressed: () {
Navigator.pushNamed(context, '/container_management');
},
icon: const Icon(Icons.inventory_2),
tooltip: 'Gérer les boîtes',
style: IconButton.styleFrom(
backgroundColor: AppColors.rouge,
foregroundColor: Colors.white,
),
),
],
),
),
// Menu horizontal de filtres par catégorie
SizedBox(
height: 60,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: ChoiceChip(
label: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.all_inclusive, size: 16, color: Colors.white),
SizedBox(width: 8),
Text('Tout'),
],
),
selected: _selectedCategory == null,
onSelected: (selected) {
if (selected) {
setState(() => _selectedCategory = null);
context
.read<EquipmentProvider>()
.setSelectedCategory(null);
}
},
selectedColor: AppColors.rouge,
labelStyle: TextStyle(
color: _selectedCategory == null
? Colors.white
: AppColors.rouge,
fontWeight: _selectedCategory == null
? FontWeight.bold
: FontWeight.normal,
),
),
),
..._buildCategoryChips(),
],
),
),
const Divider(),
// Liste des équipements
Expanded(child: _buildEquipmentList()),
],
);
}
Widget _buildDesktopLayout() {
return Row(
children: [
// Sidebar gauche avec filtres
Container(
width: 280,
decoration: BoxDecoration(
color: Colors.grey[100],
border: const Border(
right: BorderSide(color: Colors.grey, width: 1),
),
),
child: Column(
children: [
// Bouton Gérer les boîtes
Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton.icon(
onPressed: () {
Navigator.pushNamed(context, '/container_management');
},
icon: const Icon(Icons.inventory_2, color: Colors.white),
label: const Text(
'Gérer les boîtes',
style: TextStyle(color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.rouge,
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
minimumSize: const Size(double.infinity, 50),
),
),
),
const Divider(),
// En-tête filtres
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: Row(
children: [
Icon(Icons.filter_list, color: AppColors.rouge, size: 20),
const SizedBox(width: 12),
Expanded(
child: Text(
'Filtres',
style:
Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: AppColors.rouge,
),
),
),
],
),
),
// Barre de recherche
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher...',
prefixIcon: const Icon(Icons.search, size: 20),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, size: 20),
onPressed: () {
_searchController.clear();
context
.read<EquipmentProvider>()
.setSearchQuery('');
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
isDense: true,
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
onChanged: (value) {
context.read<EquipmentProvider>().setSearchQuery(value);
},
),
),
const Divider(),
// Filtres par catégorie
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'Catégories',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
),
),
Expanded(
child: ListView(
children: [
ListTile(
leading: Icon(
Icons.all_inclusive,
color: _selectedCategory == null
? AppColors.rouge
: Colors.grey[600],
),
title: Text(
'Tout',
style: TextStyle(
color: _selectedCategory == null
? AppColors.rouge
: Colors.black87,
fontWeight: _selectedCategory == null
? FontWeight.bold
: FontWeight.normal,
),
),
selected: _selectedCategory == null,
selectedTileColor: AppColors.rouge.withOpacity(0.1),
onTap: () {
setState(() => _selectedCategory = null);
context
.read<EquipmentProvider>()
.setSelectedCategory(null);
},
),
..._buildCategoryListTiles(),
],
),
),
],
),
),
// Contenu principal
Expanded(child: _buildEquipmentList()),
],
);
}
List<Widget> _buildCategoryChips() {
final categories = [
(EquipmentCategory.lighting, Icons.light_mode, 'Lumière'),
(EquipmentCategory.sound, Icons.volume_up, 'Son'),
(EquipmentCategory.video, Icons.videocam, 'Vidéo'),
(EquipmentCategory.effect, Icons.auto_awesome, 'Effets'),
(EquipmentCategory.structure, Icons.construction, 'Structure'),
(EquipmentCategory.consumable, Icons.inventory_2, 'Consommable'),
(EquipmentCategory.cable, Icons.cable, 'Câble'),
(EquipmentCategory.other, Icons.more_horiz, 'Autre'),
];
return categories.map((cat) {
final isSelected = _selectedCategory == cat.$1;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: ChoiceChip(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
cat.$2,
size: 16,
color: isSelected ? Colors.white : AppColors.rouge,
),
const SizedBox(width: 8),
Text(cat.$3),
],
),
selected: isSelected,
onSelected: (selected) {
if (selected) {
setState(() => _selectedCategory = cat.$1);
context.read<EquipmentProvider>().setSelectedCategory(cat.$1);
}
},
selectedColor: AppColors.rouge,
labelStyle: TextStyle(
color: isSelected ? Colors.white : AppColors.rouge,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
);
}).toList();
}
List<Widget> _buildCategoryListTiles() {
final categories = [
(EquipmentCategory.lighting, Icons.light_mode, 'Lumière'),
(EquipmentCategory.sound, Icons.volume_up, 'Son'),
(EquipmentCategory.video, Icons.videocam, 'Vidéo'),
(EquipmentCategory.effect, Icons.auto_awesome, 'Effets'),
(EquipmentCategory.structure, Icons.construction, 'Structure'),
(EquipmentCategory.consumable, Icons.inventory_2, 'Consommable'),
(EquipmentCategory.cable, Icons.cable, 'Câble'),
(EquipmentCategory.other, Icons.more_horiz, 'Autre'),
];
return categories.map((cat) {
final isSelected = _selectedCategory == cat.$1;
return ListTile(
leading: Icon(
cat.$2,
color: isSelected ? AppColors.rouge : Colors.grey[600],
),
title: Text(
cat.$3,
style: TextStyle(
color: isSelected ? AppColors.rouge : Colors.black87,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
selected: isSelected,
selectedTileColor: AppColors.rouge.withOpacity(0.1),
onTap: () {
setState(() => _selectedCategory = cat.$1);
context.read<EquipmentProvider>().setSelectedCategory(cat.$1);
},
);
}).toList();
}
Widget _buildEquipmentList() {
return Consumer<EquipmentProvider>(
builder: (context, provider, child) {
return StreamBuilder<List<EquipmentModel>>(
stream: provider.equipmentStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Text('Erreur: ${snapshot.error}'),
);
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inventory_2_outlined,
size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'Aucun équipement trouvé',
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
'Ajoutez votre premier équipement',
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
),
],
),
);
}
// Tri par nom
final equipment = snapshot.data!;
equipment.sort((a, b) => a.name.compareTo(b.name));
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: equipment.length,
itemBuilder: (context, index) {
return _buildEquipmentCard(equipment[index]);
},
);
},
);
},
);
}
Widget _buildEquipmentCard(EquipmentModel equipment) {
final isSelected = isItemSelected(equipment.id);
return Card(
margin: const EdgeInsets.only(bottom: 12),
color: isSelectionMode && isSelected
? AppColors.rouge.withOpacity(0.1)
: null,
child: ListTile(
leading: isSelectionMode
? Checkbox(
value: isSelected,
onChanged: (value) => toggleItemSelection(equipment.id),
activeColor: AppColors.rouge,
)
: CircleAvatar(
backgroundColor:
_getStatusColor(equipment.status).withOpacity(0.2),
child: Icon(
_getCategoryIcon(equipment.category),
color: _getStatusColor(equipment.status),
),
),
title: Row(
children: [
Expanded(
child: Text(
equipment.id,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
// Afficher le statut uniquement si ce n'est pas un consommable ou câble
if (equipment.category != EquipmentCategory.consumable &&
equipment.category != EquipmentCategory.cable)
_buildStatusBadge(equipment.status),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
'${equipment.brand ?? ''} ${equipment.model ?? ''}'
.trim()
.isNotEmpty
? '${equipment.brand ?? ''} ${equipment.model ?? ''}'.trim()
: 'Marque/Modèle non défini',
style: TextStyle(color: Colors.grey[600], fontSize: 14),
),
// Afficher la quantité disponible pour les consommables/câbles
if (equipment.category == EquipmentCategory.consumable ||
equipment.category == EquipmentCategory.cable) ...[
const SizedBox(height: 4),
_buildQuantityDisplay(equipment),
],
],
),
trailing: isSelectionMode
? null
: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Bouton Restock (uniquement pour consommables/câbles avec permission)
if (equipment.category == EquipmentCategory.consumable ||
equipment.category == EquipmentCategory.cable)
PermissionGate(
requiredPermissions: const ['manage_equipment'],
child: IconButton(
icon: const Icon(Icons.add_shopping_cart,
color: AppColors.rouge),
tooltip: 'Restock',
onPressed: () => _showRestockDialog(equipment),
),
),
// Bouton QR Code
IconButton(
icon: const Icon(Icons.qr_code, color: AppColors.rouge),
tooltip: 'QR Code',
onPressed: () => showDialog(
context: context,
builder: (context) => QRCodeDialog.forEquipment(equipment),
),
),
// Bouton Modifier (permission required)
PermissionGate(
requiredPermissions: const ['manage_equipment'],
child: IconButton(
icon: const Icon(Icons.edit, color: AppColors.rouge),
tooltip: 'Modifier',
onPressed: () => _editEquipment(equipment),
),
),
// Bouton Supprimer (permission required)
PermissionGate(
requiredPermissions: const ['manage_equipment'],
child: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
tooltip: 'Supprimer',
onPressed: () => _deleteEquipment(equipment),
),
),
],
),
onTap: isSelectionMode
? () => toggleItemSelection(equipment.id)
: () => _viewEquipmentDetails(equipment),
),
);
}
Widget _buildQuantityDisplay(EquipmentModel equipment) {
final availableQty = equipment.availableQuantity ?? 0;
final totalQty = equipment.totalQuantity ?? 0;
final criticalThreshold = equipment.criticalThreshold ?? 0;
final isCritical =
criticalThreshold > 0 && availableQty <= criticalThreshold;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isCritical
? Colors.red.withOpacity(0.15)
: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isCritical ? Colors.red : Colors.grey.shade400,
width: isCritical ? 2 : 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isCritical ? Icons.warning : Icons.inventory,
size: 16,
color: isCritical ? Colors.red : Colors.grey[700],
),
const SizedBox(width: 6),
Text(
'Disponible: $availableQty / $totalQty',
style: TextStyle(
fontSize: 13,
fontWeight: isCritical ? FontWeight.bold : FontWeight.normal,
color: isCritical ? Colors.red : Colors.grey[700],
),
),
if (isCritical) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10),
),
child: const Text(
'CRITIQUE',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
],
),
);
}
Widget _buildStatusBadge(EquipmentStatus status) {
final statusInfo = _getStatusInfo(status);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusInfo.$2.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: statusInfo.$2),
),
child: Text(
statusInfo.$1,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: statusInfo.$2,
),
),
);
}
(String, Color) _getStatusInfo(EquipmentStatus status) {
switch (status) {
case EquipmentStatus.available:
return ('Disponible', Colors.green);
case EquipmentStatus.inUse:
return ('En prestation', Colors.blue);
case EquipmentStatus.rented:
return ('Loué', Colors.orange);
case EquipmentStatus.lost:
return ('Perdu', Colors.red);
case EquipmentStatus.outOfService:
return ('HS', Colors.red[900]!);
case EquipmentStatus.maintenance:
return ('Maintenance', Colors.amber);
}
}
Color _getStatusColor(EquipmentStatus status) {
return _getStatusInfo(status).$2;
}
IconData _getCategoryIcon(EquipmentCategory category) {
switch (category) {
case EquipmentCategory.lighting:
return Icons.light_mode;
case EquipmentCategory.sound:
return Icons.volume_up;
case EquipmentCategory.video:
return Icons.videocam;
case EquipmentCategory.effect:
return Icons.auto_awesome;
case EquipmentCategory.structure:
return Icons.construction;
case EquipmentCategory.consumable:
return Icons.inventory_2;
case EquipmentCategory.cable:
return Icons.cable;
case EquipmentCategory.other:
return Icons.more_horiz;
}
}
// Actions
void _createNewEquipment() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const EquipmentFormPage(),
),
);
}
void _editEquipment(EquipmentModel equipment) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EquipmentFormPage(equipment: equipment),
),
);
}
void _deleteEquipment(EquipmentModel equipment) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmer la suppression'),
content: Text('Voulez-vous vraiment supprimer "${equipment.name}" ?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
try {
await context
.read<EquipmentProvider>()
.deleteEquipment(equipment.id);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Équipement supprimé avec succès')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e')),
);
}
}
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Supprimer'),
),
],
),
);
}
void _deleteSelectedEquipment() async {
if (!hasSelection) return;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmer la suppression'),
content: Text(
'Voulez-vous vraiment supprimer $selectedCount équipement(s) ?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
try {
final provider = context.read<EquipmentProvider>();
for (final id in selectedIds) {
await provider.deleteEquipment(id);
}
disableSelectionMode();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'$selectedCount équipement(s) supprimé(s) avec succès'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e')),
);
}
}
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Supprimer'),
),
],
),
);
}
void _generateQRCodesForSelected() async {
if (!hasSelection) return;
// Récupérer les équipements sélectionnés
final provider = context.read<EquipmentProvider>();
final List<EquipmentModel> selectedEquipment = [];
// On doit récupérer les équipements depuis le stream
await for (final equipmentList in provider.equipmentStream.take(1)) {
for (final equipment in equipmentList) {
if (isItemSelected(equipment.id)) {
selectedEquipment.add(equipment);
}
}
break;
}
if (selectedEquipment.isEmpty) return;
if (selectedEquipment.length == 1) {
// Un seul équipement : afficher le dialogue simple
showDialog(
context: context,
builder: (context) => QRCodeDialog.forEquipment(selectedEquipment.first),
);
} else {
// Plusieurs équipements : afficher le sélecteur de format
showDialog(
context: context,
builder: (context) => QRCodeFormatSelectorDialog(
equipmentList: selectedEquipment,
),
);
}
}
void _showRestockDialog(EquipmentModel equipment) {
final TextEditingController quantityController = TextEditingController();
bool addToTotal = false;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: Row(
children: [
const Icon(Icons.add_shopping_cart, color: AppColors.rouge),
const SizedBox(width: 12),
Expanded(
child: Text('Restock - ${equipment.name}'),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Quantités actuelles',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Disponible:'),
Text(
'${equipment.availableQuantity ?? 0}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Total:'),
Text(
'${equipment.totalQuantity ?? 0}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
],
),
),
const SizedBox(height: 20),
TextField(
controller: quantityController,
decoration: const InputDecoration(
labelText: 'Quantité à ajouter/retirer',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.inventory),
hintText: 'Ex: 10 ou -5',
helperText:
'Nombre positif pour ajouter, négatif pour retirer',
),
keyboardType:
const TextInputType.numberWithOptions(signed: true),
autofocus: true,
),
const SizedBox(height: 16),
CheckboxListTile(
title: const Text('Ajouter à la quantité totale'),
subtitle:
const Text('Mettre à jour aussi la quantité totale'),
value: addToTotal,
contentPadding: EdgeInsets.zero,
onChanged: (bool? value) {
setState(() {
addToTotal = value ?? false;
});
},
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () async {
final quantityText = quantityController.text.trim();
if (quantityText.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez entrer une quantité')),
);
return;
}
final quantity = int.tryParse(quantityText);
if (quantity == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Quantité invalide')),
);
return;
}
Navigator.pop(context);
try {
final currentAvailable = equipment.availableQuantity ?? 0;
final currentTotal = equipment.totalQuantity ?? 0;
final newAvailable = currentAvailable + quantity;
final newTotal =
addToTotal ? currentTotal + quantity : currentTotal;
if (newAvailable < 0) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'La quantité disponible ne peut pas être négative')),
);
return;
}
if (newTotal < 0) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'La quantité totale ne peut pas être négative')),
);
return;
}
final updatedData = {
'availableQuantity': newAvailable,
'totalQuantity': newTotal,
'updatedAt': DateTime.now().toIso8601String(),
};
await context.read<EquipmentProvider>().updateEquipment(
equipment.id,
updatedData,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
quantity > 0
? 'Ajout de $quantity unité(s) effectué'
: 'Retrait de ${quantity.abs()} unité(s) effectué',
),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e')),
);
}
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.rouge,
),
child: const Text('Valider',
style: TextStyle(color: Colors.white)),
),
],
);
},
),
);
}
void _viewEquipmentDetails(EquipmentModel equipment) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EquipmentDetailPage(equipment: equipment),
),
);
}
}