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:
Serreau Jovann
2026-02-09 11:26:52 +01:00
parent 81c4fb0df9
commit e305c21e94
12 changed files with 433 additions and 13 deletions

View File

@@ -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>

View 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');
}
}

View 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');
}
}

View File

@@ -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
];

View File

@@ -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;
}
}

View 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,
]);
}
}

View File

@@ -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') }}

View 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 %}

View 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 %}

View File

@@ -141,8 +141,18 @@
</div>
<div class="mt-6 border-t border-slate-200 pt-4 space-y-2">
<div class="flex justify-between text-sm text-slate-600">
{% 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 {% if cart.discount > 0 %}Remisé{% endif %}</span>
<span class="font-medium">{{ cart.totalHT|number_format(2, ',', ' ') }} €</span>
</div>
{% if cart.tvaEnabled %}

View File

@@ -104,8 +104,18 @@
</div>
<div class="mt-6 border-t border-slate-200 pt-4 space-y-2">
<div class="flex justify-between text-sm text-slate-600">
{% 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 {% if cart.discount > 0 %}Remisé{% endif %}</span>
<span class="font-medium">{{ cart.totalHT|number_format(2, ',', ' ') }} €</span>
</div>
{% if cart.tvaEnabled %}

View File

@@ -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>