revert: remettre le calculateur de frais de livraison

Restauration complète du système d'estimation de livraison :
- Page publique /estimer-la-livraison + liens navigation
- Calcul automatique livraison dans FlowController (admin)
- Champs distance/prix + carte Leaflet dans la vue admin flow
- Estimation livraison dans la confirmation de réservation
- Ligne "Frais de livraison" sur les devis générés

Seules les modifications CGV (suppression section 7.2 rayon 30km) sont conservées.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-26 12:20:35 +01:00
parent 553d12aac8
commit 1fb0cc6f3f
6 changed files with 515 additions and 15 deletions

View File

@@ -24,12 +24,15 @@ use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\HttpClient\HttpClientInterface;
#[Route('/crm/flow')]
class FlowController extends AbstractController
{
public function __construct(
private readonly AppLogger $appLogger,
private readonly OrderSessionRepository $orderSessionRepository,
private readonly HttpClientInterface $client,
private readonly DevisRepository $devisRepository,
private readonly ProductRepository $productRepository,
private readonly OptionsRepository $optionsRepository,
@@ -62,6 +65,12 @@ class FlowController extends AbstractController
{
$this->appLogger->record('VIEW', 'Consultation détails réservation en ligne #' . $session->getId());
// Auto-calculation of delivery if missing or geometry missing
if (($session->getDeliveryDistance() === null || $session->getDeliveryPrice() === null || $session->getDeliveryGeometry() === null) && $session->getAdressEvent()) {
$this->calculateDelivery($session);
$em->flush();
}
return $this->render('dashboard/flow/view.twig', [
'session' => $session,
'prestataires' => $this->prestaireRepository->findAll(),
@@ -71,6 +80,12 @@ class FlowController extends AbstractController
#[Route('/update/{id}', name: 'app_crm_flow_update', methods: ['POST'])]
public function update(\App\Entity\OrderSession $session, Request $request, \Doctrine\ORM\EntityManagerInterface $em): Response
{
if ($request->request->has('deliveryDistance')) {
$session->setDeliveryDistance((float)$request->request->get('deliveryDistance'));
}
if ($request->request->has('deliveryPrice')) {
$session->setDeliveryPrice((float)$request->request->get('deliveryPrice'));
}
if ($request->request->has('typePaiement')) {
$session->setTypePaiement($request->request->get('typePaiement'));
}
@@ -127,6 +142,8 @@ class FlowController extends AbstractController
$devis->setCustomer($session->getCustomer());
// 2.1 Set additional Devis fields from OrderSession
$devis->setDistance($session->getDeliveryDistance());
$devis->setPriceShip($session->getDeliveryPrice());
$devis->setPaymentMethod($session->getTypePaiement());
$devis->setOrderSession($session);
@@ -231,6 +248,18 @@ class FlowController extends AbstractController
}
}
// 7. Delivery Fee
if ($session->getDeliveryPrice() > 0) {
$devisOpt = new DevisOptions();
$devisOpt->setOption("Frais de livraison");
$dist = number_format($session->getDeliveryDistance(), 1, ',', ' ');
$town = $session->getBillingTown() ?: 'Ville inconnue';
$devisOpt->setDetails("Livraison ($dist km) - $town");
$devisOpt->setPriceHt($session->getDeliveryPrice());
$em->persist($devisOpt);
$devis->addDevisOption($devisOpt);
}
// 1. DocuSeal (Version pour signature)
$docusealService = new DevisPdfService($this->kernel, $devis, $this->productRepository, true);
$this->savePdfFile($devis, $docusealService->generate(), 'dc_', 'setDevisDocuSealFile');
@@ -257,6 +286,68 @@ class FlowController extends AbstractController
return $this->redirectToRoute('app_crm_flow');
}
private function calculateDelivery(\App\Entity\OrderSession $session): void
{
if (!$session->getAdressEvent() || !$session->getZipCodeEvent() || !$session->getTownEvent()) {
return;
}
$query = sprintf('%s %s %s', $session->getAdressEvent(), $session->getZipCodeEvent(), $session->getTownEvent());
try {
$response = $this->client->request('GET', 'https://api-adresse.data.gouv.fr/search/', [
'query' => [
'q' => $query,
'limit' => 1
]
]);
$content = $response->toArray();
if (!empty($content['features'])) {
$coords = $content['features'][0]['geometry']['coordinates'];
$lon = $coords[0];
$lat = $coords[1];
// Point de départ (LudikEvent)
$startLat = 49.849;
$startLon = 3.286;
// Calcul itinéraire via API Geoplateforme
$itineraireResponse = $this->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'
]
]);
$itineraire = $itineraireResponse->toArray();
$distance = $itineraire['distance'];
$geometry = $itineraire['geometry'] ?? null;
$rate = 0.50;
$trips = 4;
$price = 0.0;
if ($distance > 10) {
$chargedDistance = $distance - 10;
$price = ($chargedDistance * $trips) * $rate;
}
$session->setDeliveryDistance($distance);
$session->setDeliveryPrice($price);
$session->setDeliveryGeometry($geometry);
}
} catch (\Exception $e) {
// Log error or silent fail
}
}
private function savePdfFile(Devis $devis, string $content, string $prefix, string $setterMethod): void
{
$tmpPath = sys_get_temp_dir() . '/' . $prefix . uniqid() . '.pdf';

View File

@@ -38,6 +38,7 @@ use App\Service\Pdf\DevisPdfService;
use App\Entity\Devis;
use App\Entity\DevisLine;
use App\Entity\CustomerAddress;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class ReserverController extends AbstractController
{
@@ -574,6 +575,7 @@ class ReserverController extends AbstractController
ProductReserveRepository $productReserveRepository,
EntityManagerInterface $em,
OptionsRepository $optionsRepository,
HttpClientInterface $client,
Request $request,
Mailer $mailer
): Response {
@@ -642,6 +644,14 @@ class ReserverController extends AbstractController
$cartData = $this->buildCartData($products, $selectedOptionsMap, $duration, $optionsRepository, $uploaderHelper, $promotion, $formule);
// --- Calcul Frais de Livraison ---
$deliveryData = $this->calculateDelivery(
$session->getAdressEvent(),
$session->getZipCodeEvent(),
$session->getTownEvent(),
$client
);
return $this->render('revervation/flow_confirmed.twig', [
'session' => $session,
'cart' => [
@@ -658,6 +668,7 @@ class ReserverController extends AbstractController
'formule' => $cartData['total']['formule'],
'tvaEnabled' => $cartData['tvaEnabled'],
],
'delivery' => $deliveryData
]);
}
@@ -1180,6 +1191,40 @@ class ReserverController extends AbstractController
return $this->render('revervation/hosting.twig');
}
#[Route('/estimer-la-livraison', name: 'reservation_estimate_delivery')]
public function estimateDelivery(Request $request, HttpClientInterface $client): Response
{
$form = $this->createFormBuilder()
->add('address', TextType::class, ['required' => true])
->add('zipCode', TextType::class, ['required' => true])
->add('city', TextType::class, ['required' => true])
->getForm();
$form->handleRequest($request);
$estimation = null;
$details = null;
$geometry = null;
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$deliveryData = $this->calculateDelivery($data['address'], $data['zipCode'], $data['city'], $client);
if ($deliveryData['details'] !== null) {
$estimation = $deliveryData['estimation'];
$details = $deliveryData['details'];
$geometry = $deliveryData['geometry'];
} else {
$this->addFlash('warning', 'Adresse introuvable ou erreur lors du calcul.');
}
}
return $this->render('revervation/estimate_delivery.twig', [
'form' => $form->createView(),
'estimation' => $estimation,
'details' => $details,
'geometry' => $geometry
]);
}
// --- Private Helper Methods ---
@@ -1220,6 +1265,80 @@ class ReserverController extends AbstractController
return isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
}
private function calculateDelivery(?string $address, ?string $zipCode, ?string $town, HttpClientInterface $client): array
{
$result = [
'estimation' => null,
'details' => null,
'geometry' => null
];
if (!$address || !$zipCode || !$town) {
return $result;
}
$query = sprintf('%s %s %s', $address, $zipCode, $town);
try {
$response = $client->request('GET', 'https://api-adresse.data.gouv.fr/search/', [
'query' => [
'q' => $query,
'limit' => 1
]
]);
$content = $response->toArray();
if (!empty($content['features'])) {
$coords = $content['features'][0]['geometry']['coordinates'];
$lon = $coords[0];
$lat = $coords[1];
// Point de départ (LudikEvent)
$startLat = 49.849;
$startLon = 3.286;
// 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'
]
]);
$itineraire = $itineraireResponse->toArray();
$distance = $itineraire['distance'];
$result['geometry'] = $itineraire['geometry'] ?? null;
$rate = 0.50;
$trips = 4;
if ($distance <= 10) {
$result['estimation'] = 0.0;
$chargedDistance = 0.0;
} else {
$chargedDistance = $distance - 10;
$result['estimation'] = ($chargedDistance * $trips) * $rate;
}
$result['details'] = [
'distance' => $distance,
'chargedDistance' => $chargedDistance,
'trips' => $trips,
'rate' => $rate,
'isFree' => ($distance <= 10)
];
}
} catch (\Exception $e) {
// Return default nulls on error
}
return $result;
}
private function buildCartData(array $products, array $selectedOptionsMap, int $duration, OptionsRepository $optionsRepository, UploaderHelper $uploaderHelper, ?Promotion $promotion = null, ?Formules $formule = null): array
{

View File

@@ -4,9 +4,16 @@
{% block title_header %}Réservation #{{ session.id }}{% endblock %}
{% block body %}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin="" nonce="{{ csp_nonce('script') }}"></script>
<div class="space-y-8 pb-20">
{% set isActionable = session.typePaiement %}
{% set isActionable = session.deliveryPrice is not null and session.typePaiement %}
<div class="flex items-center gap-3 justify-end">
<a href="{{ path('app_crm_flow') }}"
class="group relative flex items-center gap-2 px-6 py-3 bg-slate-700 hover:bg-slate-600 rounded-2xl text-white text-xs font-bold uppercase tracking-wider transition-all shadow-lg">
@@ -36,16 +43,26 @@
{# BILLING CONFIG #}
{# BILLING & DELIVERY CONFIG #}
<div class="bg-slate-800/40 backdrop-blur-xl border border-slate-700/50 rounded-[2rem] p-8">
<h3 class="text-lg font-bold text-white mb-6 flex items-center gap-3">
<div class="p-2 bg-indigo-500/10 rounded-lg text-indigo-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
</div>
Configuration Facturation
Configuration Facturation & Livraison
</h3>
<form action="{{ path('app_crm_flow_update', {id: session.id}) }}" method="post" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Distance (km)</label>
<input type="number" step="0.1" name="deliveryDistance" value="{{ session.deliveryDistance }}"
class="w-full bg-slate-900/50 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-all">
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Prix Livraison (€)</label>
<input type="number" step="0.01" name="deliveryPrice" value="{{ session.deliveryPrice }}"
class="w-full bg-slate-900/50 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-all">
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Mode de paiement</label>
<select name="typePaiement" class="w-full bg-slate-900/50 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-all">
@@ -55,17 +72,17 @@
{% endfor %}
</select>
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Prestataire</label>
<select name="prestataire" class="w-full bg-slate-900/50 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-all">
<option value="">-- Aucun prestataire assigné --</option>
{% for p in prestataires %}
<option value="{{ p.id }}" {% if session.prestataire and session.prestataire.id == p.id %}selected{% endif %}>
{{ p.surname }} {{ p.name }} ({{ p.email }})
</option>
{% endfor %}
</select>
</div>
</div>
<div class="mt-4">
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Prestataire (Livraison)</label>
<select name="prestataire" class="w-full bg-slate-900/50 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-all">
<option value="">-- Aucun prestataire assigné --</option>
{% for p in prestataires %}
<option value="{{ p.id }}" {% if session.prestataire and session.prestataire.id == p.id %}selected{% endif %}>
{{ p.surname }} {{ p.name }} ({{ p.email }})
</option>
{% endfor %}
</select>
</div>
<div class="flex justify-end mt-4">
<button type="submit" class="py-2 px-6 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold uppercase tracking-widest rounded-lg transition-all">
@@ -147,6 +164,49 @@
</div>
</div>
{% if session.deliveryDistance is not null %}
<div class="mt-6 border-t border-slate-700/50 pt-6">
<h4 class="text-white font-bold text-sm mb-4">Détails Livraison</h4>
<div class="bg-slate-900/50 rounded-xl border border-slate-700/50 overflow-hidden mb-4">
{# Map #}
{% if session.deliveryGeometry %}
<div class="h-48 w-full relative z-0">
<leaflet-map class="absolute inset-0 w-full h-full"
data-geometry="{{ session.deliveryGeometry|json_encode|e('html_attr') }}">
</leaflet-map>
</div>
{% endif %}
{# Details #}
<div class="p-4 space-y-2 text-xs">
<div class="flex justify-between">
<span class="text-slate-400">Distance réelle (Aller)</span>
<span class="text-white font-bold">{{ session.deliveryDistance|number_format(1, ',', ' ') }} km</span>
</div>
<div class="flex justify-between">
<span class="text-slate-400">Franchise</span>
<span class="text-emerald-400 font-bold">- 10.0 km</span>
</div>
<div class="flex justify-between border-t border-slate-700 pt-2">
<span class="text-slate-400">Distance facturée</span>
<span class="text-white font-bold">{{ max(0, session.deliveryDistance - 10)|number_format(1, ',', ' ') }} km</span>
</div>
<div class="flex justify-between">
<span class="text-slate-400">Trajets</span>
<span class="text-white font-bold">4 (2 A/R)</span>
</div>
<div class="mt-2 p-2 bg-indigo-500/10 border border-indigo-500/20 rounded-lg text-center">
<code class="text-[10px] text-indigo-300 font-mono">
({{ session.deliveryDistance|number_format(1) }} - 10) x 4 x 0.50€ = {{ session.deliveryPrice|number_format(2) }}
</code>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>

View File

@@ -133,6 +133,7 @@
{{ macros.nav_link('reservation_catalogue', 'nav.catalogue') }}
{{ macros.nav_link('reservation_formules', 'nav.packages') }}
{{ macros.nav_link('/images/Catalogue.pdf', 'nav.pdf', true) }}
{{ macros.nav_link('reservation_estimate_delivery', 'Estimer la livraison') }}
{{ macros.nav_link('reservation_contact', 'nav.contact') }}
<a is="flow-reserve" class="relative p-2 text-gray-600 hover:text-[#f39e36] transition-colors" aria-label="Panier">
@@ -188,6 +189,7 @@
{{ macros.mobile_nav_link('reservation_catalogue', 'Nos structures') }}
{{ macros.mobile_nav_link('reservation_formules', 'Nos Formules') }}
{{ macros.mobile_nav_link('/images/Catalogue.pdf', 'Catalogue', true) }}
{{ macros.mobile_nav_link('reservation_estimate_delivery', 'Estimer la livraison') }}
{{ macros.mobile_nav_link('reservation_search', 'Rechercher') }}
<a is="flow-reserve" class="block px-3 py-2 text-base font-medium text-gray-700 hover:bg-gray-50 rounded-xl flex items-center justify-between">
<span>Panier</span>

View 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 %}

View File

@@ -273,6 +273,100 @@
</div>
</div>
{# Delivery Estimation #}
{% if delivery is defined and delivery.estimation is not null %}
<div class="bg-white rounded-2xl border border-slate-200 shadow-sm mb-8 overflow-hidden">
<div class="p-6 border-b border-slate-100 flex items-center gap-3 bg-slate-50/50">
<div class="bg-emerald-100 p-2 rounded-lg text-emerald-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<h3 class="text-lg font-bold text-slate-900">Estimation des frais de livraison</h3>
</div>
<div class="flex flex-col md:flex-row">
{# Details (60%) #}
<div class="w-full md:w-3/5 p-6 border-b md:border-b-0 md:border-r border-slate-100">
{% if delivery.details.isFree %}
<div class="bg-emerald-50 border border-emerald-100 rounded-xl p-4 mb-6">
<div class="flex items-center gap-3">
<span class="text-2xl font-black text-emerald-600 italic">Offert !</span>
<span class="px-3 py-1 bg-white text-emerald-700 rounded-full text-[10px] font-bold uppercase tracking-wide shadow-sm">Zone gratuite</span>
</div>
<p class="text-sm text-emerald-800 mt-2 font-medium">Votre événement se trouve à moins de 10km de nos locaux.</p>
</div>
{% else %}
<div class="mb-6">
<span class="block text-xs font-bold text-slate-400 uppercase tracking-widest mb-1">Coût estimé</span>
<p class="text-4xl font-black text-slate-900 italic">
{{ delivery.estimation|format_currency('EUR') }}
</p>
</div>
{% endif %}
<div class="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-sm">
<span class="text-slate-500">Distance réelle (Aller)</span>
<span class="font-bold text-slate-700">{{ delivery.details.distance|number_format(1, ',', ' ') }} km</span>
</div>
<div class="flex justify-between text-sm">
<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-100 my-2"></div>
<div class="flex justify-between text-sm">
<span class="text-slate-500">Distance facturée</span>
<span class="font-bold text-slate-700">{{ delivery.details.chargedDistance|number_format(1, ',', ' ') }} km</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-slate-500">Nombre de trajets</span>
<span class="font-bold text-slate-700">{{ delivery.details.trips }} (2 A/R)</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-slate-500">Tarif kilométrique</span>
<span class="font-bold text-slate-700">{{ delivery.details.rate }} € / km</span>
</div>
</div>
{% if not delivery.details.isFree %}
<div class="mt-4 p-3 bg-indigo-50 border border-indigo-100 rounded-xl text-center">
<p class="text-[10px] font-bold text-indigo-400 uppercase mb-1">Formule appliquée</p>
<code class="text-xs text-indigo-700 font-mono font-bold">
({{ delivery.details.distance|number_format(1) }} - 10) x {{ delivery.details.trips }} x {{ delivery.details.rate }}€ = {{ delivery.estimation|number_format(2) }}
</code>
</div>
{% endif %}
<div class="mt-4 p-3 bg-slate-50 rounded-xl text-center">
<p class="text-[10px] text-slate-400 italic">
Cette estimation est indicative. Le montant définitif figurera sur votre devis.
</p>
</div>
</div>
{# Map (40%) #}
<div class="w-full md:w-2/5 h-64 md:h-auto min-h-[16rem] bg-slate-100 relative">
{% if delivery.geometry %}
<leaflet-map class="absolute inset-0 z-0"
data-geometry="{{ delivery.geometry|json_encode|e('html_attr') }}">
</leaflet-map>
{% else %}
<div class="absolute inset-0 flex items-center justify-center text-slate-400">
<span class="text-xs font-medium">Carte non disponible</span>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
<div class="pt-6 border-t border-slate-100 mt-8 flex flex-col md:flex-row justify-center gap-4">
<a data-turbo="false" href="{{ path('reservation_generate_devis', {sessionId: session.uuid}) }}" class="w-full md:w-auto px-8 py-4 bg-gray-200 text-gray-800 font-bold rounded-2xl shadow-sm hover:shadow-md hover:scale-[1.01] transition-all flex items-center justify-center gap-2 text-lg">