feat(Product): Ajoute la relation avec l'entité FormulesProductInclus.
 feat(FormulesController): Crée le contrôleur pour gérer les formules.
 feat(templates): Ajoute le template pour afficher les formules dans le dashboard.
 feat(base.twig): Ajoute un lien vers la gestion des formules dans le menu.
⚙️ chore(vich_uploader): Configure vich uploader pour les images des formules.
```
This commit is contained in:
Serreau Jovann
2026-01-28 08:56:54 +01:00
parent ff9ae0e8d4
commit 349b5fc2cc
10 changed files with 516 additions and 0 deletions

View File

@@ -1,6 +1,10 @@
vich_uploader:
db_driver: orm
mappings:
image_formules:
uri_prefix: /images/image_formules
upload_destination: '%kernel.project_dir%/public/images/image_formules'
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
image_product:
uri_prefix: /images/image_product
upload_destination: '%kernel.project_dir%/public/images/image_product'

View File

@@ -0,0 +1,42 @@
<?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 Version20260128075215 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('CREATE TABLE formules (id SERIAL NOT NULL, name VARCHAR(255) NOT NULL, image_name VARCHAR(255) DEFAULT NULL, image_size INT DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, type VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('COMMENT ON COLUMN formules.updated_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('CREATE TABLE formules_product_inclus (id SERIAL NOT NULL, formules_id INT DEFAULT NULL, product_id INT DEFAULT NULL, config TEXT NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_BD36A828168F3793 ON formules_product_inclus (formules_id)');
$this->addSql('CREATE INDEX IDX_BD36A8284584665A ON formules_product_inclus (product_id)');
$this->addSql('COMMENT ON COLUMN formules_product_inclus.config IS \'(DC2Type:array)\'');
$this->addSql('ALTER TABLE formules_product_inclus ADD CONSTRAINT FK_BD36A828168F3793 FOREIGN KEY (formules_id) REFERENCES formules (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE formules_product_inclus ADD CONSTRAINT FK_BD36A8284584665A FOREIGN KEY (product_id) REFERENCES product (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
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_product_inclus DROP CONSTRAINT FK_BD36A828168F3793');
$this->addSql('ALTER TABLE formules_product_inclus DROP CONSTRAINT FK_BD36A8284584665A');
$this->addSql('DROP TABLE formules');
$this->addSql('DROP TABLE formules_product_inclus');
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Controller\Dashboard;
use App\Entity\Options;
use App\Entity\Product;
use App\Entity\ProductDoc;
use App\Form\OptionsType;
use App\Form\ProductDocType;
use App\Form\ProductType;
use App\Logger\AppLogger;
use App\Repository\FormulesRepository;
use App\Repository\OptionsRepository;
use App\Repository\ProductRepository;
use App\Service\Stripe\Client;
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\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route;
use Presta\SitemapBundle\Messenger\DumpSitemapMessage;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
class FormulesController extends AbstractController
{
// --- JSON ENDPOINTS ---
#[Route(path: '/crm/formules', name: 'app_crm_formules', methods: ['GET'])]
public function formules(PaginatorInterface $paginator,AppLogger $appLogger,Request $request,FormulesRepository $formulesRepository): Response
{
$appLogger->record('VIEW', 'Consultation des formules');
return $this->render('dashboard/formules.twig', [
'formules' => $paginator->paginate($formulesRepository->findBy([],['id'=>'asc']), $request->query->getInt('page', 1), 10),
]);
}
}

174
src/Entity/Formules.php Normal file
View File

@@ -0,0 +1,174 @@
<?php
namespace App\Entity;
use App\Repository\FormulesRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Attribute\Uploadable;
use Vich\UploaderBundle\Mapping\Attribute\UploadableField;
#[ORM\Entity(repositoryClass: FormulesRepository::class)]
#[Uploadable]
class Formules
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[UploadableField(mapping: 'image_formules', fileNameProperty: 'imageName', size: 'imageSize')]
private ?File $imageFile = null;
#[ORM\Column(nullable: true)]
private ?string $imageName = null;
#[ORM\Column(nullable: true)]
private ?int $imageSize = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $updatedAt = null;
#[ORM\Column(length: 255)]
private ?string $type = null;
/**
* @var Collection<int, FormulesProductInclus>
*/
#[ORM\OneToMany(targetEntity: FormulesProductInclus::class, mappedBy: 'formules')]
private Collection $formulesProductIncluses;
public function __construct()
{
$this->formulesProductIncluses = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
/**
* @return \DateTimeImmutable|null
*/
public function getUpdatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
/**
* @return File|null
*/
public function getImageFile(): ?File
{
return $this->imageFile;
}
/**
* @return string|null
*/
public function getImageName(): ?string
{
return $this->imageName;
}
/**
* @return int|null
*/
public function getImageSize(): ?int
{
return $this->imageSize;
}
/**
* @param \DateTimeImmutable|null $updatedAt
*/
public function setUpdatedAt(?\DateTimeImmutable $updatedAt): void
{
$this->updatedAt = $updatedAt;
}
/**
* @param File|null $imageFile
*/
public function setImageFile(?File $imageFile): void
{
$this->imageFile = $imageFile;
}
/**
* @param string|null $imageName
*/
public function setImageName(?string $imageName): void
{
$this->imageName = $imageName;
}
/**
* @param int|null $imageSize
*/
public function setImageSize(?int $imageSize): void
{
$this->imageSize = $imageSize;
}
public function getType(): ?string
{
return $this->type;
}
public function setType(string $type): static
{
$this->type = $type;
return $this;
}
/**
* @return Collection<int, FormulesProductInclus>
*/
public function getFormulesProductIncluses(): Collection
{
return $this->formulesProductIncluses;
}
public function addFormulesProductInclus(FormulesProductInclus $formulesProductInclus): static
{
if (!$this->formulesProductIncluses->contains($formulesProductInclus)) {
$this->formulesProductIncluses->add($formulesProductInclus);
$formulesProductInclus->setFormules($this);
}
return $this;
}
public function removeFormulesProductInclus(FormulesProductInclus $formulesProductInclus): static
{
if ($this->formulesProductIncluses->removeElement($formulesProductInclus)) {
// set the owning side to null (unless already changed)
if ($formulesProductInclus->getFormules() === $this) {
$formulesProductInclus->setFormules(null);
}
}
return $this;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Entity;
use App\Repository\FormulesProductInclusRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: FormulesProductInclusRepository::class)]
class FormulesProductInclus
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'formulesProductIncluses')]
private ?Formules $formules = null;
#[ORM\ManyToOne(inversedBy: 'formulesProductIncluses')]
private ?Product $PRODUCT = null;
#[ORM\Column(type: Types::ARRAY)]
private array $config = [];
public function getId(): ?int
{
return $this->id;
}
public function getFormules(): ?Formules
{
return $this->formules;
}
public function setFormules(?Formules $formules): static
{
$this->formules = $formules;
return $this;
}
public function getPRODUCT(): ?Product
{
return $this->PRODUCT;
}
public function setPRODUCT(?Product $PRODUCT): static
{
$this->PRODUCT = $PRODUCT;
return $this;
}
public function getConfig(): array
{
return $this->config;
}
public function setConfig(array $config): static
{
$this->config = $config;
return $this;
}
}

View File

@@ -81,10 +81,17 @@ class Product
#[ORM\Column(nullable: true)]
private ?float $dimP = null;
/**
* @var Collection<int, FormulesProductInclus>
*/
#[ORM\OneToMany(targetEntity: FormulesProductInclus::class, mappedBy: 'PRODUCT')]
private Collection $formulesProductIncluses;
public function __construct()
{
$this->productReserves = new ArrayCollection();
$this->productDocs = new ArrayCollection();
$this->formulesProductIncluses = new ArrayCollection();
}
public function slug()
{
@@ -363,4 +370,34 @@ class Product
return $this;
}
/**
* @return Collection<int, FormulesProductInclus>
*/
public function getFormulesProductIncluses(): Collection
{
return $this->formulesProductIncluses;
}
public function addFormulesProductInclus(FormulesProductInclus $formulesProductInclus): static
{
if (!$this->formulesProductIncluses->contains($formulesProductInclus)) {
$this->formulesProductIncluses->add($formulesProductInclus);
$formulesProductInclus->setPRODUCT($this);
}
return $this;
}
public function removeFormulesProductInclus(FormulesProductInclus $formulesProductInclus): static
{
if ($this->formulesProductIncluses->removeElement($formulesProductInclus)) {
// set the owning side to null (unless already changed)
if ($formulesProductInclus->getPRODUCT() === $this) {
$formulesProductInclus->setPRODUCT(null);
}
}
return $this;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\FormulesProductInclus;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<FormulesProductInclus>
*/
class FormulesProductInclusRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, FormulesProductInclus::class);
}
// /**
// * @return FormulesProductInclus[] Returns an array of FormulesProductInclus objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('f')
// ->andWhere('f.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('f.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?FormulesProductInclus
// {
// return $this->createQueryBuilder('f')
// ->andWhere('f.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\Formules;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Formules>
*/
class FormulesRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Formules::class);
}
// /**
// * @return Formules[] Returns an array of Formules objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('f')
// ->andWhere('f.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('f.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Formules
// {
// return $this->createQueryBuilder('f')
// ->andWhere('f.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -42,6 +42,7 @@
{{ menu.nav_link(path('app_crm'), 'Dashboard', '<path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>', 'app_crm') }}
{{ menu.nav_link(path('app_crm_product'), 'Produits', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_clients') }}
{{ menu.nav_link(path('app_crm_formules'), 'Formules', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_clients') }}
{# {{ menu.nav_link(path('app_crm_contrats'), 'Contrat de location', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_clients') }}#}
{# {{ menu.nav_link(path('app_crm_facture'), 'Facture', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_clients') }}#}
{# {{ menu.nav_link(path('app_crm_devis'), 'Devis', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_clients') }}#}

View File

@@ -0,0 +1,66 @@
{% extends 'dashboard/base.twig' %}
{% block title %}Catalogue Formules{% endblock %}
{% block title_header %}Gestion du <span class="text-blue-500">Formules</span>{% endblock %}
{% block actions %}
<div class="flex items-center space-x-3">
<a data-turbo="false" href="{{ path('app_crm_product_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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 4v16m8-8H4" />
</svg>
<span>Nouvelle Formules</span>
</a>
</div>
{% 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-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] 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 %}
{% 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>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{# PAGINATION #}
{% if formules.getTotalItemCount is defined and formules.getTotalItemCount > formules.getItemNumberPerPage %}
<div class="mt-8 flex justify-center custom-pagination">
{{ knp_pagination_render(formules) }}
</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 %}