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:
@@ -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';
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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.",
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user