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
+134
View File
@@ -0,0 +1,134 @@
import 'dart:convert';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:http/http.dart' as http;
import 'package:em2rp/config/api_config.dart';
import 'package:em2rp/models/depot_model.dart';
import 'package:em2rp/models/route_result_model.dart';
import 'package:em2rp/utils/debug_log.dart';
class TravelService {
final FirebaseFirestore _db = FirebaseFirestore.instance;
// ─── Auth token ───────────────────────────────────────────
Future<String?> _getToken() async {
final user = FirebaseAuth.instance.currentUser;
return await user?.getIdToken();
}
Future<Map<String, String>> _headers() async {
final token = await _getToken();
return {
'Content-Type': 'application/json',
if (token != null) 'Authorization': 'Bearer $token',
};
}
// ─── Autocomplétion d'adresses ────────────────────────────
Future<List<String>> autocompleteAddress(String query) async {
if (query.trim().length < 3) return [];
try {
final headers = await _headers();
final url = Uri.parse('${ApiConfig.baseUrl}/googleMapsAutocomplete');
final response = await http.post(
url,
headers: headers,
body: jsonEncode({'data': {'query': query}}),
);
if (response.statusCode != 200) return [];
final data = jsonDecode(response.body) as Map<String, dynamic>;
final predictions = data['predictions'] as List<dynamic>? ?? [];
return predictions
.map((p) => (p['description'] ?? '').toString())
.where((s) => s.isNotEmpty)
.toList();
} catch (e) {
DebugLog.error('[Travel] autocompleteAddress error', e);
return [];
}
}
// ─── Calcul des itinéraires ───────────────────────────────
Future<List<RouteResult>> computeRoutes({
required String origin,
required String destination,
int vehicleTollCategory = 2,
}) async {
try {
final headers = await _headers();
final url = Uri.parse('${ApiConfig.baseUrl}/googleMapsComputeRoute');
final response = await http.post(
url,
headers: headers,
body: jsonEncode({
'data': {
'origin': origin,
'destination': destination,
'vehicleTollCategory': vehicleTollCategory,
},
}),
);
if (response.statusCode != 200) {
final err = jsonDecode(response.body);
throw Exception('googleMapsComputeRoute: ${err['error']}');
}
final data = jsonDecode(response.body) as Map<String, dynamic>;
final routes = data['routes'] as List<dynamic>? ?? [];
return routes
.map((r) => RouteResult.fromMap(r as Map<String, dynamic>))
.toList();
} catch (e) {
DebugLog.error('[Travel] computeRoutes error', e);
rethrow;
}
}
// ─── Prix des carburants ───────────────────────────────────
Future<FuelPrices> getFuelPrices() async {
try {
final doc = await _db.collection('app_config').doc('fuel_prices').get();
if (!doc.exists) return const FuelPrices();
return FuelPrices.fromMap(doc.data()!);
} catch (e) {
return const FuelPrices();
}
}
Future<void> saveFuelPrices(FuelPrices prices) async {
await _db.collection('app_config').doc('fuel_prices').set(prices.toMap());
}
// ─── Dépôts ───────────────────────────────────────────────
Future<List<DepotModel>> getDepots() async {
final snap = await _db.collection('depots').orderBy('name').get();
return snap.docs.map((d) => DepotModel.fromFirestore(d)).toList();
}
Stream<List<DepotModel>> watchDepots() {
return _db
.collection('depots')
.orderBy('name')
.snapshots()
.map((s) => s.docs.map((d) => DepotModel.fromFirestore(d)).toList());
}
Future<String> addDepot(DepotModel depot) async {
final ref = await _db.collection('depots').add(depot.toMap());
return ref.id;
}
Future<void> updateDepot(DepotModel depot) async {
final map = depot.toMap();
map.remove('createdAt');
await _db.collection('depots').doc(depot.id).update(map);
}
Future<void> deleteDepot(String depotId) async {
await _db.collection('depots').doc(depotId).delete();
}
}
/// Instance singleton
final travelService = TravelService();
+46
View File
@@ -0,0 +1,46 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/vehicle_model.dart';
class VehicleService {
final FirebaseFirestore _db = FirebaseFirestore.instance;
static const String _collection = 'vehicles';
/// Récupère tous les véhicules, triés par nom.
Future<List<VehicleModel>> getVehicles() async {
final snapshot = await _db
.collection(_collection)
.orderBy('name')
.get();
return snapshot.docs
.map((doc) => VehicleModel.fromFirestore(doc))
.toList();
}
/// Stream en temps réel
Stream<List<VehicleModel>> watchVehicles() {
return _db
.collection(_collection)
.orderBy('name')
.snapshots()
.map((snap) =>
snap.docs.map((d) => VehicleModel.fromFirestore(d)).toList());
}
/// Ajoute un véhicule
Future<String> addVehicle(VehicleModel vehicle) async {
final ref = await _db.collection(_collection).add(vehicle.toMap());
return ref.id;
}
/// Modifie un véhicule existant
Future<void> updateVehicle(VehicleModel vehicle) async {
final map = vehicle.toMap();
map.remove('createdAt'); // Ne pas écraser la date de création
await _db.collection(_collection).doc(vehicle.id).update(map);
}
/// Supprime un véhicule
Future<void> deleteVehicle(String vehicleId) async {
await _db.collection(_collection).doc(vehicleId).delete();
}
}