feat: calculateur de frais de déplacement - backend et modèles Flutter

- Cloud Function travel.js : autocomplete Google Places + calcul itinéraires
  via Google Routes API avec péages Ulys /legs (precision=6) + /rate
- Modèles : VehicleModel, DepotModel, RouteResultModel + FuelPrices
- Services : VehicleService, TravelService (Firestore CRUD + API calls)
- Gestion des données : 3 nouveaux onglets (Dépôts, Véhicules, Prix carburants)
- Autocomplétion adresse dans le formulaire événement
- Dialog calcul frais : config + carte flutter_map OSM + sélection itinéraire
- Injection option FRAIS_KM dans l'événement à la sélection
- flutter_map 7.0.2 + latlong2 0.9.1 ajoutés
- npm: csv-parser + @mapbox/polyline installés dans functions
This commit is contained in:
ElPoyo
2026-06-04 14:28:22 +02:00
parent bc93f3fa9a
commit e14b333a67
22 changed files with 3940 additions and 24 deletions
+49
View File
@@ -0,0 +1,49 @@
import 'package:cloud_firestore/cloud_firestore.dart';
class DepotModel {
final String id;
final String name;
final String address;
final DateTime? createdAt;
const DepotModel({
required this.id,
required this.name,
required this.address,
this.createdAt,
});
factory DepotModel.fromMap(Map<String, dynamic> map, String id) {
return DepotModel(
id: id,
name: (map['name'] ?? '').toString(),
address: (map['address'] ?? '').toString(),
createdAt: map['createdAt'] is Timestamp
? (map['createdAt'] as Timestamp).toDate()
: null,
);
}
factory DepotModel.fromFirestore(DocumentSnapshot doc) {
return DepotModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
}
Map<String, dynamic> toMap() {
return {
'name': name,
'address': address,
'createdAt': createdAt != null
? Timestamp.fromDate(createdAt!)
: FieldValue.serverTimestamp(),
};
}
DepotModel copyWith({String? id, String? name, String? address}) {
return DepotModel(
id: id ?? this.id,
name: name ?? this.name,
address: address ?? this.address,
createdAt: createdAt,
);
}
}
+158
View File
@@ -0,0 +1,158 @@
/// Résultat d'un itinéraire calculé par Google Maps + Ulys.
class RouteResult {
/// 'TOLL' ou 'TOLL_FREE'
final String routeType;
final int distanceMeters;
final int durationSeconds;
final String encodedPolyline;
final double tollCost;
const RouteResult({
required this.routeType,
required this.distanceMeters,
required this.durationSeconds,
required this.encodedPolyline,
required this.tollCost,
});
factory RouteResult.fromMap(Map<String, dynamic> map) {
return RouteResult(
routeType: (map['routeType'] ?? 'TOLL').toString(),
distanceMeters: _parseInt(map['distanceMeters'] ?? 0),
durationSeconds: _parseInt(map['durationSeconds'] ?? 0),
encodedPolyline: (map['encodedPolyline'] ?? '').toString(),
tollCost: _parseDouble(map['tollCost'] ?? 0),
);
}
bool get isTollFree => routeType == 'TOLL_FREE';
double get distanceKm => distanceMeters / 1000.0;
double get durationMinutes => durationSeconds / 60.0;
double get durationHours => durationSeconds / 3600.0;
/// Calcule le coût carburant.
/// [consumptionPer100km] : L/100km (ou kWh/100km si électrique)
/// [fuelPricePerLiter] : €/L ou €/kWh
/// [freeZoneKm] : km gratuits à déduire (zone de gratuité)
double fuelCost({
required double consumptionPer100km,
required double fuelPricePerLiter,
double freeZoneKm = 0,
}) {
final effectiveKm = (distanceKm - freeZoneKm).clamp(0, double.infinity);
return (effectiveKm / 100.0) * consumptionPer100km * fuelPricePerLiter;
}
/// Calcule le coût de maintenance.
double maintenanceCost({
required double costPerKm,
double freeZoneKm = 0,
}) {
final effectiveKm = (distanceKm - freeZoneKm).clamp(0, double.infinity);
return effectiveKm * costPerKm;
}
/// Calcule le coût de main-d'œuvre (techniciens).
/// [freeZoneMinutes] : minutes gratuites à déduire (zone de gratuité)
double laborCost({
required int nbTechnicians,
required double hourlyRate,
double freeZoneMinutes = 0,
}) {
final effectiveMinutes =
(durationMinutes - freeZoneMinutes).clamp(0, double.infinity);
return (effectiveMinutes / 60.0) * nbTechnicians * hourlyRate;
}
/// Calcule le coût total pour un aller simple.
double totalCost({
required double consumptionPer100km,
required double fuelPricePerLiter,
required double maintenanceCostPerKm,
required int nbTechnicians,
required double hourlyRate,
bool applyFreeZone = false,
}) {
const freeKm = 20.0;
const freeMinutes = 20.0;
return fuelCost(
consumptionPer100km: consumptionPer100km,
fuelPricePerLiter: fuelPricePerLiter,
freeZoneKm: applyFreeZone ? freeKm : 0,
) +
maintenanceCost(
costPerKm: maintenanceCostPerKm,
freeZoneKm: applyFreeZone ? freeKm : 0,
) +
laborCost(
nbTechnicians: nbTechnicians,
hourlyRate: hourlyRate,
freeZoneMinutes: applyFreeZone ? freeMinutes : 0,
) +
tollCost;
}
static double _parseDouble(dynamic v) {
if (v is double) return v;
if (v is int) return v.toDouble();
if (v is String) return double.tryParse(v) ?? 0.0;
return 0.0;
}
static int _parseInt(dynamic v) {
if (v is int) return v;
if (v is double) return v.toInt();
if (v is String) return int.tryParse(v) ?? 0;
return 0;
}
}
/// Prix des carburants (stocké dans Firestore app_config/fuel_prices)
class FuelPrices {
final double diesel; // €/L
final double essence; // €/L
final double electricite; // €/kWh
const FuelPrices({
this.diesel = 1.60,
this.essence = 1.75,
this.electricite = 0.22,
});
factory FuelPrices.fromMap(Map<String, dynamic> map) {
return FuelPrices(
diesel: _parseDouble(map['diesel'] ?? 1.60),
essence: _parseDouble(map['essence'] ?? 1.75),
electricite: _parseDouble(map['electricite'] ?? 0.22),
);
}
Map<String, dynamic> toMap() => {
'diesel': diesel,
'essence': essence,
'electricite': electricite,
};
double priceForFuelType(String fuelType) {
switch (fuelType.toLowerCase()) {
case 'diesel':
return diesel;
case 'essence':
return essence;
case 'electrique':
case 'électrique':
return electricite;
default:
return diesel;
}
}
static double _parseDouble(dynamic v) {
if (v is double) return v;
if (v is int) return v.toDouble();
if (v is String) return double.tryParse(v) ?? 0.0;
return 0.0;
}
}
+94
View File
@@ -0,0 +1,94 @@
import 'package:cloud_firestore/cloud_firestore.dart';
class VehicleModel {
final String id;
final String name;
final double consumptionPer100km; // L/100km (ou kWh/100km si électrique)
final String fuelType; // 'Diesel', 'Essence', 'Electrique'
final double maintenanceCostPerKm; // €/km
final int tollCategoryId; // 1 à 5 (catégorie Ulys)
final DateTime? createdAt;
const VehicleModel({
required this.id,
required this.name,
required this.consumptionPer100km,
required this.fuelType,
required this.maintenanceCostPerKm,
required this.tollCategoryId,
this.createdAt,
});
factory VehicleModel.fromMap(Map<String, dynamic> map, String id) {
return VehicleModel(
id: id,
name: (map['name'] ?? '').toString(),
consumptionPer100km: _parseDouble(map['consumptionPer100km'] ?? 0),
fuelType: (map['fuelType'] ?? 'Diesel').toString(),
maintenanceCostPerKm: _parseDouble(map['maintenanceCostPerKm'] ?? 0),
tollCategoryId: _parseInt(map['tollCategoryId'] ?? 2),
createdAt: map['createdAt'] is Timestamp
? (map['createdAt'] as Timestamp).toDate()
: null,
);
}
factory VehicleModel.fromFirestore(DocumentSnapshot doc) {
return VehicleModel.fromMap(
doc.data() as Map<String, dynamic>,
doc.id,
);
}
Map<String, dynamic> toMap() {
return {
'name': name,
'consumptionPer100km': consumptionPer100km,
'fuelType': fuelType,
'maintenanceCostPerKm': maintenanceCostPerKm,
'tollCategoryId': tollCategoryId,
'createdAt': createdAt != null
? Timestamp.fromDate(createdAt!)
: FieldValue.serverTimestamp(),
};
}
VehicleModel copyWith({
String? id,
String? name,
double? consumptionPer100km,
String? fuelType,
double? maintenanceCostPerKm,
int? tollCategoryId,
}) {
return VehicleModel(
id: id ?? this.id,
name: name ?? this.name,
consumptionPer100km: consumptionPer100km ?? this.consumptionPer100km,
fuelType: fuelType ?? this.fuelType,
maintenanceCostPerKm: maintenanceCostPerKm ?? this.maintenanceCostPerKm,
tollCategoryId: tollCategoryId ?? this.tollCategoryId,
createdAt: createdAt,
);
}
/// Label lisible pour l'unité de consommation
String get consumptionUnit {
if (fuelType == 'Electrique') return 'kWh/100km';
return 'L/100km';
}
static double _parseDouble(dynamic v) {
if (v is double) return v;
if (v is int) return v.toDouble();
if (v is String) return double.tryParse(v) ?? 0.0;
return 0.0;
}
static int _parseInt(dynamic v) {
if (v is int) return v;
if (v is double) return v.toInt();
if (v is String) return int.tryParse(v) ?? 2;
return 2;
}
}