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
@@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:em2rp/models/route_result_model.dart';
/// Affiche 1 ou 2 itinéraires sur une carte OpenStreetMap.
/// Route TOLL = bleu, Route TOLL_FREE = vert.
class RouteMapWidget extends StatelessWidget {
final List<RouteResult> routes;
final RouteResult? selectedRoute;
const RouteMapWidget({
super.key,
required this.routes,
this.selectedRoute,
});
/// Décode une polyline Google encodée en liste de LatLng.
List<LatLng> _decode(String encoded) {
if (encoded.isEmpty) return [];
try {
final result = <LatLng>[];
int index = 0, lat = 0, lng = 0;
final len = encoded.length;
while (index < len) {
int shift = 0, result0 = 0;
int b;
do {
b = encoded.codeUnitAt(index++) - 63;
result0 |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
final dlat = (result0 & 1) != 0 ? ~(result0 >> 1) : (result0 >> 1);
lat += dlat;
shift = 0;
result0 = 0;
do {
b = encoded.codeUnitAt(index++) - 63;
result0 |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
final dlng = (result0 & 1) != 0 ? ~(result0 >> 1) : (result0 >> 1);
lng += dlng;
result.add(LatLng(lat / 1e5, lng / 1e5));
}
return result;
} catch (e) {
return [];
}
}
LatLngBounds? _computeBounds(List<List<LatLng>> allPoints) {
double? minLat, maxLat, minLng, maxLng;
for (final pts in allPoints) {
for (final p in pts) {
minLat = minLat == null ? p.latitude : p.latitude < minLat ? p.latitude : minLat;
maxLat = maxLat == null ? p.latitude : p.latitude > maxLat ? p.latitude : maxLat;
minLng = minLng == null ? p.longitude : p.longitude < minLng ? p.longitude : minLng;
maxLng = maxLng == null ? p.longitude : p.longitude > maxLng ? p.longitude : maxLng;
}
}
if (minLat == null) return null;
return LatLngBounds(
LatLng(minLat - 0.02, minLng! - 0.02),
LatLng(maxLat! + 0.02, maxLng! + 0.02),
);
}
@override
Widget build(BuildContext context) {
final allPolylines = <Polyline>[];
final allPointGroups = <List<LatLng>>[];
for (final route in routes) {
final pts = _decode(route.encodedPolyline);
if (pts.isEmpty) continue;
allPointGroups.add(pts);
final isSelected =
selectedRoute == null || selectedRoute!.routeType == route.routeType;
final isToll = route.routeType == 'TOLL';
allPolylines.add(Polyline(
points: pts,
strokeWidth: isSelected ? 5.0 : 3.0,
color: isToll
? (isSelected
? const Color(0xFF1565C0)
: const Color(0xFF1565C0).withValues(alpha: 0.4))
: (isSelected
? const Color(0xFF2E7D32)
: const Color(0xFF2E7D32).withValues(alpha: 0.4)),
));
}
final bounds = _computeBounds(allPointGroups);
final mapController = MapController();
// Marqueurs de départ / arrivée
final markers = <Marker>[];
for (final group in allPointGroups) {
if (group.isEmpty) continue;
// Départ
markers.add(Marker(
point: group.first,
width: 32,
height: 32,
child: const Icon(Icons.circle, color: Colors.green, size: 20),
));
// Arrivée
markers.add(Marker(
point: group.last,
width: 32,
height: 32,
child: const Icon(Icons.location_pin, color: Colors.red, size: 32),
));
}
return FlutterMap(
mapController: mapController,
options: MapOptions(
initialCameraFit: bounds != null
? CameraFit.bounds(
bounds: bounds,
padding: const EdgeInsets.all(32),
)
: const CameraFit.coordinates(
coordinates: [LatLng(46.2276, 2.2137)], padding: EdgeInsets.all(32)),
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.all,
),
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.em2events.em2rp',
),
PolylineLayer(polylines: allPolylines),
MarkerLayer(markers: markers),
],
);
}
}