✨ feat(ReserverController): Calcule l'itinéraire et affiche sur une carte
Ajoute le calcul de l'itinéraire via l'API Geoplateforme et affiche le résultat sur une carte Leaflet. Met à jour la CSP.
This commit is contained in:
@@ -4,6 +4,7 @@ import { CookieBanner } from "./tools/CookieBanner.js";
|
||||
import { FlowReserve } from "./tools/FlowReserve.js";
|
||||
import { FlowDatePicker } from "./tools/FlowDatePicker.js";
|
||||
import { FlowAddToCart } from "./tools/FlowAddToCart.js";
|
||||
import { LeafletMap } from "./tools/LeafletMap.js";
|
||||
import * as Turbo from "@hotwired/turbo";
|
||||
import { onLCP, onINP, onCLS } from 'web-vitals';
|
||||
import AOS from 'aos';
|
||||
@@ -252,7 +253,7 @@ const initRegisterLogic = () => {
|
||||
|
||||
// --- INITIALISATION ---
|
||||
const registerComponents = () => {
|
||||
const comps = [['utm-event', UtmEvent], ['utm-account', UtmAccount], ['cookie-banner', CookieBanner]];
|
||||
const comps = [['utm-event', UtmEvent], ['utm-account', UtmAccount], ['cookie-banner', CookieBanner], ['leaflet-map', LeafletMap]];
|
||||
comps.forEach(([name, cl]) => { if (!customElements.get(name)) customElements.define(name, cl); });
|
||||
|
||||
if(!customElements.get('flow-reserve'))
|
||||
|
||||
53
assets/tools/LeafletMap.js
Normal file
53
assets/tools/LeafletMap.js
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
export class LeafletMap extends HTMLElement {
|
||||
connectedCallback() {
|
||||
if (this.map) return;
|
||||
|
||||
// Ensure Leaflet is loaded
|
||||
if (typeof L === 'undefined') {
|
||||
const checkLeaflet = setInterval(() => {
|
||||
if (typeof L !== 'undefined') {
|
||||
clearInterval(checkLeaflet);
|
||||
this.initMap();
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
this.initMap();
|
||||
}
|
||||
|
||||
initMap() {
|
||||
if (!this.dataset.geometry) return;
|
||||
|
||||
try {
|
||||
const geometry = JSON.parse(this.dataset.geometry);
|
||||
|
||||
// 'this' refers to the custom element itself.
|
||||
// Leaflet can attach to it if it has dimensions.
|
||||
this.map = L.map(this);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(this.map);
|
||||
|
||||
const layer = L.geoJSON(geometry).addTo(this.map);
|
||||
this.map.fitBounds(layer.getBounds(), {padding: [50, 50]});
|
||||
|
||||
// Fix for map not rendering correctly sometimes when container size changes or initial load
|
||||
setTimeout(() => {
|
||||
this.map.invalidateSize();
|
||||
}, 100);
|
||||
|
||||
} catch (e) {
|
||||
console.error('Error initializing Leaflet map:', e);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.map) {
|
||||
this.map.remove();
|
||||
this.map = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ nelmio_security:
|
||||
- "https://auth.esy-web.dev"
|
||||
- "https://static.cloudflareinsights.com"
|
||||
- "https://challenges.cloudflare.com"
|
||||
- "https://unpkg.com"
|
||||
connect-src:
|
||||
- "'self'"
|
||||
- "https://sentry.esy-web.dev"
|
||||
@@ -54,10 +55,13 @@ nelmio_security:
|
||||
- "'self'"
|
||||
- "'unsafe-inline'"
|
||||
- "https://chat.esy-web.dev"
|
||||
- "https://unpkg.com"
|
||||
img-src:
|
||||
- "'self'"
|
||||
- "data:"
|
||||
- "https://chat.esy-web.dev"
|
||||
- "https://*.tile.openstreetmap.org"
|
||||
- "https://unpkg.com"
|
||||
font-src:
|
||||
- "'self'"
|
||||
- "data:"
|
||||
|
||||
31
migrations/Version20260204084418.php
Normal file
31
migrations/Version20260204084418.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260204084418 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE order_session ADD options JSON DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE order_session DROP options');
|
||||
}
|
||||
}
|
||||
@@ -1168,18 +1168,22 @@ class ReserverController extends AbstractController
|
||||
$startLat = 49.849;
|
||||
$startLon = 3.286;
|
||||
|
||||
// Formule de Haversine
|
||||
$earthRadius = 6371; // km
|
||||
// Calcul itinéraire via API Geoplateforme
|
||||
$itineraireResponse = $client->request('GET', 'https://data.geopf.fr/navigation/itineraire', [
|
||||
'query' => [
|
||||
'resource' => 'bdtopo-osrm',
|
||||
'start' => $startLon . ',' . $startLat,
|
||||
'end' => $lon . ',' . $lat,
|
||||
'profile' => 'car',
|
||||
'optimization' => 'fastest',
|
||||
'distanceUnit' => 'kilometer',
|
||||
'geometryFormat' => 'geojson'
|
||||
]
|
||||
]);
|
||||
|
||||
$dLat = deg2rad($lat - $startLat);
|
||||
$dLon = deg2rad($lon - $startLon);
|
||||
|
||||
$a = sin($dLat / 2) * sin($dLat / 2) +
|
||||
cos(deg2rad($startLat)) * cos(deg2rad($lat)) *
|
||||
sin($dLon / 2) * sin($dLon / 2);
|
||||
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
$distance = $earthRadius * $c;
|
||||
$itineraire = $itineraireResponse->toArray();
|
||||
$distance = $itineraire['distance'];
|
||||
$geometry = $itineraire['geometry'] ?? null;
|
||||
$rate = 0.50;
|
||||
$trips = 4;
|
||||
|
||||
@@ -1209,7 +1213,8 @@ class ReserverController extends AbstractController
|
||||
return $this->render('revervation/estimate_delivery.twig', [
|
||||
'form' => $form->createView(),
|
||||
'estimation' => $estimation,
|
||||
'details' => $details ?? null
|
||||
'details' => $details ?? null,
|
||||
'geometry' => $geometry ?? null
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
134
templates/revervation/estimate_delivery.twig
Normal file
134
templates/revervation/estimate_delivery.twig
Normal file
@@ -0,0 +1,134 @@
|
||||
{% extends 'revervation/base.twig' %}
|
||||
|
||||
{% block title %}Estimer les frais de livraison | Ludik Event{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""/>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="min-h-screen bg-white font-sans antialiased py-20 px-4">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<h1 class="text-4xl md:text-5xl font-black text-slate-900 uppercase italic tracking-tighter mb-8 text-center">
|
||||
Estimer <span class="text-[#f39e36]">la livraison</span>
|
||||
</h1>
|
||||
|
||||
<div class="bg-slate-50 p-8 rounded-[2.5rem] border border-slate-100 shadow-xl">
|
||||
<p class="text-center text-slate-500 font-medium mb-8">
|
||||
Renseignez l'adresse de votre événement pour obtenir une estimation des frais de livraison.
|
||||
</p>
|
||||
|
||||
{{ form_start(form, {'attr': {'class': 'space-y-6', 'data-turbo': 'false'}}) }}
|
||||
|
||||
<div>
|
||||
{{ form_label(form.address, 'Adresse complète', {'label_attr': {'class': 'text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 mb-2 block'}}) }}
|
||||
{{ form_widget(form.address, {'attr': {'class': 'w-full bg-white border border-slate-200 rounded-2xl px-5 py-4 text-slate-900 outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition-all', 'placeholder': 'Ex: 10 rue de la Paix'}}) }}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
{{ form_label(form.zipCode, 'Code Postal', {'label_attr': {'class': 'text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 mb-2 block'}}) }}
|
||||
{{ form_widget(form.zipCode, {'attr': {'class': 'w-full bg-white border border-slate-200 rounded-2xl px-5 py-4 text-slate-900 outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition-all', 'placeholder': '02000'}}) }}
|
||||
</div>
|
||||
<div>
|
||||
{{ form_label(form.city, 'Ville', {'label_attr': {'class': 'text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 mb-2 block'}}) }}
|
||||
{{ form_widget(form.city, {'attr': {'class': 'w-full bg-white border border-slate-200 rounded-2xl px-5 py-4 text-slate-900 outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition-all', 'placeholder': 'Laon'}}) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<button type="submit" class="w-full py-5 bg-slate-900 text-white rounded-2xl font-black uppercase tracking-[0.2em] hover:bg-[#f39e36] transition-all shadow-lg active:scale-95">
|
||||
Calculer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% if estimation is defined and estimation is not null %}
|
||||
<div class="mt-10 bg-blue-50 border border-blue-100 rounded-2xl overflow-hidden animate-in fade-in slide-in-from-bottom-4">
|
||||
<div class="p-6 border-b border-blue-100/50">
|
||||
<span class="text-[10px] font-black text-blue-400 uppercase tracking-widest block mb-2">Résultat de l'estimation</span>
|
||||
{% if details.isFree %}
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-3xl font-black text-emerald-500 italic">Offert !</span>
|
||||
<span class="px-3 py-1 bg-emerald-100 text-emerald-600 rounded-full text-[10px] font-bold uppercase tracking-wide">Zone gratuite</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 mt-2">Votre événement se trouve à moins de 10km de nos locaux.</p>
|
||||
{% else %}
|
||||
<p class="text-3xl font-black text-slate-900 italic">
|
||||
{{ estimation|format_currency('EUR') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if details is defined %}
|
||||
<div class="bg-white/50 p-6 space-y-3">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-4">Détails du calcul</p>
|
||||
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="text-slate-500">Distance réelle (Aller)</span>
|
||||
<span class="font-bold text-slate-700">{{ details.distance|number_format(1, ',', ' ') }} km</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="text-slate-500">Franchise kilométrique</span>
|
||||
<span class="font-bold text-emerald-500">- 10.0 km (Offerts)</span>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-slate-200/60 my-2"></div>
|
||||
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="text-slate-500">Distance facturée</span>
|
||||
<span class="font-bold text-slate-700">{{ details.chargedDistance|number_format(1, ',', ' ') }} km</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="text-slate-500">Nombre de trajets</span>
|
||||
<span class="font-bold text-slate-700">{{ details.trips }} (2 A/R)</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="text-slate-500">Tarif kilométrique</span>
|
||||
<span class="font-bold text-slate-700">{{ details.rate }} € / km</span>
|
||||
</div>
|
||||
|
||||
{% if not details.isFree %}
|
||||
<div class="bg-blue-100/50 p-3 rounded-xl mt-4 text-center">
|
||||
<code class="text-[10px] text-blue-600 font-mono">
|
||||
({{ details.distance|number_format(1) }} - 10) x {{ details.trips }} x {{ details.rate }}€ = {{ estimation|number_format(2) }}€
|
||||
</code>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if geometry is defined and geometry is not null %}
|
||||
<leaflet-map class="h-96 w-full block rounded-xl mt-6 z-0 border-t border-blue-100"
|
||||
data-geometry="{{ geometry|json_encode|e('html_attr') }}">
|
||||
</leaflet-map>
|
||||
{% endif %}
|
||||
|
||||
<div class="p-4 bg-blue-100/30 text-center">
|
||||
<p class="text-[10px] text-slate-500 italic">
|
||||
Cette estimation est donnée à titre indicatif et sera confirmée lors de la validation de votre devis.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
{% if geometry is defined and geometry is not null %}
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""
|
||||
nonce="{{ csp_nonce('script') }}"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user