fix: supprimer le calculateur de frais de livraison et la section 7.2 des CGV

Suppression complète du système de calcul de frais de livraison (rayon 30km depuis Danizy) :
- Route /estimer-la-livraison et template estimate_delivery.twig
- Calcul automatique livraison dans FlowController et ReserverController
- Champs distance/prix livraison dans la vue admin flow
- Ligne "Frais de livraison" sur les devis générés
- Section 7.2 (mise en relation + rayon 30km) dans les CGV (twig + PDF contrat/devis)
- Liens navigation "Estimer la livraison" (desktop + mobile)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-26 12:03:16 +01:00
parent 31b28e5df2
commit 553d12aac8
9 changed files with 17 additions and 525 deletions

View File

@@ -24,15 +24,12 @@ 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,
@@ -65,12 +62,6 @@ 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(),
@@ -80,12 +71,6 @@ 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'));
}
@@ -142,8 +127,6 @@ 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);
@@ -248,18 +231,6 @@ 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');
@@ -286,68 +257,6 @@ 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,7 +38,6 @@ 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
{
@@ -575,7 +574,6 @@ class ReserverController extends AbstractController
ProductReserveRepository $productReserveRepository,
EntityManagerInterface $em,
OptionsRepository $optionsRepository,
HttpClientInterface $client,
Request $request,
Mailer $mailer
): Response {
@@ -644,14 +642,6 @@ 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' => [
@@ -668,7 +658,6 @@ class ReserverController extends AbstractController
'formule' => $cartData['total']['formule'],
'tvaEnabled' => $cartData['tvaEnabled'],
],
'delivery' => $deliveryData
]);
}
@@ -1191,40 +1180,6 @@ 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 ---
@@ -1265,80 +1220,6 @@ 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

@@ -239,7 +239,7 @@ class ContratPdfService extends Fpdf
"ARTICLE 6 CAUTION DE GARANTIE" => "Une caution de garantie peut être exigée selon le type et la valeur du matériel loué :\n• Structures professionnelles Lilian SEGARD - Ludikevent | Selon devis | Restitution immédiate après état des lieux de fin de location et contrôle de conformité\n• Matériel mis en relation (propriétaires privés) | Selon convention propriétaire | Restitution selon accord entre parties\nLa caution peut être conservée totalement ou partiellement en cas de : dégradation du matériel, salissure importante nécessitant un nettoyage approfondi, perte d'éléments ou d'accessoires, non-respect des conditions d'utilisation ayant entraîné des dommages.",
"ARTICLE 7 LIVRAISON, INSTALLATION ET RÉCUPÉRATION" => "7.1 Structures professionnelles Lilian SEGARD - Ludikevent :\nInstallation : L'installation est réalisée exclusivement par le personnel qualifié de Lilian SEGARD - Ludikevent. Le Client doit fournir un terrain plat, propre, dégagé, avec une alimentation électrique 220V à moins de 50 mètres. L'accès doit être praticable pour les véhicules.\nRécupération : La récupération du matériel est effectuée par Lilian SEGARD - Ludikevent aux horaires convenus.\n7.2 Matériel en mise en relation :\nLilian SEGARD - Ludikevent peut assurer l'installation et la récupération pour le compte du propriétaire privé. Le Client locataire reste tenu d'assurer une surveillance permanente et constante. Des frais de déplacement supplémentaires peuvent s'appliquer au-delà d'un rayon de 30 km depuis Danizy.",
"ARTICLE 7 LIVRAISON, INSTALLATION ET RÉCUPÉRATION" => "7.1 Structures professionnelles Lilian SEGARD - Ludikevent :\nInstallation : L'installation est réalisée exclusivement par le personnel qualifié de Lilian SEGARD - Ludikevent. Le Client doit fournir un terrain plat, propre, dégagé, avec une alimentation électrique 220V à moins de 50 mètres. L'accès doit être praticable pour les véhicules.\nRécupération : La récupération du matériel est effectuée par Lilian SEGARD - Ludikevent aux horaires convenus.",
"ARTICLE 8 ÉTATS DES LIEUX ET TRANSFERT DE RESPONSABILITÉ" => "8.1 État des lieux d'installation : Un état des lieux contradictoire est OBLIGATOIREMENT réalisé. La signature par le Client vaut reconnaissance de conformité et TRANSFERT COMPLET DE LA RESPONSABILITÉ DU MATÉRIEL AU CLIENT. Le Client assume l'ENTIÈRE et EXCLUSIVE responsabilité du matériel, de son utilisation, de sa surveillance et des dommages qui pourraient en résulter.\n8.2 État des lieux de fin de location : Un état des lieux contradictoire est réalisé lors de la récupération. Sa signature vaut RETOUR DE LA RESPONSABILITÉ À Lilian SEGARD - Ludikevent.\n8.3 Absence de signature : En cas d'absence ou de refus de signature, l'état des lieux établi par Lilian SEGARD - Ludikevent fera foi. Le matériel sera réputé avoir été livré en parfait état et la responsabilité du Client reste pleine et entière durant toute la période de location.",

View File

@@ -383,7 +383,7 @@ class DevisPdfService extends Fpdf
"ARTICLE 6 CAUTION DE GARANTIE" => "Une caution de garantie peut être exigée selon le type et la valeur du matériel loué :\n• Structures professionnelles Lilian SEGARD - Ludikevent | Selon devis | Restitution immédiate après état des lieux de fin de location et contrôle de conformité\n• Matériel mis en relation (propriétaires privés) | Selon convention propriétaire | Restitution selon accord entre parties\nLa caution peut être conservée totalement ou partiellement en cas de : dégradation du matériel, salissure importante nécessitant un nettoyage approfondi, perte d'éléments ou d'accessoires, non-respect des conditions d'utilisation ayant entraîné des dommages.",
"ARTICLE 7 LIVRAISON, INSTALLATION ET RÉCUPÉRATION" => "7.1 Structures professionnelles Lilian SEGARD - Ludikevent :\nInstallation : L'installation est réalisée exclusivement par le personnel qualifié de Lilian SEGARD - Ludikevent. Le Client doit fournir un terrain plat, propre, dégagé, avec une alimentation électrique 220V à moins de 50 mètres. L'accès doit être praticable pour les véhicules.\nRécupération : La récupération du matériel est effectuée par Lilian SEGARD - Ludikevent aux horaires convenus.\n7.2 Matériel en mise en relation :\nLilian SEGARD - Ludikevent peut assurer l'installation et la récupération pour le compte du propriétaire privé. Le Client locataire reste tenu d'assurer une surveillance permanente et constante. Des frais de déplacement supplémentaires peuvent s'appliquer au-delà d'un rayon de 30 km depuis Danizy.",
"ARTICLE 7 LIVRAISON, INSTALLATION ET RÉCUPÉRATION" => "7.1 Structures professionnelles Lilian SEGARD - Ludikevent :\nInstallation : L'installation est réalisée exclusivement par le personnel qualifié de Lilian SEGARD - Ludikevent. Le Client doit fournir un terrain plat, propre, dégagé, avec une alimentation électrique 220V à moins de 50 mètres. L'accès doit être praticable pour les véhicules.\nRécupération : La récupération du matériel est effectuée par Lilian SEGARD - Ludikevent aux horaires convenus.",
"ARTICLE 8 ÉTATS DES LIEUX ET TRANSFERT DE RESPONSABILITÉ" => "8.1 État des lieux d'installation : Un état des lieux contradictoire est OBLIGATOIREMENT réalisé. La signature par le Client vaut reconnaissance de conformité et TRANSFERT COMPLET DE LA RESPONSABILITÉ DU MATÉRIEL AU CLIENT. Le Client assume l'ENTIÈRE et EXCLUSIVE responsabilité du matériel, de son utilisation, de sa surveillance et des dommages qui pourraient en résulter.\n8.2 État des lieux de fin de location : Un état des lieux contradictoire est réalisé lors de la récupération. Sa signature vaut RETOUR DE LA RESPONSABILITÉ À Lilian SEGARD - Ludikevent.\n8.3 Absence de signature : En cas d'absence ou de refus de signature, l'état des lieux établi par Lilian SEGARD - Ludikevent fera foi. Le matériel sera réputé avoir été livré en parfait état et la responsabilité du Client reste pleine et entière durant toute la période de location.",

View File

@@ -4,16 +4,9 @@
{% 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.deliveryPrice is not null and session.typePaiement %}
{% set isActionable = 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">
@@ -43,26 +36,16 @@
{# BILLING & DELIVERY CONFIG #}
{# BILLING 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 & Livraison
Configuration Facturation
</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-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 class="grid grid-cols-1 md:grid-cols-2 gap-4">
<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">
@@ -72,9 +55,8 @@
{% 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>
<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 %}
@@ -84,6 +66,7 @@
{% endfor %}
</select>
</div>
</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">
Enregistrer les modifications
@@ -164,49 +147,6 @@
</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,7 +133,6 @@
{{ 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">
@@ -189,7 +188,6 @@
{{ 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

@@ -510,14 +510,6 @@
</div>
</div>
{# Frais de déplacement #}
<div class="flex items-center gap-4 p-5 bg-amber-50 rounded-2xl border border-amber-100">
<span class="text-2xl">🌍</span>
<p class="text-xs md:text-sm font-bold text-amber-900 leading-tight">
Livraison incluse dans un rayon de <span class="underline decoration-amber-400 underline-offset-4">30 km depuis Danizy</span>.
Des frais de déplacement s'appliquent au-delà.
</p>
</div>
</div>
</div>

View File

@@ -1,134 +0,0 @@
{% 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,100 +273,6 @@
</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">