feat(Formules.php): Ajoute les propriétés de prix et la fonction slug.
 feat(ReserverController.php): Affiche les formules sur la page d'accueil.
♻️ refactor(Dashboard/FormulesController.php): Gère le statut et les prix.
 feat(templates/dashboard): Affiche les tarifs des formules.
```
This commit is contained in:
Serreau Jovann
2026-01-28 10:00:58 +01:00
parent 0e03ca1fcd
commit aa75f290d0
8 changed files with 307 additions and 22 deletions

View File

@@ -0,0 +1,38 @@
<?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 Version20260128085506 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 formules ADD price1j DOUBLE PRECISION DEFAULT NULL');
$this->addSql('ALTER TABLE formules ADD price2j DOUBLE PRECISION DEFAULT NULL');
$this->addSql('ALTER TABLE formules ADD price5j DOUBLE PRECISION DEFAULT NULL');
$this->addSql('ALTER TABLE formules ADD caution DOUBLE PRECISION DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE formules DROP price1j');
$this->addSql('ALTER TABLE formules DROP price2j');
$this->addSql('ALTER TABLE formules DROP price5j');
$this->addSql('ALTER TABLE formules DROP caution');
}
}

View File

@@ -98,16 +98,15 @@ class FormulesController extends AbstractController
]);
}
#[Route(path: '/crm/formules/{id}', name: 'app_crm_formules_view', methods: ['GET'])]
public function formulesView(?Formules $formules,Request $request,EntityManagerInterface $entityManager, AppLogger $appLogger): Response
#[Route(path: '/crm/formules/{id}', name: 'app_crm_formules_view', methods: ['GET', 'POST'])]
public function formulesView(?Formules $formules, Request $request, EntityManagerInterface $entityManager, AppLogger $appLogger): Response
{
if (!$formules instanceof Formules) {
$this->addFlash('error', 'Formule introuvable.');
return $this->redirectToRoute('app_crm_formules');
}
$appLogger->record('VIEW', 'Consultation page formule ' . $formules->getName());
// 1. GESTION DU STATUT (Toggle Publish)
if ($request->get('act') === 'togglePublish') {
$status = $request->get('status') === 'true';
$formules->setIsPublish($status);
@@ -119,15 +118,39 @@ class FormulesController extends AbstractController
return $this->redirectToRoute('app_crm_formules_view', ['id' => $formules->getId()]);
}
$form = $this->createForm(FormulesType::class,$formules);
// 2. GESTION DES PRIX (Formulaire Manuel price[])
// On vérifie si le tableau 'price' existe dans la requête POST
if ($request->isMethod('POST') && $request->request->has('price')) {
$prices = $request->request->all('price');
// Mapping manuel des champs du tableau HTML vers l'entité
$formules->setPrice1j($prices['1j'] ?? $formules->getPrice1j());
$formules->setPrice2j($prices['2j'] ?? $formules->getPrice2j());
$formules->setPrice5j($prices['5j'] ?? $formules->getPrice5j());
$formules->setCaution($prices['caution'] ?? $formules->getCaution());
$entityManager->flush();
$appLogger->record('UPDATE', "Mise à jour des tarifs pour : " . $formules->getName());
$this->addFlash("success", "Les tarifs ont été mis à jour.");
return $this->redirectToRoute('app_crm_formules_view', ['id' => $formules->getId()]);
}
// 3. GESTION DU FORMULAIRE CLASSIQUE (Symfony Form)
$form = $this->createForm(FormulesType::class, $formules);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager->persist($formules);
$entityManager->flush();
$appLogger->record('CREATED', "Modification de la formule : " . $formules->getName());
$this->addFlash("success", "La formule a été modifié avec succès.");
return $this->redirectToRoute('app_crm_formules_view',['id'=>$formules->getId()]);
$appLogger->record('UPDATE', "Modification de la formule (infos) : " . $formules->getName());
$this->addFlash("success", "La formule a été modifiée avec succès.");
return $this->redirectToRoute('app_crm_formules_view', ['id' => $formules->getId()]);
}
return $this->render('dashboard/formules/view.twig', [
'formule' => $formules,
'form' => $form->createView(),

View File

@@ -13,6 +13,7 @@ use App\Form\RequestPasswordRequestType;
use App\Logger\AppLogger;
use App\Repository\CustomerRepository;
use App\Repository\CustomerTrackingRepository;
use App\Repository\FormulesRepository;
use App\Repository\ProductRepository;
use App\Service\Mailer\Mailer;
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
@@ -56,11 +57,13 @@ class ReserverController extends AbstractController
]);
}
#[Route('/reservation', name: 'reservation')]
public function revervation(ProductRepository $productRepository): Response
public function revervation(FormulesRepository $formulesRepository,ProductRepository $productRepository): Response
{
$products =$productRepository->findBy([], ['updatedAt' => 'DESC'],3);
$formules =$formulesRepository->findBy(['isPublish'=>true], ['updatedAt' => 'DESC'],3);
return $this->render('revervation/home.twig',[
'products' => $products
'products' => $products,
'formules' => $formules,
]);
}
#[Route('/reservation/web-vitals', name: 'reservation_web-vitals', methods: ['POST'])]
@@ -148,6 +151,13 @@ class ReserverController extends AbstractController
return $this->render('revervation/formules.twig',[
]);
}
#[Route('/reservation/formules/{slug}', name: 'reservation_formule_show')]
public function revervationView(): Response
{
return $this->render('revervation/formules.twig',[
]);
}
#[Route('/reservation/comment-reserver', name: 'reservation_workflow')]
public function revervationWorkfkow(): Response
{

View File

@@ -3,6 +3,7 @@
namespace App\Entity;
use App\Repository\FormulesRepository;
use Cocur\Slugify\Slugify;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
@@ -50,6 +51,18 @@ class Formules
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;
#[ORM\Column(nullable: true)]
private ?float $price1j = null;
#[ORM\Column(nullable: true)]
private ?float $price2j = null;
#[ORM\Column(nullable: true)]
private ?float $price5j = null;
#[ORM\Column(nullable: true)]
private ?float $caution = null;
public function __construct()
{
$this->formulesProductIncluses = new ArrayCollection();
@@ -202,4 +215,59 @@ class Formules
return $this;
}
public function getPrice1j(): ?float
{
return $this->price1j;
}
public function setPrice1j(?float $price1j): static
{
$this->price1j = $price1j;
return $this;
}
public function getPrice2j(): ?float
{
return $this->price2j;
}
public function setPrice2j(?float $price2j): static
{
$this->price2j = $price2j;
return $this;
}
public function getPrice5j(): ?float
{
return $this->price5j;
}
public function setPrice5j(?float $price5j): static
{
$this->price5j = $price5j;
return $this;
}
public function getCaution(): ?float
{
return $this->caution;
}
public function setCaution(?float $caution): static
{
$this->caution = $caution;
return $this;
}
public function slug()
{
$s = new Slugify();
return $this->id."-".$s->slugify($this->name);
}
}

View File

@@ -77,9 +77,25 @@
</td>
{# TARIF #}
<td class="px-6 py-4 text-center">
<div class="inline-block px-4 py-1.5 rounded-xl bg-white/5 border border-white/5 font-black text-white italic text-sm group-hover:border-blue-500/20 transition-all">
{{ formule.price|default('0.00') }}
<td class="px-6 py-4">
<div class="flex items-center justify-center space-x-2">
{# 1 JOUR #}
<div class="flex flex-col items-center px-3 py-1.5 rounded-xl bg-white/5 border border-white/5 group-hover:border-blue-500/20 transition-all">
<span class="text-[8px] font-black text-slate-500 uppercase tracking-tighter">1J</span>
<span class="text-xs font-black text-white italic">{{ formule.price1j|default('0') }}€</span>
</div>
{# 2 JOURS #}
<div class="flex flex-col items-center px-3 py-1.5 rounded-xl bg-white/5 border border-white/5 group-hover:border-blue-500/20 transition-all">
<span class="text-[8px] font-black text-slate-500 uppercase tracking-tighter">2J</span>
<span class="text-xs font-black text-blue-400 italic">{{ formule.price2j|default('0') }}€</span>
</div>
{# 5 JOURS #}
<div class="flex flex-col items-center px-3 py-1.5 rounded-xl bg-emerald-500/5 border border-emerald-500/10 group-hover:border-emerald-500/30 transition-all">
<span class="text-[8px] font-black text-emerald-500/50 uppercase tracking-tighter">5J</span>
<span class="text-xs font-black text-emerald-400 italic">{{ formule.price5j|default('0') }}€</span>
</div>
</div>
</td>

View File

@@ -1,3 +1,71 @@
// add line for price + caution
// add line for product include in pack
// add line for options include in pack
{# SECTION 03 : FORMULAIRE TARIFICATION (AUTONOME) #}
<form action="{{ path('app_crm_formules_view', {id: formule.id}) }}" method="POST"
class="w-full backdrop-blur-2xl bg-white/5 border border-white/10 rounded-[2.5rem] p-8 shadow-2xl relative overflow-hidden mt-6">
{# Ligne décorative émeraude pour le "Money" #}
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-emerald-500/40 to-transparent"></div>
<div class="flex items-center justify-between mb-8">
<h3 class="text-[10px] font-black text-emerald-400 uppercase tracking-[0.2em] flex items-center">
<span class="w-6 h-6 bg-emerald-500/10 rounded flex items-center justify-center mr-3 font-mono text-emerald-500">03</span>
Grille Tarifaire & Caution
</h3>
{# Badge de rappel du type #}
<span class="text-[9px] font-bold text-slate-500 uppercase px-3 py-1 bg-white/5 rounded-lg border border-white/10 italic">
Tarification dégressive
</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
{# Prix 1 Jour #}
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Tarif 1 Jour</label>
<div class="relative">
<input type="number" step="0.01" name="price[1j]" value="{{ formule.price1j|default('0.00') }}"
class="w-full bg-white/5 border border-white/10 rounded-xl py-4 pl-5 pr-12 text-white font-bold focus:ring-2 focus:ring-emerald-500/50 outline-none transition-all">
<span class="absolute right-5 top-1/2 -translate-y-1/2 text-[10px] font-black text-slate-600">€</span>
</div>
</div>
{# Prix 2 Jours #}
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Tarif 2 Jours</label>
<div class="relative">
<input type="number" step="0.01" name="price[2j]" value="{{ formule.price2j|default('0.00') }}"
class="w-full bg-white/5 border border-white/10 rounded-xl py-4 pl-5 pr-12 text-white font-bold focus:ring-2 focus:ring-emerald-500/50 outline-none transition-all">
<span class="absolute right-5 top-1/2 -translate-y-1/2 text-[10px] font-black text-slate-600">€</span>
</div>
</div>
{# Prix 5 Jours #}
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Tarif 5 Jours</label>
<div class="relative">
<input type="number" step="0.01" name="price[5j]" value="{{ formule.price5j|default('0.00') }}"
class="w-full bg-white/5 border border-white/10 rounded-xl py-4 pl-5 pr-12 text-white font-bold focus:ring-2 focus:ring-emerald-500/50 outline-none transition-all">
<span class="absolute right-5 top-1/2 -translate-y-1/2 text-[10px] font-black text-slate-600">€</span>
</div>
</div>
{# Caution #}
<div class="space-y-2">
<label class="text-[10px] font-black text-rose-400 uppercase tracking-widest ml-1 italic">Garantie (Caution)</label>
<div class="relative">
<input type="number" step="1" name="price[caution]" value="{{ formule.caution|default('0') }}"
class="w-full bg-rose-500/5 border border-rose-500/20 rounded-xl py-4 pl-5 pr-12 text-rose-500 font-black focus:ring-2 focus:ring-rose-500/50 outline-none transition-all">
<span class="absolute right-5 top-1/2 -translate-y-1/2 text-[10px] font-black text-rose-500/50">€</span>
</div>
</div>
</div>
{# Bouton de sauvegarde spécifique aux prix #}
<div class="flex justify-end">
<button type="submit" class="group flex items-center space-x-3 px-8 py-4 bg-emerald-600 hover:bg-emerald-500 text-white rounded-2xl transition-all shadow-lg shadow-emerald-900/20 active:scale-95">
<span class="text-[10px] font-black uppercase tracking-widest">Mettre à jour la tarification</span>
<svg class="w-4 h-4 transform group-hover:rotate-12 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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" />
</svg>
</button>
</div>
</form>

View File

@@ -128,6 +128,7 @@
{{ form_end(form) }}
</div>
</div>
{% include 'dashboard/formules/config-'~formule.type~".twig" %}
</div>
{% endblock %}

View File

@@ -202,11 +202,72 @@
</a>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
{# Message si aucun produit (Correction contraste) #}
<div class="col-span-full py-20 text-center bg-white rounded-[3rem] border-2 border-dashed border-gray-200">
<p class="text-gray-600 font-bold italic uppercase tracking-widest">Le formules est en cours de mise à jour...</p>
{% for formule in formules %}
<div class="group bg-white rounded-[3rem] p-4 border border-gray-100 shadow-sm hover:shadow-2xl transition-all duration-500 flex flex-col">
{# Conteneur Image #}
<div class="relative overflow-hidden rounded-[2.5rem] aspect-[4/5] bg-slate-50 flex items-center justify-center">
{# Utilisation du champ imageName de la formule #}
{% if formule.imageName and formule.imageName != "" %}
<img src="{{ vich_uploader_asset(formule,'imageFile') | imagine_filter('product_card') }}"
alt="{{ formule.name }}"
loading="lazy"
class="w-full h-full object-cover transform group-hover:scale-110 transition-transform duration-700">
{% else %}
<div class="flex flex-col items-center justify-center p-12 text-center">
<img src="{{ asset('provider/images/favicon.png') }}"
alt="Image par défaut"
class="w-32 h-32 object-contain opacity-20 group-hover:opacity-100 transition-all duration-500 grayscale group-hover:grayscale-0">
</div>
{% endif %}
{# Badge Type de Formule (ex: PACK) #}
<div class="absolute top-5 left-5 bg-[#0782bc] text-white px-4 py-1.5 rounded-full shadow-lg border border-white/20">
<p class="text-[9px] font-black uppercase tracking-[0.2em] italic">
{{ formule.type|default('Pack') }}
</p>
</div>
</div>
{# Infos Formule #}
<div class="mt-6 px-2 pb-2 flex-grow flex flex-col">
<div class="mb-4">
<h3 class="text-2xl font-black text-gray-900 group-hover:text-[#f39e36] transition-colors leading-tight italic uppercase tracking-tighter">
{{ formule.name }}
</h3>
</div>
{# Section Tarifs dégressifs #}
<div class="grid grid-cols-3 gap-2 mb-6">
<div class="bg-gray-50 rounded-2xl p-2 border border-gray-100 text-center">
<p class="text-[8px] font-black text-gray-400 uppercase">1 Jour</p>
<p class="text-xs font-black text-[#0782bc]">{{ formule.price1j|format_currency('EUR') }}</p>
</div>
<div class="bg-blue-50/50 rounded-2xl p-2 border border-blue-100 text-center">
<p class="text-[8px] font-black text-gray-400 uppercase">2 Jours</p>
<p class="text-xs font-black text-[#0782bc]">{{ formule.price2j|format_currency('EUR') }}</p>
</div>
<div class="bg-emerald-50/50 rounded-2xl p-2 border border-emerald-100 text-center">
<p class="text-[8px] font-black text-gray-400 uppercase">5 Jours</p>
<p class="text-xs font-black text-emerald-600">{{ formule.price5j|format_currency('EUR') }}</p>
</div>
</div>
<div class="mt-auto">
<a href="{{ path('reservation_formule_show',{slug: formule.slug}) }}" class="block w-full py-3 bg-[#f39e36] text-white text-center rounded-[1.5rem] font-black uppercase text-sm tracking-widest hover:bg-blue-600 transition-all shadow-xl hover:shadow-blue-200 active:scale-95">
Découvrir le pack
</a>
</div>
</div>
</div>
{% else %}
<div class="col-span-full py-20 text-center bg-white rounded-[3rem] border-2 border-dashed border-gray-200">
<p class="text-gray-600 font-bold italic uppercase tracking-widest">Les formules sont en cours de mise à jour...</p>
</div>
{% endfor %}
</div>
</section>
</div>