+ ${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 %}
+
+
+
+
+
+ | Nom |
+ Pourcentage |
+ Période |
+ Actions |
+
+
+
+ {% for promotion in promotions %}
+
+ |
+ {{ promotion.name }}
+ |
+
+ -{{ promotion.percentage }}%
+ |
+
+
+ Du {{ promotion.dateStart|date('d/m/Y H:i') }}
+ Au {{ promotion.dateEnd|date('d/m/Y H:i') }}
+
+ |
+
+
+ |
+
+ {% else %}
+ | Aucune promotion |
+ {% endfor %}
+
+
+
+
+
+ {# PAGINATION #}
+ {% if promotions.getTotalItemCount is defined and promotions.getTotalItemCount > promotions.getItemNumberPerPage %}
+
+ {% 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 @@