✨ feat(promotion): Intègre la gestion et l'application des promotions au flux de réservation et au calcul du panier.
This commit is contained in:
@@ -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 = `
|
||||
<div class="flex justify-between text-xs text-[#f39e36] font-bold uppercase">
|
||||
<span>${promotion.name} (-${promotion.percentage}%)</span>
|
||||
<span>-${this.formatPrice(total.discount)}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
footer.innerHTML = `
|
||||
<div class="space-y-2 mb-6">
|
||||
<div class="flex justify-between text-xs text-slate-500 font-bold uppercase">
|
||||
<span>Total HT</span>
|
||||
<span>${this.formatPrice(total.totalHT)}</span>
|
||||
<span>${this.formatPrice(originalTotalHT)}</span>
|
||||
</div>
|
||||
${promoHtml}
|
||||
${hasTva ? `
|
||||
<div class="flex justify-between text-xs text-slate-500 font-bold uppercase">
|
||||
<span>TVA</span>
|
||||
|
||||
31
migrations/Version20260209101521.php
Normal file
31
migrations/Version20260209101521.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260209101521 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
95
src/Controller/Dashboard/PromotionController.php
Normal file
95
src/Controller/Dashboard/PromotionController.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Dashboard;
|
||||
|
||||
use App\Entity\Promotion;
|
||||
use App\Form\PromotionType;
|
||||
use App\Logger\AppLogger;
|
||||
use App\Repository\PromotionRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/crm/promotions')]
|
||||
class PromotionController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_crm_promotion', methods: ['GET'])]
|
||||
public function index(PromotionRepository $promotionRepository, PaginatorInterface $paginator, Request $request, AppLogger $logger): Response
|
||||
{
|
||||
$logger->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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
43
src/Form/PromotionType.php
Normal file
43
src/Form/PromotionType.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Promotion;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class PromotionType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@
|
||||
{{ menu.nav_link(path('app_template_point_controle_index'), 'Modèles de contrôle', '<path stroke-linecap="round" stroke-linejoin="round" d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />', 'app_template_point_controle_index') }}
|
||||
{{ menu.nav_link(path('app_crm_product'), 'Produits', '<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />', 'app_crm_product') }}
|
||||
{{ menu.nav_link(path('app_crm_formules'), 'Formules', '<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />', 'app_crm_formules') }}
|
||||
{{ menu.nav_link(path('app_crm_promotion'), 'Promotions', '<path stroke-linecap="round" stroke-linejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />', 'app_crm_promotion') }}
|
||||
{{ menu.nav_link(path('app_crm_facture'), 'Facture', '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />', 'app_crm_facture') }}
|
||||
{{ menu.nav_link(path('app_crm_customer'), 'Clients', '<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />', 'app_crm_customer') }}
|
||||
{{ menu.nav_link(path('app_crm_devis'), 'Devis', '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 00-3.7-3.7 48.678 48.678 0 00-7.324 0 4.006 4.006 0 00-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3l-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 003.7 3.7 48.656 48.656 0 007.324 0 4.006 4.006 0 003.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3l-3 3" />', 'app_crm_devis') }}
|
||||
|
||||
82
templates/dashboard/promotions.twig
Normal file
82
templates/dashboard/promotions.twig
Normal file
@@ -0,0 +1,82 @@
|
||||
{% extends 'dashboard/base.twig' %}
|
||||
|
||||
{% block title %}Gestion des Promotions{% endblock %}
|
||||
{% block title_header %}Gestion des <span class="text-blue-500">Promotions</span>{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
<a data-turbo="false" href="{{ path('app_crm_promotion_add') }}" class="flex items-center space-x-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-black uppercase tracking-[0.2em] rounded-xl transition-all shadow-lg shadow-blue-600/20 group">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<span>Nouvelle Promotion</span>
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] overflow-hidden shadow-2xl animate-in fade-in duration-700">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b border-white/5 bg-black/20">
|
||||
<th class="px-6 py-5 text-[10px] font-black text-slate-300 uppercase tracking-[0.2em]">Nom</th>
|
||||
<th class="px-6 py-5 text-[10px] font-black text-slate-300 uppercase tracking-[0.2em]">Pourcentage</th>
|
||||
<th class="px-6 py-5 text-[10px] font-black text-slate-300 uppercase tracking-[0.2em]">Période</th>
|
||||
<th class="px-6 py-5 text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-white/5">
|
||||
{% for promotion in promotions %}
|
||||
<tr class="group hover:bg-white/[0.02] transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<span class="text-sm font-bold text-white group-hover:text-blue-400 transition-colors capitalize">{{ promotion.name }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 bg-purple-500/10 border border-purple-500/20 text-purple-400 rounded-lg text-[10px] font-black uppercase tracking-widest">-{{ promotion.percentage }}%</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex flex-col text-xs text-slate-400">
|
||||
<span>Du {{ promotion.dateStart|date('d/m/Y H:i') }}</span>
|
||||
<span>Au {{ promotion.dateEnd|date('d/m/Y H:i') }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
<a data-turbo="false" href="{{ path('app_crm_promotion_edit', {id: promotion.id}) }}" class="p-2 bg-blue-600/10 hover:bg-blue-600 text-blue-500 hover:text-white rounded-xl transition-all border border-blue-500/20 shadow-lg shadow-blue-600/5">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
|
||||
</a>
|
||||
<form method="post" action="{{ path('app_crm_promotion_delete', {id: promotion.id}) }}" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cette promotion ?');" class="inline-block">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ promotion.id) }}">
|
||||
<button type="submit" class="p-2 bg-rose-500/10 hover:bg-rose-500 text-rose-500 hover:text-white rounded-xl transition-all border border-rose-500/20 shadow-lg shadow-rose-500/5">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="4" class="py-24 text-center italic text-slate-300 text-[10px] font-black uppercase tracking-[0.2em]">Aucune promotion</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# PAGINATION #}
|
||||
{% if promotions.getTotalItemCount is defined and promotions.getTotalItemCount > promotions.getItemNumberPerPage %}
|
||||
<div class="mt-8 flex justify-center custom-pagination">{{ knp_pagination_render(promotions) }}</div>
|
||||
{% endif %}
|
||||
|
||||
<style>
|
||||
.custom-pagination nav ul { @apply flex space-x-2; }
|
||||
.custom-pagination nav ul li span,
|
||||
.custom-pagination nav ul li a {
|
||||
@apply px-4 py-2 rounded-xl bg-[#1e293b]/40 backdrop-blur-md border border-white/5 text-slate-400 text-xs font-bold transition-all;
|
||||
}
|
||||
.custom-pagination nav ul li.active span {
|
||||
@apply bg-blue-600 border-blue-500 text-white shadow-lg shadow-blue-600/20;
|
||||
}
|
||||
.custom-pagination nav ul li a:hover {
|
||||
@apply bg-white/10 text-white border-white/20;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
61
templates/dashboard/promotions/add.twig
Normal file
61
templates/dashboard/promotions/add.twig
Normal file
@@ -0,0 +1,61 @@
|
||||
{% extends 'dashboard/base.twig' %}
|
||||
|
||||
{% block title %}{{ is_edit ? 'Modifier' : 'Nouvelle' }} Promotion{% endblock %}
|
||||
{% block title_header %}Gestion des <span class="text-blue-500">Promotions</span>{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="max-w-4xl mx-auto animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
{{ form_start(form) }}
|
||||
|
||||
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] p-8 shadow-2xl">
|
||||
<h3 class="text-lg font-bold text-white mb-6 flex items-center">
|
||||
<span class="w-8 h-8 bg-blue-600/20 text-blue-500 rounded-lg flex items-center justify-center mr-3 text-[10px] font-black">01</span>
|
||||
Détails de la promotion
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="md:col-span-2">
|
||||
{{ 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'}}) }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ 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'}}) }}
|
||||
<div class="relative">
|
||||
{{ 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'}}) }}
|
||||
<span class="absolute right-5 top-1/2 -translate-y-1/2 text-slate-500 font-bold">%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div></div> {# Spacer #}
|
||||
|
||||
<div>
|
||||
{{ 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'}}) }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ 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'}}) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# FOOTER ACTIONS #}
|
||||
<div class="mt-12 mb-20 flex items-center justify-between backdrop-blur-xl bg-slate-900/40 p-6 rounded-[2rem] border border-white/5 shadow-xl">
|
||||
<a href="{{ path('app_crm_promotion') }}" class="text-slate-400 hover:text-white text-xs font-bold uppercase tracking-widest transition-colors flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg>
|
||||
Annuler
|
||||
</a>
|
||||
<button type="submit" class="relative overflow-hidden group px-12 py-4 bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-black uppercase tracking-[0.2em] rounded-2xl transition-all shadow-lg shadow-blue-600/30">
|
||||
<span class="relative z-10 flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" /></svg>
|
||||
{{ is_edit ? 'Enregistrer les modifications' : 'Créer la promotion' }}
|
||||
</span>
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000"></div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -141,8 +141,18 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-6 border-t border-slate-200 pt-4 space-y-2">
|
||||
{% if cart.discount > 0 %}
|
||||
<div class="flex justify-between text-sm text-slate-400">
|
||||
<span>Total HT</span>
|
||||
<span class="font-medium line-through">{{ (cart.totalHT + cart.discount)|number_format(2, ',', ' ') }} €</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm text-[#f39e36] font-bold">
|
||||
<span>Promotion : {{ cart.promotion }}</span>
|
||||
<span>-{{ cart.discount|number_format(2, ',', ' ') }} €</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex justify-between text-sm text-slate-600">
|
||||
<span>Total HT</span>
|
||||
<span>Total HT {% if cart.discount > 0 %}Remisé{% endif %}</span>
|
||||
<span class="font-medium">{{ cart.totalHT|number_format(2, ',', ' ') }} €</span>
|
||||
</div>
|
||||
{% if cart.tvaEnabled %}
|
||||
|
||||
@@ -104,8 +104,18 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-6 border-t border-slate-200 pt-4 space-y-2">
|
||||
{% if cart.discount > 0 %}
|
||||
<div class="flex justify-between text-sm text-slate-400">
|
||||
<span>Total HT</span>
|
||||
<span class="font-medium line-through">{{ (cart.totalHT + cart.discount)|number_format(2, ',', ' ') }} €</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm text-[#f39e36] font-bold">
|
||||
<span>Promotion : {{ cart.promotion }}</span>
|
||||
<span>-{{ cart.discount|number_format(2, ',', ' ') }} €</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex justify-between text-sm text-slate-600">
|
||||
<span>Total HT</span>
|
||||
<span>Total HT {% if cart.discount > 0 %}Remisé{% endif %}</span>
|
||||
<span class="font-medium">{{ cart.totalHT|number_format(2, ',', ' ') }} €</span>
|
||||
</div>
|
||||
{% if cart.tvaEnabled %}
|
||||
|
||||
@@ -102,6 +102,15 @@
|
||||
<span class="text-[11px] md:text-[12px] font-black text-slate-300 uppercase tracking-[0.4em] mb-4 block italic text-center md:text-left">
|
||||
Référence : {{ product.ref }}
|
||||
</span>
|
||||
|
||||
{% if promotion %}
|
||||
<div class="mb-6 text-center md:text-left">
|
||||
<span class="inline-block bg-gradient-to-r from-pink-500 to-rose-500 text-white px-4 py-2 rounded-xl text-[10px] md:text-xs font-black uppercase tracking-widest shadow-lg transform -rotate-2">
|
||||
PROMO : {{ promotion.name }} (-{{ promotion.percentage }}%)
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h1 class="text-5xl md:text-8xl font-black text-slate-900 uppercase italic tracking-tighter leading-[0.9] mb-10 text-center md:text-left">
|
||||
{{ product.name }}
|
||||
</h1>
|
||||
|
||||
Reference in New Issue
Block a user