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.).
925 lines
31 KiB
Dart
925 lines
31 KiB
Dart
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/providers/equipment_provider.dart';
|
|
import 'package:em2rp/utils/id_generator.dart';
|
|
|
|
class ContainerFormPage extends StatefulWidget {
|
|
final ContainerModel? container;
|
|
|
|
const ContainerFormPage({super.key, this.container});
|
|
|
|
@override
|
|
State<ContainerFormPage> createState() => _ContainerFormPageState();
|
|
}
|
|
|
|
class _ContainerFormPageState extends State<ContainerFormPage> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
|
|
// Controllers
|
|
final _nameController = TextEditingController();
|
|
final _idController = TextEditingController();
|
|
final _weightController = TextEditingController();
|
|
final _lengthController = TextEditingController();
|
|
final _widthController = TextEditingController();
|
|
final _heightController = TextEditingController();
|
|
final _notesController = TextEditingController();
|
|
|
|
// Form fields
|
|
ContainerType _selectedType = ContainerType.flightCase;
|
|
EquipmentStatus _selectedStatus = EquipmentStatus.available;
|
|
bool _autoGenerateId = true;
|
|
final Set<String> _selectedEquipmentIds = {};
|
|
|
|
bool _isEditing = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (widget.container != null) {
|
|
_isEditing = true;
|
|
_loadContainerData();
|
|
}
|
|
}
|
|
|
|
void _loadContainerData() {
|
|
final container = widget.container!;
|
|
_nameController.text = container.name;
|
|
_idController.text = container.id;
|
|
_selectedType = container.type;
|
|
_selectedStatus = container.status;
|
|
_weightController.text = container.weight?.toString() ?? '';
|
|
_lengthController.text = container.length?.toString() ?? '';
|
|
_widthController.text = container.width?.toString() ?? '';
|
|
_heightController.text = container.height?.toString() ?? '';
|
|
_notesController.text = container.notes ?? '';
|
|
_selectedEquipmentIds.addAll(container.equipmentIds);
|
|
_autoGenerateId = false;
|
|
}
|
|
|
|
void _updateIdFromName() {
|
|
if (_autoGenerateId && !_isEditing) {
|
|
final name = _nameController.text;
|
|
if (name.isNotEmpty) {
|
|
final baseId = IdGenerator.generateContainerId(
|
|
type: _selectedType,
|
|
name: name,
|
|
);
|
|
_idController.text = baseId;
|
|
}
|
|
}
|
|
}
|
|
|
|
void _updateIdFromType() {
|
|
if (_autoGenerateId && !_isEditing) {
|
|
final name = _nameController.text;
|
|
if (name.isNotEmpty) {
|
|
final baseId = IdGenerator.generateContainerId(
|
|
type: _selectedType,
|
|
name: name,
|
|
);
|
|
_idController.text = baseId;
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(_isEditing ? 'Modifier Container' : 'Nouveau Container'),
|
|
backgroundColor: AppColors.rouge,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
body: Form(
|
|
key: _formKey,
|
|
child: ListView(
|
|
padding: const EdgeInsets.all(24),
|
|
children: [
|
|
|
|
// Nom
|
|
TextFormField(
|
|
controller: _nameController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Nom du container *',
|
|
hintText: 'ex: Flight Case Beam 7R',
|
|
border: OutlineInputBorder(),
|
|
prefixIcon: Icon(Icons.label),
|
|
),
|
|
onChanged: (_) => _updateIdFromName(),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Veuillez entrer un nom';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// ID
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: TextFormField(
|
|
controller: _idController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Identifiant *',
|
|
hintText: 'ex: FLIGHTCASE_BEAM',
|
|
border: OutlineInputBorder(),
|
|
prefixIcon: Icon(Icons.qr_code),
|
|
),
|
|
enabled: !_autoGenerateId || _isEditing,
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Veuillez entrer un identifiant';
|
|
}
|
|
final validation = IdGenerator.validateContainerId(value);
|
|
return validation;
|
|
},
|
|
),
|
|
),
|
|
if (!_isEditing) ...[
|
|
const SizedBox(width: 8),
|
|
IconButton(
|
|
icon: Icon(
|
|
_autoGenerateId ? Icons.lock : Icons.lock_open,
|
|
color: _autoGenerateId ? AppColors.rouge : Colors.grey,
|
|
),
|
|
tooltip: _autoGenerateId
|
|
? 'Génération automatique'
|
|
: 'Saisie manuelle',
|
|
onPressed: () {
|
|
setState(() {
|
|
_autoGenerateId = !_autoGenerateId;
|
|
if (_autoGenerateId) {
|
|
_updateIdFromName();
|
|
}
|
|
});
|
|
},
|
|
),
|
|
],
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Type
|
|
DropdownButtonFormField<ContainerType>(
|
|
value: _selectedType,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Type de container *',
|
|
border: OutlineInputBorder(),
|
|
prefixIcon: Icon(Icons.category),
|
|
),
|
|
items: ContainerType.values.map((type) {
|
|
return DropdownMenuItem(
|
|
value: type,
|
|
child: Text(containerTypeLabel(type)),
|
|
);
|
|
}).toList(),
|
|
onChanged: (value) {
|
|
if (value != null) {
|
|
setState(() {
|
|
_selectedType = value;
|
|
_updateIdFromType();
|
|
});
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Statut
|
|
DropdownButtonFormField<EquipmentStatus>(
|
|
value: _selectedStatus,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Statut *',
|
|
border: OutlineInputBorder(),
|
|
prefixIcon: Icon(Icons.info),
|
|
),
|
|
items: [
|
|
EquipmentStatus.available,
|
|
EquipmentStatus.inUse,
|
|
EquipmentStatus.maintenance,
|
|
EquipmentStatus.outOfService,
|
|
].map((status) {
|
|
String label;
|
|
switch (status) {
|
|
case EquipmentStatus.available:
|
|
label = 'Disponible';
|
|
break;
|
|
case EquipmentStatus.inUse:
|
|
label = 'En prestation';
|
|
break;
|
|
case EquipmentStatus.maintenance:
|
|
label = 'En maintenance';
|
|
break;
|
|
case EquipmentStatus.outOfService:
|
|
label = 'Hors service';
|
|
break;
|
|
default:
|
|
label = 'Autre';
|
|
}
|
|
return DropdownMenuItem(
|
|
value: status,
|
|
child: Text(label),
|
|
);
|
|
}).toList(),
|
|
onChanged: (value) {
|
|
if (value != null) {
|
|
setState(() {
|
|
_selectedStatus = value;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Section Caractéristiques physiques
|
|
Text(
|
|
'Caractéristiques physiques',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const Divider(),
|
|
const SizedBox(height: 16),
|
|
|
|
// Poids
|
|
TextFormField(
|
|
controller: _weightController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Poids à vide (kg)',
|
|
hintText: 'ex: 15.5',
|
|
border: OutlineInputBorder(),
|
|
prefixIcon: Icon(Icons.scale),
|
|
),
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
validator: (value) {
|
|
if (value != null && value.isNotEmpty) {
|
|
if (double.tryParse(value) == null) {
|
|
return 'Veuillez entrer un nombre valide';
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Dimensions
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextFormField(
|
|
controller: _lengthController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Longueur (cm)',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
|
validator: (value) {
|
|
if (value != null && value.isNotEmpty) {
|
|
if (double.tryParse(value) == null) {
|
|
return 'Nombre invalide';
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: TextFormField(
|
|
controller: _widthController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Largeur (cm)',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
|
validator: (value) {
|
|
if (value != null && value.isNotEmpty) {
|
|
if (double.tryParse(value) == null) {
|
|
return 'Nombre invalide';
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: TextFormField(
|
|
controller: _heightController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Hauteur (cm)',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
|
validator: (value) {
|
|
if (value != null && value.isNotEmpty) {
|
|
if (double.tryParse(value) == null) {
|
|
return 'Nombre invalide';
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Section Équipements
|
|
Text(
|
|
'Équipements dans ce container',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const Divider(),
|
|
const SizedBox(height: 16),
|
|
|
|
// Liste des équipements sélectionnés
|
|
if (_selectedEquipmentIds.isNotEmpty)
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'${_selectedEquipmentIds.length} équipement(s) sélectionné(s)',
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: _selectedEquipmentIds.map((id) {
|
|
return Chip(
|
|
label: Text(id),
|
|
deleteIcon: const Icon(Icons.close, size: 18),
|
|
onDeleted: () {
|
|
setState(() {
|
|
_selectedEquipmentIds.remove(id);
|
|
});
|
|
},
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
else
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(8),
|
|
color: Colors.grey.shade50,
|
|
),
|
|
child: const Center(
|
|
child: Text(
|
|
'Aucun équipement sélectionné',
|
|
style: TextStyle(color: Colors.grey),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Bouton pour ajouter des équipements
|
|
OutlinedButton.icon(
|
|
onPressed: _selectEquipment,
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('Ajouter des équipements'),
|
|
style: OutlinedButton.styleFrom(
|
|
minimumSize: const Size(double.infinity, 48),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Notes
|
|
TextFormField(
|
|
controller: _notesController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Notes',
|
|
hintText: 'Informations additionnelles...',
|
|
border: OutlineInputBorder(),
|
|
prefixIcon: Icon(Icons.notes),
|
|
),
|
|
maxLines: 3,
|
|
),
|
|
const SizedBox(height: 32),
|
|
|
|
// Boutons
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Annuler'),
|
|
),
|
|
const SizedBox(width: 16),
|
|
ElevatedButton.icon(
|
|
onPressed: _saveContainer,
|
|
icon: const Icon(Icons.save, color: Colors.white),
|
|
label: Text(
|
|
_isEditing ? 'Mettre à jour' : 'Créer',
|
|
style: const TextStyle(color: Colors.white),
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.rouge,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 24,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _selectEquipment() async {
|
|
final equipmentProvider = context.read<EquipmentProvider>();
|
|
|
|
await showDialog(
|
|
context: context,
|
|
builder: (context) => _EquipmentSelectorDialog(
|
|
selectedIds: _selectedEquipmentIds,
|
|
equipmentProvider: equipmentProvider,
|
|
),
|
|
);
|
|
|
|
setState(() {});
|
|
}
|
|
|
|
Future<bool> _isIdUnique(String id) async {
|
|
final provider = context.read<ContainerProvider>();
|
|
final container = await provider.getContainerById(id);
|
|
return container == null;
|
|
}
|
|
|
|
Future<void> _saveContainer() async {
|
|
if (!_formKey.currentState!.validate()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final containerProvider = context.read<ContainerProvider>();
|
|
|
|
if (_isEditing) {
|
|
await _updateContainer(containerProvider);
|
|
} else {
|
|
await _createSingleContainer(containerProvider);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Erreur: $e')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _createSingleContainer(ContainerProvider provider) async {
|
|
final baseId = _idController.text.trim();
|
|
|
|
// Vérifier l'unicité de l'ID directement
|
|
String uniqueId = baseId;
|
|
if (!await _isIdUnique(baseId)) {
|
|
uniqueId = '${baseId}_${DateTime.now().millisecondsSinceEpoch}';
|
|
}
|
|
|
|
final container = ContainerModel(
|
|
id: uniqueId,
|
|
name: _nameController.text.trim(),
|
|
type: _selectedType,
|
|
status: _selectedStatus,
|
|
equipmentIds: _selectedEquipmentIds.toList(),
|
|
weight: _weightController.text.isNotEmpty
|
|
? double.tryParse(_weightController.text)
|
|
: null,
|
|
length: _lengthController.text.isNotEmpty
|
|
? double.tryParse(_lengthController.text)
|
|
: null,
|
|
width: _widthController.text.isNotEmpty
|
|
? double.tryParse(_widthController.text)
|
|
: null,
|
|
height: _heightController.text.isNotEmpty
|
|
? double.tryParse(_heightController.text)
|
|
: null,
|
|
notes: _notesController.text.trim().isNotEmpty
|
|
? _notesController.text.trim()
|
|
: null,
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
);
|
|
|
|
await provider.createContainer(container);
|
|
|
|
// Mettre à jour les parentBoxIds des équipements
|
|
for (final equipmentId in _selectedEquipmentIds) {
|
|
try {
|
|
await provider.addEquipmentToContainer(
|
|
containerId: uniqueId,
|
|
equipmentId: equipmentId,
|
|
);
|
|
} catch (e) {
|
|
print('Erreur lors de l\'ajout de l\'équipement $equipmentId: $e');
|
|
}
|
|
}
|
|
|
|
if (mounted) {
|
|
Navigator.pop(context);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Container créé avec succès')),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _updateContainer(ContainerProvider provider) async {
|
|
final container = widget.container!;
|
|
|
|
await provider.updateContainer(container.id, {
|
|
'name': _nameController.text.trim(),
|
|
'type': containerTypeToString(_selectedType),
|
|
'status': equipmentStatusToString(_selectedStatus),
|
|
'equipmentIds': _selectedEquipmentIds.toList(),
|
|
'weight': _weightController.text.isNotEmpty
|
|
? double.tryParse(_weightController.text)
|
|
: null,
|
|
'length': _lengthController.text.isNotEmpty
|
|
? double.tryParse(_lengthController.text)
|
|
: null,
|
|
'width': _widthController.text.isNotEmpty
|
|
? double.tryParse(_widthController.text)
|
|
: null,
|
|
'height': _heightController.text.isNotEmpty
|
|
? double.tryParse(_heightController.text)
|
|
: null,
|
|
'notes': _notesController.text.trim().isNotEmpty
|
|
? _notesController.text.trim()
|
|
: null,
|
|
});
|
|
|
|
// Gérer les équipements ajoutés
|
|
final addedEquipment = _selectedEquipmentIds.difference(container.equipmentIds.toSet());
|
|
for (final equipmentId in addedEquipment) {
|
|
try {
|
|
await provider.addEquipmentToContainer(
|
|
containerId: container.id,
|
|
equipmentId: equipmentId,
|
|
);
|
|
} catch (e) {
|
|
print('Erreur lors de l\'ajout de l\'équipement $equipmentId: $e');
|
|
}
|
|
}
|
|
|
|
// Gérer les équipements retirés
|
|
final removedEquipment = container.equipmentIds.toSet().difference(_selectedEquipmentIds);
|
|
for (final equipmentId in removedEquipment) {
|
|
try {
|
|
await provider.removeEquipmentFromContainer(
|
|
containerId: container.id,
|
|
equipmentId: equipmentId,
|
|
);
|
|
} catch (e) {
|
|
print('Erreur lors du retrait de l\'équipement $equipmentId: $e');
|
|
}
|
|
}
|
|
|
|
if (mounted) {
|
|
Navigator.pop(context);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Container mis à jour avec succès')),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_nameController.dispose();
|
|
_idController.dispose();
|
|
_weightController.dispose();
|
|
_lengthController.dispose();
|
|
_widthController.dispose();
|
|
_heightController.dispose();
|
|
_notesController.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
/// Widget de dialogue pour sélectionner les équipements
|
|
class _EquipmentSelectorDialog extends StatefulWidget {
|
|
final Set<String> selectedIds;
|
|
final EquipmentProvider equipmentProvider;
|
|
|
|
const _EquipmentSelectorDialog({
|
|
required this.selectedIds,
|
|
required this.equipmentProvider,
|
|
});
|
|
|
|
@override
|
|
State<_EquipmentSelectorDialog> createState() => _EquipmentSelectorDialogState();
|
|
}
|
|
|
|
class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
|
final TextEditingController _searchController = TextEditingController();
|
|
EquipmentCategory? _filterCategory;
|
|
String _searchQuery = '';
|
|
|
|
@override
|
|
void dispose() {
|
|
_searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Dialog(
|
|
child: Container(
|
|
width: MediaQuery.of(context).size.width * 0.8,
|
|
height: MediaQuery.of(context).size.height * 0.8,
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
children: [
|
|
// En-tête
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.inventory, color: AppColors.rouge),
|
|
const SizedBox(width: 12),
|
|
const Expanded(
|
|
child: Text(
|
|
'Sélectionner des équipements',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
],
|
|
),
|
|
const Divider(),
|
|
const SizedBox(height: 16),
|
|
|
|
// Barre de recherche
|
|
TextField(
|
|
controller: _searchController,
|
|
decoration: InputDecoration(
|
|
hintText: 'Rechercher un équipement...',
|
|
prefixIcon: const Icon(Icons.search),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
suffixIcon: _searchController.text.isNotEmpty
|
|
? IconButton(
|
|
icon: const Icon(Icons.clear),
|
|
onPressed: () {
|
|
_searchController.clear();
|
|
setState(() {
|
|
_searchQuery = '';
|
|
});
|
|
},
|
|
)
|
|
: null,
|
|
),
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_searchQuery = value;
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Filtres par catégorie
|
|
SizedBox(
|
|
height: 50,
|
|
child: ListView(
|
|
scrollDirection: Axis.horizontal,
|
|
children: [
|
|
ChoiceChip(
|
|
label: const Text('Tout'),
|
|
selected: _filterCategory == null,
|
|
onSelected: (selected) {
|
|
setState(() {
|
|
_filterCategory = null;
|
|
});
|
|
},
|
|
selectedColor: AppColors.rouge,
|
|
labelStyle: TextStyle(
|
|
color: _filterCategory == null ? Colors.white : Colors.black,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
...EquipmentCategory.values.map((category) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(right: 8),
|
|
child: ChoiceChip(
|
|
label: Text(_getCategoryLabel(category)),
|
|
selected: _filterCategory == category,
|
|
onSelected: (selected) {
|
|
setState(() {
|
|
_filterCategory = selected ? category : null;
|
|
});
|
|
},
|
|
selectedColor: AppColors.rouge,
|
|
labelStyle: TextStyle(
|
|
color: _filterCategory == category ? Colors.white : Colors.black,
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Compteur de sélection
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.rouge.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.check_circle, color: AppColors.rouge),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'${widget.selectedIds.length} équipement(s) sélectionné(s)',
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Liste des équipements
|
|
Expanded(
|
|
child: StreamBuilder<List<EquipmentModel>>(
|
|
stream: widget.equipmentProvider.equipmentStream,
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
if (snapshot.hasError) {
|
|
return Center(child: Text('Erreur: ${snapshot.error}'));
|
|
}
|
|
|
|
var equipment = snapshot.data ?? [];
|
|
|
|
// Filtrer par catégorie
|
|
if (_filterCategory != null) {
|
|
equipment = equipment.where((e) => e.category == _filterCategory).toList();
|
|
}
|
|
|
|
// Filtrer par recherche
|
|
if (_searchQuery.isNotEmpty) {
|
|
final query = _searchQuery.toLowerCase();
|
|
equipment = equipment.where((e) {
|
|
return e.id.toLowerCase().contains(query) ||
|
|
(e.brand?.toLowerCase().contains(query) ?? false) ||
|
|
(e.model?.toLowerCase().contains(query) ?? false);
|
|
}).toList();
|
|
}
|
|
|
|
if (equipment.isEmpty) {
|
|
return const Center(
|
|
child: Text('Aucun équipement trouvé'),
|
|
);
|
|
}
|
|
|
|
return ListView.builder(
|
|
itemCount: equipment.length,
|
|
itemBuilder: (context, index) {
|
|
final item = equipment[index];
|
|
final isSelected = widget.selectedIds.contains(item.id);
|
|
|
|
return CheckboxListTile(
|
|
value: isSelected,
|
|
onChanged: (selected) {
|
|
setState(() {
|
|
if (selected == true) {
|
|
widget.selectedIds.add(item.id);
|
|
} else {
|
|
widget.selectedIds.remove(item.id);
|
|
}
|
|
});
|
|
},
|
|
title: Text(
|
|
item.id,
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
subtitle: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (item.brand != null || item.model != null)
|
|
Text('${item.brand ?? ''} ${item.model ?? ''}'),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
_getCategoryLabel(item.category),
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
secondary: Icon(
|
|
_getCategoryIcon(item.category),
|
|
color: AppColors.rouge,
|
|
),
|
|
activeColor: AppColors.rouge,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
|
|
// Boutons d'action
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Annuler'),
|
|
),
|
|
const SizedBox(width: 16),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.rouge,
|
|
),
|
|
child: const Text(
|
|
'Valider',
|
|
style: TextStyle(color: Colors.white),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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';
|
|
}
|
|
}
|
|
|
|
IconData _getCategoryIcon(EquipmentCategory category) {
|
|
switch (category) {
|
|
case EquipmentCategory.lighting:
|
|
return Icons.lightbulb;
|
|
case EquipmentCategory.sound:
|
|
return Icons.speaker;
|
|
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;
|
|
case EquipmentCategory.cable:
|
|
return Icons.cable;
|
|
case EquipmentCategory.other:
|
|
return Icons.category;
|
|
}
|
|
}
|
|
}
|
|
|