Files
EM2_ERP/em2rp/lib/views/equipment_management_page.dart
ElPoyo b79791ff7a refactor: Ajout des sous-catégories et refonte de la gestion de l'appartenance
Cette mise à jour structurelle améliore la classification des équipements en introduisant la notion de sous-catégories et supprime la gestion directe de l'appartenance d'un équipement à une boîte (`parentBoxIds`). L'appartenance est désormais uniquement définie côté conteneur. Une nouvelle catégorie "Régie / Backline" est également ajoutée.

**Changements majeurs :**

-   **Suppression de `parentBoxIds` sur `EquipmentModel` :**
    -   Le champ `parentBoxIds` a été retiré du modèle de données `EquipmentModel` et de toutes les logiques associées (création, mise à jour, copie).
    -   La responsabilité de lier un équipement à un conteneur est désormais exclusivement gérée par le `ContainerModel` via sa liste `equipmentIds`.
    -   La logique de synchronisation complexe dans `EquipmentFormPage` qui mettait à jour les conteneurs lors de la modification d'un équipement a été entièrement supprimée, simplifiant considérablement le code.
    -   Le sélecteur de boîtes parentes (`ParentBoxesSelector`) a été retiré du formulaire d'équipement.

-   **Ajout des sous-catégories :**
    -   Un champ optionnel `subCategory` (String) a été ajouté au `EquipmentModel`.
    -   Le formulaire de création/modification d'équipement inclut désormais un nouveau champ "Sous-catégorie" avec autocomplétion.
    -   Ce champ est contextuel : il propose des suggestions basées sur les sous-catégories existantes pour la catégorie principale sélectionnée (ex: "Console", "Micro" pour la catégorie "Son").
    -   La sous-catégorie est maintenant affichée sur les fiches de détail des équipements et dans les listes de la page de gestion, améliorant la visibilité du classement.

**Nouvelle catégorie d'équipement :**

-   Une nouvelle catégorie `backline` ("Régie / Backline") a été ajoutée à `EquipmentCategory` avec une icône (`Icons.piano`) et une couleur associée.

**Refactorisation et nettoyage :**

-   Le `EquipmentProvider` et `EquipmentService` ont été mis à jour pour charger et filtrer les sous-catégories.
-   De nombreuses instanciations d'un `EquipmentModel` vide (`dummy`) à travers l'application ont été nettoyées pour retirer la référence à `parentBoxIds`.

-   **Version de l'application :**
    -   La version a été incrémentée à `1.0.4`.
2026-01-17 12:07:20 +01:00

1194 lines
41 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/providers/container_provider.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/views/equipment_form_page.dart';
import 'package:em2rp/views/equipment_detail_page.dart';
import 'package:em2rp/views/container_detail_page.dart';
import 'package:em2rp/views/widgets/common/qr_code_dialog.dart';
import 'package:em2rp/views/widgets/common/qr_code_scanner_dialog.dart';
import 'package:em2rp/views/widgets/common/qr_code_format_selector_dialog.dart';
import 'package:em2rp/views/widgets/equipment/equipment_status_badge.dart';
import 'package:em2rp/utils/debug_log.dart';
import 'package:em2rp/mixins/selection_mode_mixin.dart';
import 'package:em2rp/views/widgets/management/management_list.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;
List<EquipmentModel>? _cachedEquipment;
@override
void initState() {
super.initState();
DebugLog.info('[EquipmentManagementPage] initState called');
// Charger les équipements au démarrage
WidgetsBinding.instance.addPostFrameCallback((_) {
DebugLog.info('[EquipmentManagementPage] Loading equipments...');
context.read<EquipmentProvider>().loadEquipments();
});
}
@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 Scanner QR
IconButton.filled(
onPressed: _scanQRCode,
icon: const Icon(Icons.qr_code_scanner),
tooltip: 'Scanner un QR Code',
style: IconButton.styleFrom(
backgroundColor: Colors.grey[700],
foregroundColor: Colors.white,
),
),
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),
),
),
),
// Bouton Scanner QR
Padding(
padding: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 16.0),
child: ElevatedButton.icon(
onPressed: _scanQRCode,
icon: const Icon(Icons.qr_code_scanner, color: Colors.white),
label: const Text(
'Scanner QR Code',
style: TextStyle(color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[700],
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);
},
),
),
// 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() {
return EquipmentCategory.values.map((category) {
final isSelected = _selectedCategory == category;
final color = isSelected ? Colors.white : AppColors.rouge;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: ChoiceChip(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
category.getIcon(
size: 16,
color: color,
),
const SizedBox(width: 8),
Text(category.label),
],
),
selected: isSelected,
onSelected: (selected) {
if (selected) {
setState(() => _selectedCategory = category);
context.read<EquipmentProvider>().setSelectedCategory(category);
}
},
selectedColor: AppColors.rouge,
labelStyle: TextStyle(
color: color,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
);
}).toList();
}
List<Widget> _buildCategoryListTiles() {
return EquipmentCategory.values.map((category) {
final isSelected = _selectedCategory == category;
final color = isSelected ? AppColors.rouge : Colors.grey[600]!;
return ListTile(
leading: category.getIcon(
size: 24,
color: color,
),
title: Text(
category.label,
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 = category);
context.read<EquipmentProvider>().setSelectedCategory(category);
},
);
}).toList();
}
Widget _buildEquipmentList() {
return Consumer<EquipmentProvider>(
builder: (context, provider, child) {
DebugLog.info('[EquipmentManagementPage] Building list - isLoading: ${provider.isLoading}, equipment count: ${provider.equipment.length}');
if (provider.isLoading && _cachedEquipment == null) {
DebugLog.info('[EquipmentManagementPage] Showing loading indicator');
return const Center(child: CircularProgressIndicator());
}
final equipments = provider.equipment;
if (equipments.isEmpty && !provider.isLoading) {
DebugLog.info('[EquipmentManagementPage] No equipment found');
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],
),
),
],
),
);
}
DebugLog.info('[EquipmentManagementPage] Building list with ${equipments.length} items');
return ListView.builder(
itemCount: equipments.length,
itemBuilder: (context, index) {
return _buildEquipmentCard(equipments[index]);
},
);
},
);
}
Widget _buildEquipmentCard(EquipmentModel equipment) {
final isSelected = isItemSelected(equipment.id);
return Card(
margin: const EdgeInsets.only(bottom: 12),
color: isSelectionMode && isSelected
? AppColors.rouge.withValues(alpha: 0.1)
: null,
child: ListTile(
leading: isSelectionMode
? Checkbox(
value: isSelected,
onChanged: (value) => toggleItemSelection(equipment.id),
activeColor: AppColors.rouge,
)
: CircleAvatar(
backgroundColor: equipment.category.color.withValues(alpha: 0.2),
child: equipment.category.getIcon(
size: 20,
color: equipment.category.color,
),
),
title: Row(
children: [
Expanded(
child: Text(
equipment.id,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
// Afficher le badge de statut calculé dynamiquement
if (equipment.category != EquipmentCategory.consumable &&
equipment.category != EquipmentCategory.cable)
EquipmentStatusBadge(equipment: equipment),
],
),
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 sous-catégorie si elle existe
if (equipment.subCategory != null && equipment.subCategory!.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
'📁 ${equipment.subCategory}',
style: TextStyle(
color: Colors.grey[500],
fontSize: 12,
fontStyle: FontStyle.italic,
),
),
],
// 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,
),
),
),
],
],
),
);
}
// 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;
// Afficher un indicateur de chargement
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(
child: CircularProgressIndicator(color: AppColors.rouge),
),
);
try {
// 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;
}
// Fermer l'indicateur de chargement
if (mounted) {
Navigator.of(context).pop();
}
if (selectedEquipment.isEmpty) return;
if (selectedEquipment.length == 1) {
// Un seul équipement : afficher le dialogue simple
if (mounted) {
showDialog(
context: context,
builder: (context) => QRCodeDialog.forEquipment(selectedEquipment.first),
);
}
} else {
// Plusieurs équipements : afficher le sélecteur de format
if (mounted) {
showDialog(
context: context,
builder: (context) => QRCodeFormatSelectorDialog<EquipmentModel>(
itemList: selectedEquipment,
getId: (eq) => eq.id,
getTitle: (eq) => '${eq.brand ?? ''} ${eq.model ?? ''}'.trim(),
dialogTitle: 'Générer ${selectedEquipment.length} QR Code(s)',
),
);
}
}
} catch (e) {
// Fermer l'indicateur si une erreur survient
if (mounted) {
Navigator.of(context).pop();
}
DebugLog.error('[EquipmentManagementPage] Error generating QR codes', e);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la génération : ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
}
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(),
};
final updatedEquipment = equipment.copyWith(
availableQuantity: newAvailable,
totalQuantity: newTotal,
updatedAt: DateTime.now(),
);
await context.read<EquipmentProvider>().updateEquipment(updatedEquipment);
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),
),
);
}
/// Scanner un QR Code et ouvrir la vue de détail correspondante
Future<void> _scanQRCode() async {
try {
// Ouvrir le scanner
final scannedCode = await showDialog<String>(
context: context,
builder: (context) => const QRCodeScannerDialog(),
);
if (scannedCode == null || scannedCode.isEmpty) {
return; // L'utilisateur a annulé
}
if (!mounted) return;
// Afficher un indicateur de chargement
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(
child: CircularProgressIndicator(color: AppColors.rouge),
),
);
// Rechercher d'abord dans les équipements
final equipmentProvider = context.read<EquipmentProvider>();
await equipmentProvider.ensureLoaded();
final equipment = equipmentProvider.allEquipment.firstWhere(
(eq) => eq.id == scannedCode,
orElse: () => EquipmentModel(
id: '',
name: '',
category: EquipmentCategory.other,
status: EquipmentStatus.available,
maintenanceIds: [],
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
);
if (mounted) {
Navigator.of(context).pop(); // Fermer l'indicateur
}
if (equipment.id.isNotEmpty) {
// Équipement trouvé
if (mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EquipmentDetailPage(equipment: equipment),
),
);
}
return;
}
// Si pas trouvé dans les équipements, chercher dans les conteneurs
final containerProvider = context.read<ContainerProvider>();
if (containerProvider.containers.isEmpty) {
await containerProvider.loadContainers();
}
final container = containerProvider.containers.firstWhere(
(c) => c.id == scannedCode,
orElse: () => ContainerModel(
id: '',
name: '',
type: ContainerType.flightCase,
status: EquipmentStatus.available,
equipmentIds: [],
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
);
if (container.id.isNotEmpty) {
// Conteneur trouvé
if (mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ContainerDetailPage(container: container),
),
);
}
return;
}
// Rien trouvé
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Aucun équipement ou conteneur trouvé avec l\'ID : $scannedCode'),
backgroundColor: Colors.orange,
),
);
}
} catch (e) {
DebugLog.error('[EquipmentManagementPage] Error scanning QR code', e);
if (mounted) {
// Fermer l'indicateur si ouvert
Navigator.of(context).popUntil((route) => route.isFirst);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors du scan : ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
}
}