feat(Formules.php): Ajoute les champs description et isPublish à l'entité Formules.
 feat(add.twig): Ajoute le champ description au formulaire d'ajout de formules.
 feat(FormulesType.php): Ajoute le champ description au formulaire FormulesType.
 feat(Dashboard/FormulesController.php): Gère l'ajout, la suppression et la vue des formules.
 feat(view.twig): Crée la vue pour modifier les détails d'une formule.
 feat(formules.twig): Affiche la liste des formules avec actions et statut.
```
This commit is contained in:
Serreau Jovann
2026-01-28 09:38:27 +01:00
parent fb608b79fe
commit c3f585bf2b
16 changed files with 417 additions and 29 deletions

View File

@@ -0,0 +1,32 @@
<?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 Version20260128082804 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 is_publish BOOLEAN 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 is_publish');
}
}

View File

@@ -0,0 +1,32 @@
<?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 Version20260128083151 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 description TEXT 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 description');
}
}

View File

@@ -38,19 +38,57 @@ class FormulesController extends AbstractController
'formules' => $paginator->paginate($formulesRepository->findBy([],['id'=>'asc']), $request->query->getInt('page', 1), 10),
]);
}
#[Route(path: '/crm/formules/add', name: 'app_crm_formules_add', methods: ['GET'])]
public function formulesAdd(PaginatorInterface $paginator,AppLogger $appLogger,Request $request,FormulesRepository $formulesRepository): Response
#[Route(path: '/crm/formules/delete/{id}', name: 'app_crm_formules_delete', methods: ['POST', 'GET'])]
public function formulesDelete(
?Formules $formules,
AppLogger $appLogger,
EntityManagerInterface $entityManager,
Request $request
): Response {
// 1. Vérification de l'existence de la formule
if (!$formules instanceof Formules) {
$this->addFlash('error', 'Formule introuvable.');
return $this->redirectToRoute('app_crm_formules');
}
// 2. Vérification du jeton CSRF (Sécurité contre les suppressions malveillantes)
// Note: Dans ton Twig, tu passes déjà le token en query string
if ($this->isCsrfTokenValid('delete' . $formules->getId(), $request->query->get('_token'))) {
$nomFormule = $formules->getName();
// 3. Suppression
foreach ($formules->getFormulesProductIncluses() as $formulesProductInclus)
$entityManager->remove($formulesProductInclus);
$entityManager->remove($formules);
$entityManager->flush();
// 4. Logging & Feedback
$appLogger->record('DELETE', 'Suppression définitive de la formule : ' . $nomFormule);
$this->addFlash('success', 'La formule "' . $nomFormule . '" a été supprimée.');
} else {
$this->addFlash('error', 'Jeton de sécurité invalide.');
}
return $this->redirectToRoute('app_crm_formules');
}
#[Route(path: '/crm/formules/add', name: 'app_crm_formules_add', methods: ['GET','POST'])]
public function formulesAdd(EntityManagerInterface $entityManager,PaginatorInterface $paginator,AppLogger $appLogger,Request $request,FormulesRepository $formulesRepository): Response
{
$appLogger->record('VIEW', 'Consultation page création formules');
$formules = new Formules();
$formules->setIsPublish(false);
$formules->setUpdatedAt(new \DateTimeImmutable());
$formules->setType($request->get('type',"pack")); // pack selected // free add listing allow product
$form = $this->createForm(FormulesType::class,$formules);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager->persist($formules);
$entityManager->flush();
$appLogger->record('CREATED', "Création de la formule : " . $formules->getName());
$this->addFlash("success", "La formule a été créée avec succès.");
return $this->redirectToRoute('app_crm_formules_view',['id'=>$formules->getId()]);
}
return $this->render('dashboard/formules/add.twig', [
@@ -60,4 +98,41 @@ 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
{
if (!$formules instanceof Formules) {
$this->addFlash('error', 'Formule introuvable.');
return $this->redirectToRoute('app_crm_formules');
}
$appLogger->record('VIEW', 'Consultation page formule ' . $formules->getName());
if ($request->get('act') === 'togglePublish') {
$status = $request->get('status') === 'true';
$formules->setIsPublish($status);
$entityManager->flush();
$appLogger->record('UPDATE', ($status ? 'Publication' : 'Désactivation') . ' de la formule ' . $formules->getName());
$this->addFlash('success', 'Statut mis à jour avec succès.');
return $this->redirectToRoute('app_crm_formules_view', ['id' => $formules->getId()]);
}
$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()]);
}
return $this->render('dashboard/formules/view.twig', [
'formule' => $formules,
'form' => $form->createView(),
'type' => $formules->getType(),
]);
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Entity;
use App\Repository\FormulesRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Attribute\Uploadable;
@@ -43,6 +44,12 @@ class Formules
#[ORM\OneToMany(targetEntity: FormulesProductInclus::class, mappedBy: 'formules')]
private Collection $formulesProductIncluses;
#[ORM\Column(nullable: true)]
private ?bool $isPublish = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;
public function __construct()
{
$this->formulesProductIncluses = new ArrayCollection();
@@ -171,4 +178,28 @@ class Formules
return $this;
}
public function isPublish(): ?bool
{
return $this->isPublish;
}
public function setIsPublish(bool $isPublish): static
{
$this->isPublish = $isPublish;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
}

View File

@@ -12,7 +12,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class AccountType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('username',TextType::class,[
@@ -33,7 +33,7 @@ class AccountType extends AbstractType
]);
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefault('data_class',Account::class);
}

View File

@@ -15,7 +15,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class CustomerAddAddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
@@ -50,7 +50,7 @@ class CustomerAddAddressType extends AbstractType
;
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => CustomerAddress::class,

View File

@@ -14,7 +14,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class CustomerAddType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('civ', ChoiceType::class, [
@@ -99,7 +99,7 @@ class CustomerAddType extends AbstractType
;
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Customer::class,

View File

@@ -14,7 +14,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class CustomerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('civ', ChoiceType::class, [
@@ -63,7 +63,7 @@ class CustomerType extends AbstractType
]);
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Customer::class,

View File

@@ -5,18 +5,22 @@ namespace App\Form;
use App\Entity\Formules;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FormulesType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name',TextType::class,[
'label' => 'Nom de la formule',
])
->add('description',TextareaType::class,[
'label' => 'Description de la formule',
])
->add('imageFile',FileType::class,[
'label' => 'Image de la formule',
'required' => false,
@@ -24,8 +28,8 @@ class FormulesType extends AbstractType
;
}
public function configureOptions(OptionsResolver $resolver)
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefault('data_class',Formules::class);
}
}

View File

@@ -14,7 +14,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class NewDevisType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('num', TextType::class, [

View File

@@ -18,7 +18,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class OptionsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name',TextType::class,[
@@ -36,7 +36,7 @@ class OptionsType extends AbstractType
;
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Options::class,

View File

@@ -18,7 +18,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class ProductDocType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
@@ -40,7 +40,7 @@ class ProductDocType extends AbstractType
;
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => ProductDoc::class,

View File

@@ -17,7 +17,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name',TextType::class,[
@@ -81,7 +81,7 @@ class ProductType extends AbstractType
;
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Product::class,

View File

@@ -1,15 +1,17 @@
{% extends 'dashboard/base.twig' %}
{% block title %}Catalogue Formules{% endblock %}
{% block title_header %}Gestion du <span class="text-blue-500">Formules</span>{% endblock %}
{% block title_header %}Gestion des <span class="text-blue-500 italic">Formules</span>{% endblock %}
{% block actions %}
<div class="flex items-center space-x-3">
<a data-turbo="false" href="{{ path('app_crm_formules_add') }}" class="hidden 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">
{# Bouton Nouvelle Formule avec animation au survol #}
<a data-turbo="false" href="{{ path('app_crm_formules_add', {type: 'pack'}) }}"
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 hover:-translate-y-0.5 active:translate-y-0">
<svg class="w-4 h-4 transform group-hover:rotate-90 transition-transform duration-300" 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 Formules</span>
<span>Nouvelle Formule</span>
</a>
</div>
{% endblock %}
@@ -22,17 +24,90 @@
<tr class="border-b border-white/5 bg-black/20">
<th class="px-6 py-5 text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">Visuel</th>
<th class="px-6 py-5 text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">Désignation</th>
<th class="px-6 py-5 text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">Type</th>
<th class="px-6 py-5 text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">Statut</th>
<th class="px-6 py-5 text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] text-center">Tarif</th>
<th class="px-6 py-5 text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-white/5">
{% for formule in formules %}
<tr class="group hover:bg-white/[0.02] transition-colors">
{# VISUEL #}
<td class="px-6 py-4">
<div class="h-14 w-14 rounded-2xl overflow-hidden border border-white/10 bg-slate-900 flex-shrink-0 shadow-inner group-hover:border-blue-500/30 transition-all">
{% if formule.imageName %}
<img src="{{ vich_uploader_asset(formule, 'imageFile') }}" class="h-full w-full object-cover">
{% else %}
<div class="h-full w-full flex items-center justify-center bg-slate-800 text-slate-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
</div>
{% endif %}
</div>
</td>
{# NOM & REF #}
<td class="px-6 py-4">
<div class="flex flex-col">
<span class="text-sm font-bold text-white group-hover:text-blue-400 transition-colors capitalize tracking-tight">
{{ formule.name }}
</span>
</div>
</td>
{# TYPE #}
<td class="px-6 py-4">
<span class="text-[10px] font-black uppercase tracking-tighter {{ formule.type == 'pack' ? 'text-blue-400' : 'text-purple-400' }}">
{{ formule.type }}
</span>
</td>
{# STATUT #}
<td class="px-6 py-4">
{% if formule.isPublish %}
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-[9px] font-black uppercase tracking-widest italic">
<span class="w-1.5 h-1.5 bg-emerald-400 rounded-full animate-pulse"></span>
En ligne
</span>
{% else %}
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-slate-500/10 border border-white/5 text-slate-500 text-[9px] font-black uppercase tracking-widest italic">
Brouillon
</span>
{% endif %}
</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') }}
</div>
</td>
{# ACTIONS #}
<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_formules_view', {id: formule.id}) }}" class="p-2.5 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>
<a data-turbo="false" href="{{ path('app_crm_formules_delete', {id: formule.id}) }}?_token={{ csrf_token('delete' ~ formule.id) }}"
data-turbo-method="post"
data-turbo-confirm="Supprimer la formule '{{ formule.name }}' ?"
class="p-2.5 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>
</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="py-24 text-center">
<p class="text-slate-500 italic uppercase tracking-[0.2em] text-[10px] font-black">Aucune formules</p>
<td colspan="6" class="py-32 text-center">
<div class="flex flex-col items-center justify-center space-y-4">
<div class="w-16 h-16 bg-white/5 rounded-full flex items-center justify-center border border-white/10">
<svg class="w-8 h-8 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
</div>
<p class="text-slate-500 italic uppercase tracking-[0.3em] text-[10px] font-black">Aucune formule dans le catalogue</p>
</div>
</td>
</tr>
{% endfor %}
@@ -48,8 +123,6 @@
</div>
{% endif %}
<style>
.custom-pagination nav ul { @apply flex space-x-2; }
.custom-pagination nav ul li span,

View File

@@ -77,6 +77,15 @@
}) }}
</div>
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Description détaillée</label>
{{ form_widget(form.description, {
'attr': {
'class': 'w-full bg-white/5 border border-white/10 rounded-xl py-4 px-5 text-white placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all min-h-[150px]',
'placeholder': 'Décrivez les avantages de cette formule...'
}
}) }}
</div>
<button type="submit" class="group relative w-full inline-flex items-center justify-center px-8 py-4 font-black text-[10px] uppercase tracking-widest text-white transition-all bg-blue-600 rounded-xl hover:bg-blue-500 shadow-[0_10px_20px_rgba(37,99,235,0.3)]">
Enregistrer la configuration
<svg class="w-4 h-4 ml-2 transform group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/></svg>

View File

@@ -0,0 +1,132 @@
{% extends 'dashboard/base.twig' %}
{% block title %}Modifier : {{ formule.name }}{% endblock %}
{% block actions %}
<div class="flex items-center space-x-4">
{# SYSTÈME DE STATUT TYPE 'ADMIN' ADAPTÉ AUX FORMULES #}
<div class="flex items-center">
{% if not formule.isPublish %}
<div class="flex items-center bg-white/5 backdrop-blur-xl border border-rose-500/30 rounded-2xl overflow-hidden shadow-xl">
<div class="flex items-center px-5 py-3 space-x-3 bg-rose-500/5">
<div class="w-2 h-2 rounded-full bg-rose-500 shadow-[0_0_8px_rgba(244,63,94,0.8)]"></div>
<span class="text-[10px] font-black text-rose-500 uppercase tracking-widest">Hors ligne</span>
</div>
<a data-turbo="false" href="{{ path('app_crm_formules_view', {id: formule.id, act: 'togglePublish', status: 'true'}) }}"
class="px-6 py-3 bg-emerald-600 hover:bg-emerald-500 text-white text-[10px] font-black uppercase tracking-widest transition-all active:scale-95 border-l border-white/5">
Publier
</a>
</div>
{% else %}
<div class="flex items-center bg-white/5 backdrop-blur-xl border border-emerald-500/30 rounded-2xl overflow-hidden shadow-xl">
<div class="flex items-center px-5 py-3 space-x-3 bg-emerald-500/5">
<div class="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.8)] animate-pulse"></div>
<span class="text-[10px] font-black text-emerald-400 uppercase tracking-widest">En ligne</span>
</div>
<a data-turbo="false" href="{{ path('app_crm_formules_view', {id: formule.id, act: 'togglePublish', status: 'false'}) }}"
onclick="return confirm('Voulez-vous vraiment masquer cette formule ?')"
class="px-6 py-3 bg-white/5 hover:bg-rose-600 text-slate-400 hover:text-white text-[10px] font-black uppercase tracking-widest transition-all active:scale-95 border-l border-white/10">
Désactiver
</a>
</div>
{% endif %}
</div>
<a href="{{ path('app_crm_formules') }}" class="px-6 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest text-slate-400 hover:text-white border border-white/5 hover:bg-white/5 transition-all">
Annuler
</a>
</div>
{% endblock %}
{% block body %}
<div class="min-h-screen p-6 flex flex-col items-center justify-start animate-in fade-in duration-500">
{# Header Infos #}
<div class="w-full mb-8 flex flex-col items-start gap-2">
<h1 class="text-4xl font-black text-white italic tracking-tighter uppercase">Modification</h1>
<div class="flex items-center space-x-3">
<p class="text-[10px] font-bold text-slate-500 uppercase tracking-[0.3em]">ID : #{{ formule.id|format("%04d") }}</p>
<span class="w-1 h-1 bg-slate-700 rounded-full"></span>
<p class="text-[10px] font-bold text-blue-500 uppercase tracking-[0.3em]">{{ type|default('PACK')|upper }}</p>
</div>
</div>
<div class="w-full space-y-6">
{{ form_start(form, {'attr': {'class': 'space-y-6'}}) }}
{# SECTION 01 : VISUEL #}
<div class="backdrop-blur-2xl bg-white/5 border border-white/10 rounded-[2.5rem] p-8 shadow-2xl relative group overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-blue-500/40 to-transparent"></div>
<h3 class="text-[10px] font-black text-blue-400 uppercase tracking-[0.2em] mb-8 flex items-center">
<span class="w-6 h-6 bg-blue-500/10 rounded flex items-center justify-center mr-3 font-mono text-blue-500">01</span>
Visuel de la formule
</h3>
<div class="flex flex-col md:flex-row items-center gap-10">
<div class="relative">
<div class="w-48 h-48 rounded-[2rem] overflow-hidden border-2 border-white/10 bg-slate-900 flex items-center justify-center shadow-2xl transition-transform duration-500 group-hover:scale-[1.02]">
{% if formule.imageName %}
<img src="{{ vich_uploader_asset(formule, 'imageFile') }}" class="w-full h-full object-cover">
{% else %}
<div class="text-slate-700">
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
</div>
{% endif %}
</div>
</div>
<div class="flex-1 w-full space-y-4">
<label class="text-[10px] font-black text-slate-500 uppercase tracking-widest">Remplacer l'image</label>
{{ form_widget(form.imageFile, {
'attr': {
'class': 'block w-full text-xs text-slate-400 file:mr-6 file:py-3 file:px-6 file:rounded-xl file:border file:border-white/10 file:text-[10px] file:font-black file:uppercase file:bg-white/5 file:text-white cursor-pointer transition-all hover:file:bg-white/10'
}
}) }}
<p class="text-[9px] text-slate-600 italic uppercase tracking-tighter">JPG, PNG ou WEBP • Max 2Mo</p>
</div>
</div>
</div>
{# SECTION 02 : CONTENU #}
<div class="backdrop-blur-2xl bg-white/5 border border-white/10 rounded-[2.5rem] p-8 shadow-2xl relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-purple-500/40 to-transparent"></div>
<h3 class="text-[10px] font-black text-purple-400 uppercase tracking-[0.2em] mb-8 flex items-center">
<span class="w-6 h-6 bg-purple-500/10 rounded flex items-center justify-center mr-3 font-mono text-purple-500">02</span>
Détails & Configuration
</h3>
<div class="grid grid-cols-1 gap-6">
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Nom du pack</label>
{{ form_widget(form.name, {
'attr': {'class': 'w-full bg-white/5 border border-white/10 rounded-xl py-4 px-5 text-white focus:ring-2 focus:ring-blue-500/50 outline-none transition-all placeholder-slate-700'}
}) }}
</div>
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Description catalogue</label>
{{ form_widget(form.description, {
'attr': {'class': 'w-full bg-white/5 border border-white/10 rounded-xl py-4 px-5 text-white focus:ring-2 focus:ring-blue-500/50 outline-none transition-all min-h-[150px]'}
}) }}
</div>
<button type="submit" class="group w-full py-5 bg-blue-600 hover:bg-blue-500 text-white font-black text-[11px] uppercase tracking-[0.3em] rounded-2xl shadow-xl shadow-blue-600/20 transition-all hover:-translate-y-1 active:scale-[0.98] flex items-center justify-center">
<span>Sauvegarder les modifications</span>
<svg class="w-4 h-4 ml-3 transform group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</button>
</div>
</div>
{# Champ caché pour la publication si tu l'utilises via le formulaire classique aussi #}
<div class="hidden">
{{ form_rest(form) }}
</div>
{{ form_end(form) }}
</div>
</div>
{% endblock %}