Files
EM2_ERP/em2rp/lib/views/widgets/data_management/vehicles_management.dart
T

382 lines
15 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:em2rp/models/vehicle_model.dart';
import 'package:em2rp/services/vehicle_service.dart';
import 'package:em2rp/utils/colors.dart';
class VehiclesManagement extends StatefulWidget {
const VehiclesManagement({super.key});
@override
State<VehiclesManagement> createState() => _VehiclesManagementState();
}
class _VehiclesManagementState extends State<VehiclesManagement> {
final _service = VehicleService();
bool _isLoading = false;
static const _fuelTypes = ['Diesel', 'Essence', 'Electrique'];
void _showVehicleDialog({VehicleModel? vehicle}) {
final formKey = GlobalKey<FormState>();
final nameCtrl =
TextEditingController(text: vehicle?.name ?? '');
final consoCtrl = TextEditingController(
text: vehicle?.consumptionPer100km.toString() ?? '');
final maintCtrl = TextEditingController(
text: vehicle?.maintenanceCostPerKm.toString() ?? '');
String fuelType = vehicle?.fuelType ?? 'Diesel';
int tollCategory = vehicle?.tollCategoryId ?? 2;
showDialog(
context: context,
builder: (ctx) => StatefulBuilder(builder: (ctx, setDlg) {
return AlertDialog(
title: Text(vehicle == null ? 'Ajouter un véhicule' : 'Modifier le véhicule'),
content: SizedBox(
width: 480,
child: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Nom
TextFormField(
controller: nameCtrl,
decoration: const InputDecoration(
labelText: 'Nom du véhicule *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.directions_car_outlined),
hintText: 'ex: Renault Master',
),
validator: (v) =>
v == null || v.trim().isEmpty ? 'Requis' : null,
),
const SizedBox(height: 14),
// Type de carburant
DropdownButtonFormField<String>(
value: fuelType,
decoration: const InputDecoration(
labelText: 'Type de carburant *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.local_gas_station_outlined),
),
items: _fuelTypes
.map((f) => DropdownMenuItem(value: f, child: Text(f)))
.toList(),
onChanged: (v) => setDlg(() => fuelType = v!),
),
const SizedBox(height: 14),
// Consommation
TextFormField(
controller: consoCtrl,
decoration: InputDecoration(
labelText: fuelType == 'Electrique'
? 'Consommation (kWh/100km) *'
: 'Consommation (L/100km) *',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.speed_outlined),
),
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'^\d+\.?\d{0,2}'))
],
validator: (v) {
if (v == null || v.isEmpty) return 'Requis';
if (double.tryParse(v) == null) return 'Nombre invalide';
return null;
},
),
const SizedBox(height: 14),
// Coût maintenance
TextFormField(
controller: maintCtrl,
decoration: const InputDecoration(
labelText: 'Coût maintenance (€/km) *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.build_outlined),
hintText: 'ex: 0.08',
),
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'^\d+\.?\d{0,3}'))
],
validator: (v) {
if (v == null || v.isEmpty) return 'Requis';
if (double.tryParse(v) == null) return 'Nombre invalide';
return null;
},
),
const SizedBox(height: 14),
// Catégorie péage
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Catégorie de péage : $tollCategory',
style: const TextStyle(fontWeight: FontWeight.w500),
),
const SizedBox(height: 4),
Row(
children: List.generate(5, (i) {
final cat = i + 1;
return Expanded(
child: GestureDetector(
onTap: () => setDlg(() => tollCategory = cat),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 3),
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: tollCategory == cat
? AppColors.rouge
: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'$cat',
style: TextStyle(
color: tollCategory == cat
? Colors.white
: Colors.black87,
fontWeight: FontWeight.bold,
),
),
),
),
),
);
}),
),
const SizedBox(height: 4),
Text(
'Classe 1 Véhicules légers\n'
'Classe 2 Véhicules intermédiaires\n'
'Classe 3 Poids lourds, autocars et autres véhicules à 2 essieux\n'
'Classe 4 - Poids lourds et autres véhicules à 3 essieux et plus\n'
'Classe 5 Motos, side-cars et trikes',
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
],
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Annuler'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.rouge,
foregroundColor: Colors.white,
),
onPressed: () async {
if (!formKey.currentState!.validate()) return;
Navigator.pop(ctx);
setState(() => _isLoading = true);
try {
final v = VehicleModel(
id: vehicle?.id ?? '',
name: nameCtrl.text.trim(),
fuelType: fuelType,
consumptionPer100km:
double.parse(consoCtrl.text.trim()),
maintenanceCostPerKm:
double.parse(maintCtrl.text.trim()),
tollCategoryId: tollCategory,
);
if (vehicle == null) {
await _service.addVehicle(v);
} else {
await _service.updateVehicle(v);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: $e'),
backgroundColor: Colors.red),
);
}
} finally {
if (mounted) setState(() => _isLoading = false);
}
},
child: Text(vehicle == null ? 'Ajouter' : 'Enregistrer'),
),
],
);
}),
);
}
Future<void> _delete(VehicleModel v) async {
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Supprimer le véhicule'),
content: Text('Supprimer "${v.name}" ?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Annuler')),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, foregroundColor: Colors.white),
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Supprimer'),
),
],
),
);
if (confirm == true) {
setState(() => _isLoading = true);
try {
await _service.deleteVehicle(v.id);
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
}
Icon _fuelIcon(String fuelType) {
switch (fuelType) {
case 'Electrique':
return const Icon(Icons.electric_bolt, color: Colors.amber);
case 'Essence':
return const Icon(Icons.local_gas_station, color: Colors.green);
default:
return const Icon(Icons.local_gas_station, color: Colors.orange);
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.directions_car_outlined,
color: AppColors.rouge, size: 28),
const SizedBox(width: 12),
Text(
'Véhicules',
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(fontWeight: FontWeight.bold),
),
const Spacer(),
ElevatedButton.icon(
icon: const Icon(Icons.add),
label: const Text('Ajouter un véhicule'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.rouge,
foregroundColor: Colors.white,
),
onPressed: () => _showVehicleDialog(),
),
],
),
const SizedBox(height: 8),
Text(
'Paramétrez la flotte de véhicules pour le calcul automatique des frais de déplacement.',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.grey[600]),
),
const SizedBox(height: 24),
if (_isLoading) const LinearProgressIndicator(),
Expanded(
child: StreamBuilder<List<VehicleModel>>(
stream: _service.watchVehicles(),
builder: (context, snap) {
if (snap.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
final vehicles = snap.data ?? [];
if (vehicles.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.directions_car_outlined,
size: 64, color: Colors.grey[300]),
const SizedBox(height: 16),
Text(
'Aucun véhicule',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(color: Colors.grey[500]),
),
const SizedBox(height: 8),
const Text('Ajoutez des véhicules pour commencer.'),
],
),
);
}
return ListView.separated(
itemCount: vehicles.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (ctx, i) {
final v = vehicles[i];
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.grey[100],
child: _fuelIcon(v.fuelType),
),
title: Text(v.name,
style:
const TextStyle(fontWeight: FontWeight.w600)),
subtitle: Text(
'${v.consumptionPer100km} ${v.consumptionUnit} • Maint. ${v.maintenanceCostPerKm} €/km • Classe péage ${v.tollCategoryId}',
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Chip(
label: Text(v.fuelType),
backgroundColor: v.fuelType == 'Electrique'
? Colors.amber[50]
: v.fuelType == 'Essence'
? Colors.green[50]
: Colors.orange[50],
side: BorderSide.none,
),
IconButton(
icon: const Icon(Icons.edit_outlined),
tooltip: 'Modifier',
onPressed: () => _showVehicleDialog(vehicle: v),
),
IconButton(
icon: const Icon(Icons.delete_outline,
color: Colors.red),
tooltip: 'Supprimer',
onPressed: () => _delete(v),
),
],
),
);
},
);
},
),
),
],
),
);
}
}