diff --git a/assets/admin.js b/assets/admin.js
index 9bbe82f..254f4f7 100644
--- a/assets/admin.js
+++ b/assets/admin.js
@@ -8,7 +8,7 @@ import { CrmEditor } from "./libs/CrmEditor.js";
import { initTomSelect } from "./libs/initTomSelect.js";
import { SearchProduct,SearchOptions } from "./libs/SearchProduct.js";
import { SearchProductDevis,SearchOptionsDevis } from "./libs/SearchProductDevis.js";
-import { SearchProductFormule } from "./libs/SearchProductFormule.js";
+import { SearchProductFormule,SearchOptionsFormule } from "./libs/SearchProductFormule.js";
// --- INITIALISATION SENTRY ---
Sentry.init({
dsn: "https://803814be6540031b1c37bf92ba9c0f79@sentry.esy-web.dev/24",
@@ -95,6 +95,9 @@ function initAdminLayout() {
if (!customElements.get('search-productformule')) {
customElements.define('search-productformule', SearchProductFormule, { extends: 'button' });
}
+ if (!customElements.get('search-optionsformule')) {
+ customElements.define('search-optionsformule', SearchOptionsFormule, { extends: 'button' });
+ }
if (!customElements.get('search-options')) {
customElements.define('search-options', SearchOptions, { extends: 'button' });
}
diff --git a/assets/libs/SearchProductFormule.js b/assets/libs/SearchProductFormule.js
index 911b845..4f9d6bf 100644
--- a/assets/libs/SearchProductFormule.js
+++ b/assets/libs/SearchProductFormule.js
@@ -1,5 +1,147 @@
+export class SearchOptionsFormule extends HTMLButtonElement {
+ constructor() {
+ super();
+ this.allOptions = [];
+ }
+ connectedCallback() {
+ this.addEventListener('click', () => this.openModal());
+ }
+
+ async openModal() {
+ let modal = document.getElementById('modal-search-options');
+ if (!modal) {
+ modal = this.createModalStructure();
+ document.body.appendChild(modal);
+ this.setupSearchEvent(modal);
+ }
+
+ modal.classList.remove('hidden');
+ const container = modal.querySelector('#results-container-options');
+ const searchInput = modal.querySelector('#modal-search-input-options');
+
+ searchInput.value = '';
+ container.innerHTML = '
Chargement des options...
';
+
+ try {
+ const response = await fetch('/crm/options/json');
+ this.allOptions = await response.json();
+ this.renderOptions(this.allOptions, container, modal);
+ searchInput.focus();
+ } catch (error) {
+ container.innerHTML = 'Erreur catalogue options.
';
+ }
+ }
+
+ createModalStructure() {
+ const div = document.createElement('div');
+ div.id = 'modal-search-options';
+ div.className = 'fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-950/90 backdrop-blur-md hidden animate-in fade-in duration-300';
+ div.innerHTML = `
+
+
+
+
+
+ Sélection Option
+
+
+
+
+
+
+
+
+
+
+
Appuyez sur Échap pour fermer
+
+
+ `;
+ return div;
+ }
+
+ setupSearchEvent(modal) {
+ const input = modal.querySelector('#modal-search-input-options');
+ const container = modal.querySelector('#results-container-options');
+
+ input.oninput = () => {
+ const query = input.value.toLowerCase().trim();
+ const filtered = this.allOptions.filter(o =>
+ o.name.toLowerCase().includes(query)
+ );
+ this.renderOptions(filtered, container, modal);
+ };
+
+ window.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') modal.classList.add('hidden');
+ });
+ }
+
+ renderOptions(options, container, modal) {
+ container.innerHTML = '';
+
+ if (options.length === 0) {
+ container.innerHTML = 'Aucune option trouvée
';
+ return;
+ }
+
+ options.forEach(option => {
+ const card = document.createElement('div');
+ card.className = 'flex items-center gap-5 p-4 bg-white/5 border border-white/5 rounded-2xl hover:bg-blue-500/10 hover:border-blue-500/40 transition-all cursor-pointer group animate-in fade-in slide-in-from-bottom-2';
+
+ const imgHtml = option.image
+ ? `
`
+ : `OPT
`;
+
+ card.innerHTML = `
+ ${imgHtml}
+
+
${option.name}
+
+ PRIX HT: ${option.price}€
+
+
+
+ `;
+
+ card.onclick = () => {
+ this.fillOptionLine(option);
+ modal.classList.add('hidden');
+ };
+ container.appendChild(card);
+ });
+ }
+
+ fillOptionLine(option) {
+ // On cherche la ligne parente (ajuste le sélecteur si différent de celui des produits)
+ const row = this.closest('.form-repeater__row');
+ if (row) {
+ // Mapping selon ta structure de DevisOption
+ const nameInput = row.querySelector('input[name*="[product]"]');
+
+ if(nameInput) nameInput.value = option.name;
+
+ // Feedback visuel (Bleu pour les options)
+ const fieldset = row.querySelector('fieldset');
+ if (fieldset) {
+ fieldset.classList.add('border-blue-500/50', 'bg-blue-500/5');
+ setTimeout(() => fieldset.classList.remove('border-blue-500/50', 'bg-blue-500/5'), 800);
+ }
+ }
+ }
+}
export class SearchProductFormule extends HTMLButtonElement {
constructor() {
super();
diff --git a/config/packages/pwa.yaml b/config/packages/pwa.yaml
index bf44419..4076a9c 100644
--- a/config/packages/pwa.yaml
+++ b/config/packages/pwa.yaml
@@ -32,7 +32,7 @@ pwa:
enabled: true
name: "Réservation Lukikevent"
short_name: "Réservation Lukikevent"
- start_url: "reservation"
+ start_url: "/"
display: "standalone"
background_color: "#ffffff"
theme_color: "#f4c842"
diff --git a/migrations/Version20260128112815.php b/migrations/Version20260128112815.php
new file mode 100644
index 0000000..557710d
--- /dev/null
+++ b/migrations/Version20260128112815.php
@@ -0,0 +1,35 @@
+addSql('CREATE TABLE formules_options_inclus (id SERIAL NOT NULL, formule_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
+ $this->addSql('CREATE INDEX IDX_90546E632A68F4D1 ON formules_options_inclus (formule_id)');
+ $this->addSql('ALTER TABLE formules_options_inclus ADD CONSTRAINT FK_90546E632A68F4D1 FOREIGN KEY (formule_id) REFERENCES formules (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_options_inclus DROP CONSTRAINT FK_90546E632A68F4D1');
+ $this->addSql('DROP TABLE formules_options_inclus');
+ }
+}
diff --git a/src/Controller/Dashboard/FormulesController.php b/src/Controller/Dashboard/FormulesController.php
index d7ab894..69f66ee 100644
--- a/src/Controller/Dashboard/FormulesController.php
+++ b/src/Controller/Dashboard/FormulesController.php
@@ -3,6 +3,7 @@
namespace App\Controller\Dashboard;
use App\Entity\Formules;
+use App\Entity\FormulesOptionsInclus;
use App\Entity\FormulesProductInclus;
use App\Entity\Options;
use App\Entity\Product;
@@ -140,6 +141,25 @@ class FormulesController extends AbstractController
return $this->redirectToRoute('app_crm_formules_view', ['id' => $formules->getId()]);
}
+ if ($request->isMethod('POST') && $request->request->has('option')) {
+ $options = $request->request->all('option');
+ foreach ($options as $option) {
+ if(isset($option['id']) && $option['id'] !="") {
+ $productInclus = $entityManager->getRepository(FormulesOptionsInclus::class)->find($option['id']);
+ } else {
+ $productInclus = new FormulesOptionsInclus();
+ $productInclus->setFormule($formules);
+ }
+ $productInclus->setName($option['product']);
+ $entityManager->persist($productInclus);
+ }
+ $entityManager->flush();
+ $appLogger->record('UPDATE', 'Mise à jour des options inclus dans la formule' . $formules->getName());
+ $this->addFlash('success', 'Options mis à jour avec succès.');
+
+ return $this->redirectToRoute('app_crm_formules_view', ['id' => $formules->getId()]);
+ }
+
// 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')) {
@@ -178,15 +198,25 @@ class FormulesController extends AbstractController
'product' => '',
]
];
+ $options = [
+ [
+ 'product' => '',
+ ]
+ ];
foreach ($formules->getFormulesProductIncluses() as $key=>$fc){
$lines[$key]['product'] = $fc->getProduct()->getName();
$lines[$key]['id'] = $fc->getId();
}
+ foreach ($formules->getFormulesOptionsIncluses() as $key=>$fc){
+ $options[$key]['product'] = $fc->getName();
+ $options[$key]['id'] = $fc->getId();
+ }
return $this->render('dashboard/formules/view.twig', [
'formule' => $formules,
'form' => $form->createView(),
'type' => $formules->getType(),
'lines' => $lines,
+ 'option' => $options,
]);
}
diff --git a/src/Entity/Formules.php b/src/Entity/Formules.php
index 1f7ff04..7487593 100644
--- a/src/Entity/Formules.php
+++ b/src/Entity/Formules.php
@@ -63,9 +63,16 @@ class Formules
#[ORM\Column(nullable: true)]
private ?float $caution = null;
+ /**
+ * @var Collection
+ */
+ #[ORM\OneToMany(targetEntity: FormulesOptionsInclus::class, mappedBy: 'formule')]
+ private Collection $formulesOptionsIncluses;
+
public function __construct()
{
$this->formulesProductIncluses = new ArrayCollection();
+ $this->formulesOptionsIncluses = new ArrayCollection();
}
@@ -270,4 +277,34 @@ class Formules
return $this->id."-".$s->slugify($this->name);
}
+
+ /**
+ * @return Collection
+ */
+ public function getFormulesOptionsIncluses(): Collection
+ {
+ return $this->formulesOptionsIncluses;
+ }
+
+ public function addFormulesOptionsInclus(FormulesOptionsInclus $formulesOptionsInclus): static
+ {
+ if (!$this->formulesOptionsIncluses->contains($formulesOptionsInclus)) {
+ $this->formulesOptionsIncluses->add($formulesOptionsInclus);
+ $formulesOptionsInclus->setFormule($this);
+ }
+
+ return $this;
+ }
+
+ public function removeFormulesOptionsInclus(FormulesOptionsInclus $formulesOptionsInclus): static
+ {
+ if ($this->formulesOptionsIncluses->removeElement($formulesOptionsInclus)) {
+ // set the owning side to null (unless already changed)
+ if ($formulesOptionsInclus->getFormule() === $this) {
+ $formulesOptionsInclus->setFormule(null);
+ }
+ }
+
+ return $this;
+ }
}
diff --git a/src/Entity/FormulesOptionsInclus.php b/src/Entity/FormulesOptionsInclus.php
new file mode 100644
index 0000000..c693667
--- /dev/null
+++ b/src/Entity/FormulesOptionsInclus.php
@@ -0,0 +1,50 @@
+id;
+ }
+
+ public function getName(): ?string
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name): static
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ public function getFormule(): ?Formules
+ {
+ return $this->formule;
+ }
+
+ public function setFormule(?Formules $formule): static
+ {
+ $this->formule = $formule;
+
+ return $this;
+ }
+}
diff --git a/src/Repository/FormulesOptionsInclusRepository.php b/src/Repository/FormulesOptionsInclusRepository.php
new file mode 100644
index 0000000..f6ff27e
--- /dev/null
+++ b/src/Repository/FormulesOptionsInclusRepository.php
@@ -0,0 +1,43 @@
+
+ */
+class FormulesOptionsInclusRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, FormulesOptionsInclus::class);
+ }
+
+ // /**
+ // * @return FormulesOptionsInclus[] Returns an array of FormulesOptionsInclus 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): ?FormulesOptionsInclus
+ // {
+ // return $this->createQueryBuilder('f')
+ // ->andWhere('f.exampleField = :val')
+ // ->setParameter('val', $value)
+ // ->getQuery()
+ // ->getOneOrNullResult()
+ // ;
+ // }
+}
diff --git a/templates/dashboard/formules/config-pack.twig b/templates/dashboard/formules/config-pack.twig
index 6ca3995..179c625 100644
--- a/templates/dashboard/formules/config-pack.twig
+++ b/templates/dashboard/formules/config-pack.twig
@@ -76,3 +76,78 @@
+
+
diff --git a/templates/revervation/formule/show.twig b/templates/revervation/formule/show.twig
index 39cafe1..dd05906 100644
--- a/templates/revervation/formule/show.twig
+++ b/templates/revervation/formule/show.twig
@@ -24,13 +24,13 @@
{# Gauche : Image & Badge #}
-
+
{% if formule.imageName %}
 }})
+ class="w-full h-auto object-contain max-h-[600px] mx-auto transform group-hover:scale-105 transition-transform duration-700">
{% else %}
-
+
{% endif %}
@@ -90,39 +90,67 @@
- {# --- COMPOSITION DU PACK --- #}
-
-
-
-
- {# Liste des produits inclus #}
-
-
Équipements & Structures
- {% for item in formule.formulesProductIncluses %}
-
-
- {% if item.product.imageName %}
-
 }})
- {% endif %}
-
-
-
Réf: {{ item.product.ref }}
-
{{ item.product.name }}
-
-
-
- {% else %}
-
Aucun produit inclus.
- {% endfor %}
+ {% if formule.type == "pack" %}
+ {# --- COMPOSITION DU PACK --- #}
+
+
- {# BLOC INFOS COMPLEMENTAIRES #}
+
+ {# Liste des produits inclus #}
+
+
+ Structures & Matériel
+
+ {% for item in formule.formulesProductIncluses %}
+
+
+ {% if item.product.imageName %}
+
 }})
+ {% endif %}
+
+
+
Réf: {{ item.product.ref }}
+
{{ item.product.name }}
+
+
+
+ {% else %}
+
Aucun produit inclus.
+ {% endfor %}
+
+
+ {# Liste des options incluses #}
+
+
+ Services & Bonus Inclus
+
+ {% for option in formule.formulesOptionsIncluses %}
+
+
+
+
{{ option.name }}
+
Avantage inclus
+
+ {# Déco de fond #}
+
🎁
+
+ {% else %}
+
+
Aucun bonus supplémentaire
+
+ {% endfor %}
+
+
-
+ {% endif %}
{% endblock %}