Files
EM2_ERP/em2rp/lib/views/container_form_page.dart
ElPoyo df6d54a007 Refactor: Centralisation des labels et icônes pour les enums
Centralise la gestion des libellés, couleurs et icônes pour `EquipmentStatus`, `EquipmentCategory`, et `ContainerType` en utilisant des extensions Dart.

- Ajout de nouvelles icônes SVG pour `flight-case`, `truss` et `tape`.
- Refactorisation des vues pour utiliser les nouvelles extensions, supprimant ainsi la logique d'affichage dupliquée.
- Mise à jour des `ChoiceChip` et des listes de filtres pour afficher les icônes à côté des labels.
2025-10-29 18:43:24 +01:00

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(type.label),
);
}).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;
}
}
}