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

Détail des options inclus

+
+ +
    + {% for key,line in option %} +
  1. +
    + {% if line.id is defined %} + + {% endif %} + +
    + {# 1. PRODUIT / WYSIWYG #} +
    + +
    + {# Utilisation de l'input texte standard sans l'attribut 'is' #} + + + {# BOUTON RECHERCHER #} + +
    +
    + + {# 6. SUPPRIMER #} +
    + +
    +
    +
    +
  2. + {% endfor %} +
+ +
+ +
+ +
+
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 %} {{ formule.name }} + 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 --- #} -
-
-

Ce pack comprend

-
-
- -
- {# 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 --- #} +
+
+

Ce pack comprend

+
- {# 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 %}