diff --git a/src/Controller/Dashboard/FlowController.php b/src/Controller/Dashboard/FlowController.php index a9cb1d3..2d15096 100644 --- a/src/Controller/Dashboard/FlowController.php +++ b/src/Controller/Dashboard/FlowController.php @@ -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'; diff --git a/src/Controller/ReserverController.php b/src/Controller/ReserverController.php index edaf0d7..1f77bcf 100644 --- a/src/Controller/ReserverController.php +++ b/src/Controller/ReserverController.php @@ -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 { diff --git a/templates/dashboard/flow/view.twig b/templates/dashboard/flow/view.twig index a0c1a5d..4c7b9ca 100644 --- a/templates/dashboard/flow/view.twig +++ b/templates/dashboard/flow/view.twig @@ -4,9 +4,16 @@ {% block title_header %}Réservation #{{ session.id }}{% endblock %} {% block body %} + + +
- {% set isActionable = session.typePaiement %} + {% set isActionable = session.deliveryPrice is not null and session.typePaiement %}
@@ -36,16 +43,26 @@ - {# BILLING CONFIG #} + {# BILLING & DELIVERY CONFIG #}

- Configuration Facturation + Configuration Facturation & Livraison

-
+
+
+ + +
+
+ + +
-
- - -
+
+
+ +
+ {% if session.deliveryDistance is not null %} +
+

Détails Livraison

+ +
+ {# Map #} + {% if session.deliveryGeometry %} +
+ + +
+ {% endif %} + + {# Details #} +
+
+ Distance réelle (Aller) + {{ session.deliveryDistance|number_format(1, ',', ' ') }} km +
+
+ Franchise + - 10.0 km +
+
+ Distance facturée + {{ max(0, session.deliveryDistance - 10)|number_format(1, ',', ' ') }} km +
+
+ Trajets + 4 (2 A/R) +
+
+ + ({{ session.deliveryDistance|number_format(1) }} - 10) x 4 x 0.50€ = {{ session.deliveryPrice|number_format(2) }}€ + +
+
+
+
+ {% endif %} + +
diff --git a/templates/revervation/base.twig b/templates/revervation/base.twig index f0f8d9c..a9182c0 100644 --- a/templates/revervation/base.twig +++ b/templates/revervation/base.twig @@ -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') }} @@ -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') }} Panier diff --git a/templates/revervation/estimate_delivery.twig b/templates/revervation/estimate_delivery.twig new file mode 100644 index 0000000..4dd3322 --- /dev/null +++ b/templates/revervation/estimate_delivery.twig @@ -0,0 +1,134 @@ +{% extends 'revervation/base.twig' %} + +{% block title %}Estimer les frais de livraison | Ludik Event{% endblock %} + +{% block stylesheets %} + {{ parent() }} + +{% endblock %} + +{% block body %} +
+
+

+ Estimer la livraison +

+ +
+

+ Renseignez l'adresse de votre événement pour obtenir une estimation des frais de livraison. +

+ + {{ form_start(form, {'attr': {'class': 'space-y-6', 'data-turbo': 'false'}}) }} + +
+ {{ 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'}}) }} +
+ +
+
+ {{ 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'}}) }} +
+
+ {{ 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'}}) }} +
+
+ +
+ +
+ + {{ form_end(form) }} + + {% if estimation is defined and estimation is not null %} +
+
+ Résultat de l'estimation + {% if details.isFree %} +
+ Offert ! + Zone gratuite +
+

Votre événement se trouve à moins de 10km de nos locaux.

+ {% else %} +

+ {{ estimation|format_currency('EUR') }} +

+ {% endif %} +
+ + {% if details is defined %} +
+

Détails du calcul

+ +
+ Distance réelle (Aller) + {{ details.distance|number_format(1, ',', ' ') }} km +
+ +
+ Franchise kilométrique + - 10.0 km (Offerts) +
+ +
+ +
+ Distance facturée + {{ details.chargedDistance|number_format(1, ',', ' ') }} km +
+ +
+ Nombre de trajets + {{ details.trips }} (2 A/R) +
+ +
+ Tarif kilométrique + {{ details.rate }} € / km +
+ + {% if not details.isFree %} +
+ + ({{ details.distance|number_format(1) }} - 10) x {{ details.trips }} x {{ details.rate }}€ = {{ estimation|number_format(2) }}€ + +
+ {% endif %} +
+ {% endif %} + + {% if geometry is defined and geometry is not null %} + + + {% endif %} + +
+

+ Cette estimation est donnée à titre indicatif et sera confirmée lors de la validation de votre devis. +

+
+
+ {% endif %} +
+
+
+{% endblock %} + +{% block javascripts %} + {{ parent() }} + {% if geometry is defined and geometry is not null %} + + {% endif %} +{% endblock %} diff --git a/templates/revervation/flow_confirmed.twig b/templates/revervation/flow_confirmed.twig index 9860c52..2b8eed6 100644 --- a/templates/revervation/flow_confirmed.twig +++ b/templates/revervation/flow_confirmed.twig @@ -273,6 +273,100 @@
+ {# Delivery Estimation #} + {% if delivery is defined and delivery.estimation is not null %} +
+
+
+ + + +
+

Estimation des frais de livraison

+
+ +
+ {# Details (60%) #} +
+ {% if delivery.details.isFree %} +
+
+ Offert ! + Zone gratuite +
+

Votre événement se trouve à moins de 10km de nos locaux.

+
+ {% else %} +
+ Coût estimé +

+ {{ delivery.estimation|format_currency('EUR') }} +

+
+ {% endif %} + +
+

Détails du calcul

+ +
+ Distance réelle (Aller) + {{ delivery.details.distance|number_format(1, ',', ' ') }} km +
+ +
+ Franchise kilométrique + - 10.0 km (Offerts) +
+ +
+ +
+ Distance facturée + {{ delivery.details.chargedDistance|number_format(1, ',', ' ') }} km +
+ +
+ Nombre de trajets + {{ delivery.details.trips }} (2 A/R) +
+ +
+ Tarif kilométrique + {{ delivery.details.rate }} € / km +
+
+ + {% if not delivery.details.isFree %} +
+

Formule appliquée

+ + ({{ delivery.details.distance|number_format(1) }} - 10) x {{ delivery.details.trips }} x {{ delivery.details.rate }}€ = {{ delivery.estimation|number_format(2) }}€ + +
+ {% endif %} + +
+

+ Cette estimation est indicative. Le montant définitif figurera sur votre devis. +

+
+
+ + {# Map (40%) #} +
+ {% if delivery.geometry %} + + + {% else %} +
+ Carte non disponible +
+ {% endif %} +
+
+
+ {% endif %}