From e305c21e94c541927cfc7aa67666a764d84a5f44 Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Mon, 9 Feb 2026 11:26:52 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(promotion):=20Int=C3=A8gre=20l?= =?UTF-8?q?a=20gestion=20et=20l'application=20des=20promotions=20au=20flux?= =?UTF-8?q?=20de=20r=C3=A9servation=20et=20au=20calcul=20du=20panier.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/tools/FlowReserve.js | 17 +++- migrations/Version20260209101521.php | 31 ++++++ .../Dashboard/PromotionController.php | 95 +++++++++++++++++++ src/Controller/ReserverController.php | 68 +++++++++++-- src/Entity/OrderSession.php | 15 +++ src/Form/PromotionType.php | 43 +++++++++ templates/dashboard/base.twig | 1 + templates/dashboard/promotions.twig | 82 ++++++++++++++++ templates/dashboard/promotions/add.twig | 61 ++++++++++++ templates/revervation/flow.twig | 12 ++- templates/revervation/flow_confirmed.twig | 12 ++- templates/revervation/produit.twig | 9 ++ 12 files changed, 433 insertions(+), 13 deletions(-) create mode 100644 migrations/Version20260209101521.php create mode 100644 src/Controller/Dashboard/PromotionController.php create mode 100644 src/Form/PromotionType.php create mode 100644 templates/dashboard/promotions.twig create mode 100644 templates/dashboard/promotions/add.twig diff --git a/assets/tools/FlowReserve.js b/assets/tools/FlowReserve.js index dce73ab..da092bd 100644 --- a/assets/tools/FlowReserve.js +++ b/assets/tools/FlowReserve.js @@ -394,13 +394,28 @@ export class FlowReserve extends HTMLAnchorElement { // --- RENDER FOOTER (TOTALS) --- const total = data.total || {}; const hasTva = total.totalTva > 0; + const promotion = data.promotion; + + let promoHtml = ''; + let originalTotalHT = total.totalHT; + + if (promotion && total.discount > 0) { + originalTotalHT = total.totalHT + total.discount; + promoHtml = ` +
+ ${promotion.name} (-${promotion.percentage}%) + -${this.formatPrice(total.discount)} +
+ `; + } footer.innerHTML = `
Total HT - ${this.formatPrice(total.totalHT)} + ${this.formatPrice(originalTotalHT)}
+ ${promoHtml} ${hasTva ? `
TVA diff --git a/migrations/Version20260209101521.php b/migrations/Version20260209101521.php new file mode 100644 index 0000000..a634a56 --- /dev/null +++ b/migrations/Version20260209101521.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE order_session ADD promotion JSON DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE order_session DROP promotion'); + } +} diff --git a/src/Controller/Dashboard/PromotionController.php b/src/Controller/Dashboard/PromotionController.php new file mode 100644 index 0000000..3ccedb5 --- /dev/null +++ b/src/Controller/Dashboard/PromotionController.php @@ -0,0 +1,95 @@ +record('VIEW', 'Consultation des promotions'); + + $pagination = $paginator->paginate( + $promotionRepository->findBy([], ['dateStart' => 'DESC']), + $request->query->getInt('page', 1), + 10 + ); + + return $this->render('dashboard/promotions.twig', [ + 'promotions' => $pagination, + ]); + } + + #[Route('/add', name: 'app_crm_promotion_add', methods: ['GET', 'POST'])] + public function add(Request $request, EntityManagerInterface $entityManager, AppLogger $logger): Response + { + $promotion = new Promotion(); + $form = $this->createForm(PromotionType::class, $promotion); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->persist($promotion); + $entityManager->flush(); + + $logger->record('CREATE', "Nouvelle promotion : {$promotion->getName()}"); + $this->addFlash('success', 'Promotion créée avec succès.'); + + return $this->redirectToRoute('app_crm_promotion'); + } + + return $this->render('dashboard/promotions/add.twig', [ + 'promotion' => $promotion, + 'form' => $form, + 'is_edit' => false, + ]); + } + + #[Route('/{id}/edit', name: 'app_crm_promotion_edit', methods: ['GET', 'POST'])] + public function edit(Request $request, Promotion $promotion, EntityManagerInterface $entityManager, AppLogger $logger): Response + { + $form = $this->createForm(PromotionType::class, $promotion); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->flush(); + + $logger->record('UPDATE', "Modification promotion : {$promotion->getName()}"); + $this->addFlash('success', 'Promotion modifiée avec succès.'); + + return $this->redirectToRoute('app_crm_promotion'); + } + + return $this->render('dashboard/promotions/add.twig', [ + 'promotion' => $promotion, + 'form' => $form, + 'is_edit' => true, + ]); + } + + #[Route('/{id}', name: 'app_crm_promotion_delete', methods: ['POST'])] + public function delete(Request $request, Promotion $promotion, EntityManagerInterface $entityManager, AppLogger $logger): Response + { + if ($this->isCsrfTokenValid('delete'.$promotion->getId(), $request->request->get('_token'))) { + $name = $promotion->getName(); + $entityManager->remove($promotion); + $entityManager->flush(); + + $logger->record('DELETE', "Suppression promotion : $name"); + $this->addFlash('success', 'Promotion supprimée avec succès.'); + } + + return $this->redirectToRoute('app_crm_promotion'); + } +} diff --git a/src/Controller/ReserverController.php b/src/Controller/ReserverController.php index a28a7de..dcfc97d 100644 --- a/src/Controller/ReserverController.php +++ b/src/Controller/ReserverController.php @@ -6,6 +6,7 @@ use App\Entity\Customer; use App\Entity\CustomerTracking; use App\Entity\Product; use App\Entity\ProductReserve; +use App\Entity\Promotion; use App\Entity\SitePerformance; use App\Repository\CustomerRepository; use App\Repository\CustomerTrackingRepository; @@ -14,6 +15,7 @@ use App\Repository\OrderSessionRepository; use App\Repository\OptionsRepository; use App\Repository\ProductRepository; use App\Repository\ProductReserveRepository; +use App\Repository\PromotionRepository; use App\Service\Mailer\Mailer; use App\Service\Search\Client; use Doctrine\ORM\EntityManagerInterface; @@ -356,7 +358,8 @@ class ReserverController extends AbstractController Request $request, ProductRepository $productRepository, OptionsRepository $optionsRepository, - UploaderHelper $uploaderHelper + UploaderHelper $uploaderHelper, + PromotionRepository $promotionRepository ): Response { $data = json_decode($request->getContent(), true); $ids = $data['ids'] ?? []; @@ -374,7 +377,10 @@ class ReserverController extends AbstractController $foundIds = array_map(fn($p) => $p->getId(), $products); $removedIds = array_values(array_diff($ids, $foundIds)); - $cartData = $this->buildCartData($products, $selectedOptionsMap, $duration, $optionsRepository, $uploaderHelper); + $promotions = $promotionRepository->findActivePromotions(new \DateTime()); + $promotion = $promotions[0] ?? null; + + $cartData = $this->buildCartData($products, $selectedOptionsMap, $duration, $optionsRepository, $uploaderHelper, $promotion); return new JsonResponse([ 'start_date' => $startStr, @@ -382,12 +388,13 @@ class ReserverController extends AbstractController 'products' => $cartData['items'], 'options' => $cartData['rootOptions'], 'unavailable_products_ids' => $removedIds, - 'total' => $cartData['total'] + 'total' => $cartData['total'], + 'promotion' => $promotion ? ['name' => $promotion->getName(), 'percentage' => $promotion->getPercentage()] : null ]); } #[Route('/session', name: 'reservation_session_create', methods: ['POST'])] - public function createSession(Request $request, EntityManagerInterface $em, OrderSessionRepository $sessionRepository): Response + public function createSession(Request $request, EntityManagerInterface $em, OrderSessionRepository $sessionRepository, PromotionRepository $promotionRepository): Response { $data = json_decode($request->getContent(), true); $existingUuid = $request->getSession()->get('order_session_uuid'); @@ -408,6 +415,15 @@ class ReserverController extends AbstractController // Ensure promos key exists if sent (it should be in $data if frontend sends it) $session->setProducts($sessionData); + $promotions = $promotionRepository->findActivePromotions(new \DateTime()); + $promotion = $promotions[0] ?? null; + + if ($promotion) { + $session->setPromotion(['name' => $promotion->getName(), 'percentage' => $promotion->getPercentage()]); + } else { + $session->setPromotion(null); + } + $user = $this->getUser(); if ($user instanceof Customer) { $session->setCustomer($user); @@ -489,7 +505,15 @@ class ReserverController extends AbstractController $em->flush(); } - $cartData = $this->buildCartData($products, $selectedOptionsMap, $duration, $optionsRepository, $uploaderHelper); + $promoData = $session->getPromotion(); + $promotion = null; + if ($promoData) { + $promotion = new Promotion(); + $promotion->setName($promoData['name']); + $promotion->setPercentage($promoData['percentage']); + } + + $cartData = $this->buildCartData($products, $selectedOptionsMap, $duration, $optionsRepository, $uploaderHelper, $promotion); // --- Calcul Frais de Livraison --- $deliveryData = $this->calculateDelivery( @@ -510,6 +534,8 @@ class ReserverController extends AbstractController 'totalHT' => $cartData['total']['totalHT'], 'totalTva' => $cartData['total']['totalTva'], 'totalTTC' => $cartData['total']['totalTTC'], + 'discount' => $cartData['total']['discount'], + 'promotion' => $cartData['total']['promotion'], 'tvaEnabled' => $cartData['tvaEnabled'], ], 'delivery' => $deliveryData @@ -581,7 +607,15 @@ class ReserverController extends AbstractController // Added simple cleanup. } - $cartData = $this->buildCartData($products, $selectedOptionsMap, $duration, $optionsRepository, $uploaderHelper); + $promoData = $session->getPromotion(); + $promotion = null; + if ($promoData) { + $promotion = new Promotion(); + $promotion->setName($promoData['name']); + $promotion->setPercentage($promoData['percentage']); + } + + $cartData = $this->buildCartData($products, $selectedOptionsMap, $duration, $optionsRepository, $uploaderHelper, $promotion); return $this->render('revervation/flow.twig', [ 'session' => $session, @@ -596,6 +630,8 @@ class ReserverController extends AbstractController 'totalHT' => $cartData['total']['totalHT'], 'totalTva' => $cartData['total']['totalTva'], 'totalTTC' => $cartData['total']['totalTTC'], + 'discount' => $cartData['total']['discount'], + 'promotion' => $cartData['total']['promotion'], 'tvaEnabled' => $cartData['tvaEnabled'], ] ]); @@ -730,7 +766,7 @@ class ReserverController extends AbstractController } #[Route('/produit/{id}', name: 'reservation_product_show')] - public function revervationShowProduct(string $id, ProductRepository $productRepository): Response + public function revervationShowProduct(string $id, ProductRepository $productRepository, PromotionRepository $promotionRepository): Response { $parts = explode('-', $id); $realId = $parts[0]; @@ -747,10 +783,14 @@ class ReserverController extends AbstractController return $p->getId() !== $product->getId(); }); + $promotions = $promotionRepository->findActivePromotions(new \DateTime()); + $promotion = $promotions[0] ?? null; + return $this->render('revervation/produit.twig', [ 'product' => $product, 'tvaEnabled' => $this->isTvaEnabled(), - 'otherProducts' => array_slice($otherProducts, 0, 4) + 'otherProducts' => array_slice($otherProducts, 0, 4), + 'promotion' => $promotion ]); } @@ -1151,7 +1191,7 @@ class ReserverController extends AbstractController return $result; } - private function buildCartData(array $products, array $selectedOptionsMap, int $duration, OptionsRepository $optionsRepository, UploaderHelper $uploaderHelper): array + private function buildCartData(array $products, array $selectedOptionsMap, int $duration, OptionsRepository $optionsRepository, UploaderHelper $uploaderHelper, ?Promotion $promotion = null): array { $items = []; $rootOptions = []; @@ -1233,6 +1273,12 @@ class ReserverController extends AbstractController } } + $discountAmount = 0; + if ($promotion) { + $discountAmount = $totalHT * ($promotion->getPercentage() / 100); + $totalHT -= $discountAmount; + } + $totalTva = $totalHT * $tvaRate; $totalTTC = $totalHT + $totalTva; @@ -1242,7 +1288,9 @@ class ReserverController extends AbstractController 'total' => [ 'totalHT' => $totalHT, 'totalTva' => $totalTva, - 'totalTTC' => $totalTTC + 'totalTTC' => $totalTTC, + 'discount' => $discountAmount, + 'promotion' => $promotion ? $promotion->getName() : null ], 'tvaEnabled' => $tvaEnabled ]; diff --git a/src/Entity/OrderSession.php b/src/Entity/OrderSession.php index 2b029bf..f09f335 100644 --- a/src/Entity/OrderSession.php +++ b/src/Entity/OrderSession.php @@ -90,6 +90,9 @@ class OrderSession #[ORM\Column(length: 255, nullable: true)] private ?string $typePaiement = null; + #[ORM\Column(type: Types::JSON, nullable: true)] + private ?array $promotion = null; + #[ORM\OneToOne(mappedBy: 'orderSession', cascade: ['persist', 'remove'])] private ?Devis $devis = null; @@ -454,4 +457,16 @@ class OrderSession return $this; } + + public function getPromotion(): ?array + { + return $this->promotion; + } + + public function setPromotion(?array $promotion): static + { + $this->promotion = $promotion; + + return $this; + } } diff --git a/src/Form/PromotionType.php b/src/Form/PromotionType.php new file mode 100644 index 0000000..d8d5c8f --- /dev/null +++ b/src/Form/PromotionType.php @@ -0,0 +1,43 @@ +add('name', TextType::class, [ + 'label' => 'Nom de la promotion', + 'attr' => ['placeholder' => 'Ex: Soldes d\'été'], + ]) + ->add('percentage', NumberType::class, [ + 'label' => 'Pourcentage de réduction', + 'scale' => 2, + ]) + ->add('dateStart', DateTimeType::class, [ + 'widget' => 'single_text', + 'label' => 'Date de début', + ]) + ->add('dateEnd', DateTimeType::class, [ + 'widget' => 'single_text', + 'label' => 'Date de fin', + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Promotion::class, + ]); + } +} diff --git a/templates/dashboard/base.twig b/templates/dashboard/base.twig index e7140da..5586b5e 100644 --- a/templates/dashboard/base.twig +++ b/templates/dashboard/base.twig @@ -50,6 +50,7 @@ {{ menu.nav_link(path('app_template_point_controle_index'), 'Modèles de contrôle', '', 'app_template_point_controle_index') }} {{ menu.nav_link(path('app_crm_product'), 'Produits', '', 'app_crm_product') }} {{ menu.nav_link(path('app_crm_formules'), 'Formules', '', 'app_crm_formules') }} + {{ menu.nav_link(path('app_crm_promotion'), 'Promotions', '', 'app_crm_promotion') }} {{ menu.nav_link(path('app_crm_facture'), 'Facture', '', 'app_crm_facture') }} {{ menu.nav_link(path('app_crm_customer'), 'Clients', '', 'app_crm_customer') }} {{ menu.nav_link(path('app_crm_devis'), 'Devis', '', 'app_crm_devis') }} diff --git a/templates/dashboard/promotions.twig b/templates/dashboard/promotions.twig new file mode 100644 index 0000000..af28bc9 --- /dev/null +++ b/templates/dashboard/promotions.twig @@ -0,0 +1,82 @@ +{% extends 'dashboard/base.twig' %} + +{% block title %}Gestion des Promotions{% endblock %} +{% block title_header %}Gestion des Promotions{% endblock %} + +{% block actions %} + + + + + Nouvelle Promotion + +{% endblock %} + +{% block body %} +
+
+ + + + + + + + + + + {% for promotion in promotions %} + + + + + + + {% else %} + + {% endfor %} + +
NomPourcentagePériodeActions
+ {{ promotion.name }} + + -{{ promotion.percentage }}% + +
+ Du {{ promotion.dateStart|date('d/m/Y H:i') }} + Au {{ promotion.dateEnd|date('d/m/Y H:i') }} +
+
+
+ + + +
+ + +
+
+
Aucune promotion
+
+
+ + {# PAGINATION #} + {% if promotions.getTotalItemCount is defined and promotions.getTotalItemCount > promotions.getItemNumberPerPage %} +
{{ knp_pagination_render(promotions) }}
+ {% endif %} + + +{% endblock %} diff --git a/templates/dashboard/promotions/add.twig b/templates/dashboard/promotions/add.twig new file mode 100644 index 0000000..2f99593 --- /dev/null +++ b/templates/dashboard/promotions/add.twig @@ -0,0 +1,61 @@ +{% extends 'dashboard/base.twig' %} + +{% block title %}{{ is_edit ? 'Modifier' : 'Nouvelle' }} Promotion{% endblock %} +{% block title_header %}Gestion des Promotions{% endblock %} + +{% block body %} +
+ {{ form_start(form) }} + +
+

+ 01 + Détails de la promotion +

+ +
+
+ {{ form_label(form.name, 'Nom de la promotion', {'label_attr': {'class': 'text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }} + {{ form_widget(form.name, {'attr': {'placeholder': 'Ex: Soldes d\'été', 'class': 'w-full bg-slate-900/50 border-white/5 rounded-2xl text-white focus:ring-blue-500/20 focus:border-blue-500 transition-all py-3.5 px-5'}}) }} +
+ +
+ {{ form_label(form.percentage, 'Pourcentage de réduction', {'label_attr': {'class': 'text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }} +
+ {{ form_widget(form.percentage, {'attr': {'placeholder': 'Ex: 20', 'class': 'w-full bg-slate-900/50 border-white/5 rounded-2xl text-white focus:ring-blue-500/20 focus:border-blue-500 transition-all py-3.5 px-5'}}) }} + % +
+
+ +
{# Spacer #} + +
+ {{ form_label(form.dateStart, 'Date de début', {'label_attr': {'class': 'text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }} + {{ form_widget(form.dateStart, {'attr': {'class': 'w-full bg-slate-900/50 border-white/5 rounded-2xl text-white focus:ring-blue-500/20 focus:border-blue-500 transition-all py-3.5 px-5'}}) }} +
+ +
+ {{ form_label(form.dateEnd, 'Date de fin', {'label_attr': {'class': 'text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }} + {{ form_widget(form.dateEnd, {'attr': {'class': 'w-full bg-slate-900/50 border-white/5 rounded-2xl text-white focus:ring-blue-500/20 focus:border-blue-500 transition-all py-3.5 px-5'}}) }} +
+
+
+ + {# FOOTER ACTIONS #} +
+ + + Annuler + + +
+ + {{ form_end(form) }} +
+{% endblock %} diff --git a/templates/revervation/flow.twig b/templates/revervation/flow.twig index b3b2f35..faa0548 100644 --- a/templates/revervation/flow.twig +++ b/templates/revervation/flow.twig @@ -141,8 +141,18 @@
+ {% if cart.discount > 0 %} +
+ Total HT + {{ (cart.totalHT + cart.discount)|number_format(2, ',', ' ') }} € +
+
+ Promotion : {{ cart.promotion }} + -{{ cart.discount|number_format(2, ',', ' ') }} € +
+ {% endif %}
- Total HT + Total HT {% if cart.discount > 0 %}Remisé{% endif %} {{ cart.totalHT|number_format(2, ',', ' ') }} €
{% if cart.tvaEnabled %} diff --git a/templates/revervation/flow_confirmed.twig b/templates/revervation/flow_confirmed.twig index c645feb..b348258 100644 --- a/templates/revervation/flow_confirmed.twig +++ b/templates/revervation/flow_confirmed.twig @@ -104,8 +104,18 @@
+ {% if cart.discount > 0 %} +
+ Total HT + {{ (cart.totalHT + cart.discount)|number_format(2, ',', ' ') }} € +
+
+ Promotion : {{ cart.promotion }} + -{{ cart.discount|number_format(2, ',', ' ') }} € +
+ {% endif %}
- Total HT + Total HT {% if cart.discount > 0 %}Remisé{% endif %} {{ cart.totalHT|number_format(2, ',', ' ') }} €
{% if cart.tvaEnabled %} diff --git a/templates/revervation/produit.twig b/templates/revervation/produit.twig index f8facd0..cc9e621 100644 --- a/templates/revervation/produit.twig +++ b/templates/revervation/produit.twig @@ -102,6 +102,15 @@ Référence : {{ product.ref }} + + {% if promotion %} +
+ + PROMO : {{ promotion.name }} (-{{ promotion.percentage }}%) + +
+ {% endif %} +

{{ product.name }}