feat: updated container management system with core models, providers, and UI pages

This commit is contained in:
ElPoyo
2026-05-26 21:34:35 +02:00
parent fb740d97a3
commit 64a9fe382a
13 changed files with 1363 additions and 797 deletions
+404 -336
View File
@@ -10,6 +10,7 @@ import 'package:em2rp/utils/id_generator.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
import 'package:em2rp/utils/debouncer.dart';
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
class ContainerFormPage extends StatefulWidget {
final ContainerModel? container;
@@ -90,369 +91,436 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
}
}
Widget _buildCard({
required String title,
required IconData icon,
required List<Widget> children,
}) {
return Card(
elevation: 2,
shadowColor: Colors.black12,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.grey.shade200, width: 1),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Icon(icon, color: AppColors.rouge, size: 20),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColors.noir,
),
),
],
),
const Divider(height: 24, thickness: 1),
...children,
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_isEditing ? 'Modifier boite' : 'Nouvelle boite'),
backgroundColor: AppColors.rouge,
foregroundColor: Colors.white,
appBar: CustomAppBar(
title: _isEditing ? 'Modifier boîte' : 'Nouvelle boîte',
),
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
ValueListenableBuilder<bool>(
valueListenable: _autoGenerateIdNotifier,
builder: (context, autoGenerateId, child) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: TextFormField(
controller: _idController,
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 800),
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Card 1: Informations Générales
_buildCard(
title: 'Informations générales',
icon: Icons.info_outline,
children: [
// Nom
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Identifiant *',
hintText: 'ex: FLIGHTCASE_BEAM',
labelText: 'Nom du container *',
hintText: 'ex: Flight Case Beam 7R',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.qr_code),
prefixIcon: Icon(Icons.label),
),
enabled: !autoGenerateId || _isEditing,
onChanged: (_) => _updateIdFromName(),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un identifiant';
return 'Veuillez entrer un nom';
}
final validation = IdGenerator.validateContainerId(value);
return validation;
return null;
},
),
),
if (!_isEditing) ...[
const SizedBox(width: 8),
IconButton(
icon: Icon(
autoGenerateId ? Icons.lock : Icons.lock_open,
color: autoGenerateId ? AppColors.rouge : Colors.grey,
const SizedBox(height: 16),
// ID
ValueListenableBuilder<bool>(
valueListenable: _autoGenerateIdNotifier,
builder: (context, autoGenerateId, child) {
return 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: () {
_autoGenerateIdNotifier.value = !autoGenerateId;
if (_autoGenerateIdNotifier.value) {
_updateIdFromName();
}
},
),
],
],
);
},
),
const SizedBox(height: 16),
// Type
DropdownButtonFormField<ContainerType>(
initialValue: _selectedType,
decoration: const InputDecoration(
labelText: 'Type de container *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.category),
),
tooltip: autoGenerateId
? 'Génération automatique'
: 'Saisie manuelle',
onPressed: () {
_autoGenerateIdNotifier.value = !autoGenerateId;
if (_autoGenerateIdNotifier.value) {
_updateIdFromName();
items: ContainerType.values.map((type) {
return DropdownMenuItem(
value: type,
child: Row(
children: [
type.getIcon(size: 20, color: AppColors.rouge),
const SizedBox(width: 8),
Text(type.label),
],
),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedType = value;
_updateIdFromType();
});
}
},
),
const SizedBox(height: 16),
// Statut
DropdownButtonFormField<EquipmentStatus>(
initialValue: _selectedStatus,
decoration: const InputDecoration(
labelText: 'Statut *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.info_outline),
),
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: 16),
// Type
DropdownButtonFormField<ContainerType>(
initialValue: _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>(
initialValue: _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),
const SizedBox(height: 20),
// 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),
// Card 2: Caractéristiques Physiques
_buildCard(
title: 'Caractéristiques physiques',
icon: Icons.scale_outlined,
children: [
// 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;
},
// Dimensions
Row(
children: [
Expanded(
child: TextFormField(
controller: _lengthController,
decoration: const InputDecoration(
labelText: 'Longueur (cm)',
border: OutlineInputBorder(),
),
keyboardType: const 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: const 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: const 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),
const SizedBox(height: 20),
// Section Équipements
Text(
'Équipements dans ce container',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Divider(),
const SizedBox(height: 16),
// Card 3: Équipements dans ce container
_buildCard(
title: 'Équipements dans ce container',
icon: Icons.inventory_2_outlined,
children: [
if (_selectedEquipmentIds.isNotEmpty)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade200),
borderRadius: BorderRadius.circular(8),
color: Colors.grey.shade50,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${_selectedEquipmentIds.length} équipement(s) sélectionné(s) :',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _selectedEquipmentIds.map((id) {
return Chip(
label: Text(
id,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
),
deleteIcon: const Icon(Icons.close, size: 16),
onDeleted: () {
setState(() {
_selectedEquipmentIds.remove(id);
});
},
backgroundColor: AppColors.rouge.withValues(alpha: 0.08),
side: BorderSide(color: AppColors.rouge.withValues(alpha: 0.2)),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}).toList(),
),
],
),
)
else
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade200),
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: 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),
// 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),
side: BorderSide(color: AppColors.rouge),
foregroundColor: AppColors.rouge,
),
),
],
),
),
),
const SizedBox(height: 12),
const SizedBox(height: 20),
// 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),
// Card 4: Notes
_buildCard(
title: 'Notes & Remarques',
icon: Icons.notes_outlined,
children: [
TextFormField(
controller: _notesController,
decoration: const InputDecoration(
labelText: 'Notes complémentaires',
hintText: 'Informations additionnelles...',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.edit_note),
),
maxLines: 3,
),
],
),
const SizedBox(height: 32),
// Actions
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler', style: TextStyle(fontSize: 16)),
),
const SizedBox(width: 16),
ElevatedButton.icon(
onPressed: _saveContainer,
icon: const Icon(Icons.save, color: Colors.white),
label: Text(
_isEditing ? 'Enregistrer' : 'Créer',
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.rouge,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
),
const SizedBox(height: 24),
],
),
),
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,
),
),
),
],
),
],
),
),
),
);