diff --git a/app.py b/app.py
index b2fed17..3c515c8 100644
--- a/app.py
+++ b/app.py
@@ -26,6 +26,8 @@ COLONNES = [
"Nom du prospect", "Statut", "Département", "Ville", "Code Postal", "Adresse",
"Date d'ajout", "Date de contact", "Nom de contact",
"Téléphone", "Email", "Infos du lieu", "Type", "Lien Facebook", "Notes",
+ "Trajet distance km", "Trajet durée min", "Trajet km péage",
+ "Trajet adresse départ", "Trajet adresse arrivée",
]
STATUT_DEFAUT = "À contacter"
@@ -49,6 +51,8 @@ CONFIG_DEFAUT = {
app = Flask(__name__, static_folder="static")
verrou_scrape = threading.Lock()
verrou_csv = threading.Lock()
+# Sérialise les calculs de trajet (un appel Nominatim/OSRM à la fois, ~1 req/s).
+verrou_trajet = threading.Lock()
def lire_prospects():
@@ -121,6 +125,56 @@ def ecrire_config(config):
os.replace(tmp, CONFIG_PATH)
+def _adresse_arrivee(p):
+ """Reconstruit l'adresse d'arrivée d'un prospect (Adresse, Code Postal, Ville)."""
+ return ", ".join(
+ x.strip() for x in (p.get("Adresse"), p.get("Code Postal"), p.get("Ville")) if x and x.strip())
+
+
+def _tarifs(config):
+ """(conso L/100km, prix €/L, péage €/km) depuis la config, avec replis."""
+ return (
+ float(config.get("conso_l_100km") or 0) or CONFIG_DEFAUT["conso_l_100km"],
+ float(config.get("prix_carburant") or 0) or CONFIG_DEFAUT["prix_carburant"],
+ float(config.get("cout_peage_km") or 0), # 0 = pas de péage compté
+ )
+
+
+def _ecrire_colonnes_trajet(ligne, depart, arrivee, res):
+ """Mémorise dans une ligne les données géo + adresses utilisées pour le calcul."""
+ ligne["Trajet distance km"] = str(res["distance_km"])
+ ligne["Trajet durée min"] = str(res["duree_min"])
+ ligne["Trajet km péage"] = str(res["km_peage"])
+ ligne["Trajet adresse départ"] = depart
+ ligne["Trajet adresse arrivée"] = arrivee
+
+
+def _auto_trajet(ligne, config):
+ """Calcule et mémorise le trajet d'un prospect fraîchement ajouté (best-effort).
+
+ Conçu pour tourner dans un thread daemon : un échec réseau ne doit jamais
+ affecter le scrape. `verrou_trajet` sérialise les appels Nominatim/OSRM.
+ """
+ try:
+ depart = (config.get("adresse_depart") or "").strip()
+ arrivee = _adresse_arrivee(ligne)
+ lien = (ligne.get("Lien Facebook") or "").strip()
+ if not depart or not arrivee:
+ return
+ with verrou_trajet:
+ res = trajet.calculer(depart, arrivee, *_tarifs(config))
+ with verrou_csv:
+ lignes = lire_prospects()
+ cible = next(
+ (l for l in lignes if (l.get("Lien Facebook") or "").strip() == lien), None
+ ) if lien else None
+ if cible is not None:
+ _ecrire_colonnes_trajet(cible, depart, arrivee, res)
+ ecrire_prospects(lignes)
+ except Exception:
+ pass # best-effort : le trajet sera calculé au clic si besoin
+
+
@app.get("/")
def accueil():
return app.send_static_file("index.html")
@@ -186,6 +240,7 @@ def api_ajouter():
lignes = lire_prospects()
lignes.append(ligne)
ecrire_prospects(lignes)
+ threading.Thread(target=_auto_trajet, args=(ligne, lire_config()), daemon=True).start()
return jsonify({"ok": True, "index": len(lignes) - 1})
@@ -274,9 +329,11 @@ def _ajouter_resultat_lot(resultat):
journaliser(url, "doublon", champs["Nom du prospect"])
return {"url": url, "statut": "doublon", "nom": champs["Nom du prospect"]}
champs["Statut"] = STATUT_DEFAUT
- lignes.append({col: champs.get(col, "") for col in COLONNES})
+ ligne = {col: champs.get(col, "") for col in COLONNES}
+ lignes.append(ligne)
ecrire_prospects(lignes)
journaliser(url, "ajoute", champs["Nom du prospect"])
+ threading.Thread(target=_auto_trajet, args=(ligne, lire_config()), daemon=True).start()
return {"url": url, "statut": "ajoute", "nom": champs["Nom du prospect"]}
@@ -377,24 +434,42 @@ def api_distance(idx):
"error": "pas_de_depart",
"message": "Renseignez d'abord votre adresse de départ dans les Paramètres.",
}), 400
- arrivee = ", ".join(x.strip() for x in (p.get("Adresse"), p.get("Code Postal"), p.get("Ville")) if x and x.strip())
+ arrivee = _adresse_arrivee(p)
if not arrivee:
return jsonify({
"error": "pas_d_adresse",
"message": "Ce prospect n'a pas d'adresse ou de ville exploitable.",
}), 400
+ conso, prix, peage = _tarifs(config)
+
+ # Recalcul local : les données géo en cache restent valides tant que les deux
+ # adresses n'ont pas changé. On réapplique simplement les prix courants.
+ if (p.get("Trajet distance km")
+ and p.get("Trajet adresse départ") == depart
+ and p.get("Trajet adresse arrivée") == arrivee):
+ try:
+ return jsonify({**trajet.couts(
+ float(p["Trajet distance km"]),
+ float(p.get("Trajet durée min") or 0),
+ float(p.get("Trajet km péage") or 0),
+ conso, prix, peage,
+ ), "source": "local"})
+ except (TypeError, ValueError):
+ pass # données en cache illisibles : on bascule sur un recalcul complet
+
+ # Recalcul complet : appels Nominatim/OSRM, puis mémorisation des données géo.
try:
- resultat = trajet.calculer(
- depart, arrivee,
- float(config.get("conso_l_100km") or 0) or CONFIG_DEFAUT["conso_l_100km"],
- float(config.get("prix_carburant") or 0) or CONFIG_DEFAUT["prix_carburant"],
- float(config.get("cout_peage_km") or 0), # 0 = pas de péage compté
- )
- return jsonify(resultat)
+ resultat = trajet.calculer(depart, arrivee, conso, prix, peage)
except trajet.ErreurTrajet as e:
return jsonify({"error": "trajet", "message": str(e)}), 502
except Exception as e:
return jsonify({"error": "erreur", "message": f"Échec du calcul : {e}"}), 500
+ with verrou_csv:
+ lignes = lire_prospects()
+ if 0 <= idx < len(lignes):
+ _ecrire_colonnes_trajet(lignes[idx], depart, arrivee, resultat)
+ ecrire_prospects(lignes)
+ return jsonify({**resultat, "source": "api"})
if __name__ == "__main__":
diff --git a/static/index.html b/static/index.html
index d3d5bb8..8a6eca8 100644
--- a/static/index.html
+++ b/static/index.html
@@ -908,13 +908,18 @@ function construireDetail(corps, p) {
trajetBtn.textContent = "🚗 Calculer le trajet";
const resu = document.createElement("div");
resu.className = "trajet-resu";
- trajetBtn.addEventListener("click", async () => {
+ // `silencieux` : affichage auto à l'ouverture (données déjà en cache) — pas de spinner.
+ async function calculerTrajet(silencieux) {
trajetBtn.disabled = true;
- resu.innerHTML = ' Calcul en cours…';
+ if (!silencieux) resu.innerHTML = ' Calcul en cours…';
try {
const rep = await fetch(`/api/distance/${p.index}`);
const d = await rep.json();
- if (!rep.ok) { resu.innerHTML = `${echap(d.message || "Échec du calcul.")}`; return; }
+ if (!rep.ok) {
+ // En mode silencieux, on n'affiche pas d'erreur : on laisse simplement le bouton.
+ if (!silencieux) resu.innerHTML = `${echap(d.message || "Échec du calcul.")}`;
+ return;
+ }
const detailAller = d.peage_aller > 0
? ` (carburant ${d.carburant_aller} € + péage ${d.peage_aller} €)` : "";
const detailAR = d.peage_aller_retour > 0
@@ -925,13 +930,17 @@ function construireDetail(corps, p) {
`Aller : ${d.cout_aller} €${detailAller}
` +
`Aller-retour : ${d.cout_aller_retour} €${detailAR} ` +
`(${d.distance_aller_retour_km} km)`;
+ trajetBtn.textContent = "🔄 Recalculer le trajet";
} catch {
- resu.innerHTML = 'Le serveur ne répond pas.';
+ if (!silencieux) resu.innerHTML = 'Le serveur ne répond pas.';
} finally {
trajetBtn.disabled = false;
}
- });
+ }
+ trajetBtn.addEventListener("click", () => calculerTrajet(false));
secTrajet.append(trajetBtn, resu);
+ // Trajet déjà calculé (après le scrape) : on l'affiche tout de suite (recalcul local instantané).
+ if ((p["Trajet distance km"] || "").trim()) calculerTrajet(true);
corps.append(secInfos, secContact, secTrajet);
}
diff --git a/trajet.py b/trajet.py
index 0c7301a..fc4fd39 100644
--- a/trajet.py
+++ b/trajet.py
@@ -75,14 +75,15 @@ def itineraire(depart, arrivee):
return route["distance"] / 1000.0, route["duration"] / 60.0, metres_autoroute / 1000.0
-def calculer(adresse_depart, adresse_arrivee, conso_l_100km, prix_carburant, cout_peage_km=0.0):
- """Estime distance, durée, coût carburant et péage (aller simple et aller-retour).
+def couts(distance_km, duree_min, km_peage, conso_l_100km, prix_carburant, cout_peage_km=0.0):
+ """Données géographiques + tarifs -> dict complet (coûts aller simple et aller-retour).
+
+ Calcul purement local (aucun appel réseau) : permet de réappliquer de nouveaux prix
+ sur une distance déjà connue sans re-géocoder ni re-router.
Le péage est estimé : km d'autoroute du trajet × `cout_peage_km` (tarif moyen
paramétrable, en €/km). Mettre 0 pour ne pas compter de péage.
"""
- distance_km, duree_min, km_peage = itineraire(
- geocoder(adresse_depart), geocoder(adresse_arrivee))
carburant = distance_km / 100.0 * conso_l_100km * prix_carburant
peage = km_peage * cout_peage_km
cout_aller = carburant + peage
@@ -100,6 +101,16 @@ def calculer(adresse_depart, adresse_arrivee, conso_l_100km, prix_carburant, cou
}
+def calculer(adresse_depart, adresse_arrivee, conso_l_100km, prix_carburant, cout_peage_km=0.0):
+ """Estime distance, durée, coût carburant et péage (aller simple et aller-retour).
+
+ Fait les appels réseau (Nominatim + OSRM) puis délègue le calcul des coûts à `couts`.
+ """
+ distance_km, duree_min, km_peage = itineraire(
+ geocoder(adresse_depart), geocoder(adresse_arrivee))
+ return couts(distance_km, duree_min, km_peage, conso_l_100km, prix_carburant, cout_peage_km)
+
+
if __name__ == "__main__":
import sys
if len(sys.argv) != 3: