382 lines
15 KiB
Dart
382 lines
15 KiB
Dart
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),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|