diff --git a/assets/admin.js b/assets/admin.js index 5d37a0f..e614706 100644 --- a/assets/admin.js +++ b/assets/admin.js @@ -5,6 +5,7 @@ import TomSelect from "tom-select"; import { RepeatLine } from "./libs/RepeatLine.js"; import { DevisManager } from "./libs/DevisManager.js"; import { initTomSelect } from "./libs/initTomSelect.js"; +import { SearchProduct } from "./libs/SearchProduct.js"; // --- INITIALISATION SENTRY --- Sentry.init({ dsn: "https://803814be6540031b1c37bf92ba9c0f79@sentry.esy-web.dev/24", @@ -36,6 +37,9 @@ function initAdminLayout() { customElements.define('devis-manager', DevisManager, { extends: 'div' }); } + if (!customElements.get('search-product')) { + customElements.define('search-product', SearchProduct, { extends: 'button' }); + } // Sidebar & UI const sidebar = document.getElementById('sidebar'); const overlay = document.getElementById('sidebar-overlay'); diff --git a/assets/libs/SearchProduct.js b/assets/libs/SearchProduct.js new file mode 100644 index 0000000..62e3df3 --- /dev/null +++ b/assets/libs/SearchProduct.js @@ -0,0 +1,143 @@ +export class SearchProduct extends HTMLButtonElement { + constructor() { + super(); + this.allProducts = []; // Stockage local pour la recherche + } + + connectedCallback() { + this.addEventListener('click', () => this.openModal()); + } + + async openModal() { + let modal = document.getElementById('modal-search-product'); + if (!modal) { + modal = this.createModalStructure(); + document.body.appendChild(modal); + this.setupSearchEvent(modal); + } + + modal.classList.remove('hidden'); + const container = modal.querySelector('#results-container'); + const searchInput = modal.querySelector('#modal-search-input'); + + searchInput.value = ''; // Reset recherche + container.innerHTML = '
Synchronisation catalogue...
'; + + try { + const response = await fetch('/crm/products/json'); + this.allProducts = await response.json(); + this.renderProducts(this.allProducts, container, modal); + searchInput.focus(); + } catch (error) { + container.innerHTML = '
Erreur catalogue.
'; + } + } + + createModalStructure() { + const div = document.createElement('div'); + div.id = 'modal-search-product'; + 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 Produit +

+ +
+ +
+ + + + +
+
+ +
+ +
+

Appuyez sur Échap pour fermer

+
+
+ `; + return div; + } + + setupSearchEvent(modal) { + const input = modal.querySelector('#modal-search-input'); + const container = modal.querySelector('#results-container'); + + input.oninput = () => { + const query = input.value.toLowerCase().trim(); + const filtered = this.allProducts.filter(p => + p.name.toLowerCase().includes(query) + ); + this.renderProducts(filtered, container, modal); + }; + + // Fermeture sur Echap + window.addEventListener('keydown', (e) => { + if (e.key === 'Escape') modal.classList.add('hidden'); + }); + } + + renderProducts(products, container, modal) { + container.innerHTML = ''; + + if (products.length === 0) { + container.innerHTML = '
Aucun produit trouvé
'; + return; + } + + products.forEach(product => { + 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-purple-500/10 hover:border-purple-500/40 transition-all cursor-pointer group animate-in fade-in slide-in-from-bottom-2'; + + const imgHtml = product.image + ? `` + : `
IMG
`; + + card.innerHTML = ` + ${imgHtml} +
+
${product.name}
+
+ 1J: ${product.price1day}€ + SUP: ${product.priceSup}€ + CAUTION: ${product.caution}€ +
+
+
+ +
+ `; + + card.onclick = () => { + this.fillFormLine(product); + modal.classList.add('hidden'); + }; + container.appendChild(card); + }); + } + + fillFormLine(product) { + const row = this.closest('.form-repeater__row'); + if (row) { + row.querySelector('input[name*="[name]"]').value = product.name; + row.querySelector('input[name*="[priceHt1Day]"]').value = product.price1day; + row.querySelector('input[name*="[priceHtSupDay]"]').value = product.priceSup; + row.querySelector('input[name*="[caution]"]').value = product.caution; + + const fieldset = row.querySelector('fieldset'); + fieldset.classList.add('border-emerald-500/50', 'bg-emerald-500/5'); + setTimeout(() => fieldset.classList.remove('border-emerald-500/50', 'bg-emerald-500/5'), 800); + } + } +} + diff --git a/migrations/Version20260121151117.php b/migrations/Version20260121151117.php new file mode 100644 index 0000000..5df288d --- /dev/null +++ b/migrations/Version20260121151117.php @@ -0,0 +1,35 @@ +addSql('CREATE TABLE contrats_line (id SERIAL NOT NULL, contrat_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, price1_day_ht DOUBLE PRECISION NOT NULL, price_sup_day_ht DOUBLE PRECISION NOT NULL, caution DOUBLE PRECISION NOT NULL, type VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_61D68661823061F ON contrats_line (contrat_id)'); + $this->addSql('ALTER TABLE contrats_line ADD CONSTRAINT FK_61D68661823061F FOREIGN KEY (contrat_id) REFERENCES contrats (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 contrats_line DROP CONSTRAINT FK_61D68661823061F'); + $this->addSql('DROP TABLE contrats_line'); + } +} diff --git a/src/Controller/Dashboard/ContratsController.php b/src/Controller/Dashboard/ContratsController.php index caae209..3f5276e 100644 --- a/src/Controller/Dashboard/ContratsController.php +++ b/src/Controller/Dashboard/ContratsController.php @@ -35,7 +35,19 @@ class ContratsController extends AbstractController $devis = $devisRepository->find($request->get('idDevis',0)); $c = new Contrats(); + $lines =[ + [ + 'id' => 0, + 'name' => '', + 'priceHt1Day' => 0, + 'priceHtSupDay' => 0, + 'caution' => 0, + ] + ]; if($devis instanceof Devis){ + $line = $devis->getDevisLines()[0]; + $c->setDateAt($line->getStartAt()); + $c->setEndAt($line->getEndAt()); $c->setCustomer($devis->getCustomer()); $c->setDevis($devis); $c->setAddressEvent($devis->getAddressShip()->getAddress()); @@ -43,6 +55,16 @@ class ContratsController extends AbstractController $c->setAddress3Event($devis->getAddressShip()->getAddress3()); $c->setZipCodeEvent($devis->getAddressShip()->getZipcode()); $c->setTownEvent($devis->getAddressShip()->getCity()); + $lines = []; + foreach ($devis->getDevisLines() as $line){ + $lines[] =[ + 'id' => $line->getId(), + 'name' => $line->getProduct()->getName()." - ".$line->getProduct()->getRef(), + 'priceHt1Day' => $line->getPriceHt(), + 'priceHtSupDay' => $line->getPriceHtSup(), + 'caution' => $line->getProduct()->getCaution(), + ]; + } } $form = $this->createForm(ContratsType::class,$c); @@ -53,6 +75,7 @@ class ContratsController extends AbstractController return $this->render('dashboard/contrats/add.twig',[ 'devis' => $devis, 'form'=> $form->createView(), + 'lines' => $lines, ]); } diff --git a/src/Controller/Dashboard/ProductController.php b/src/Controller/Dashboard/ProductController.php index 5d0cc09..2b03400 100644 --- a/src/Controller/Dashboard/ProductController.php +++ b/src/Controller/Dashboard/ProductController.php @@ -44,6 +44,7 @@ class ProductController extends AbstractController 'image' => $uploaderHelper->asset($product, 'imageFile'), 'price1day' => $product->getPriceDay(), 'priceSup' => $product->getPriceSup(), + 'caution' => $product->getCaution(), ]; } diff --git a/src/Entity/Contrats.php b/src/Entity/Contrats.php index cd93a77..f76d3b0 100644 --- a/src/Entity/Contrats.php +++ b/src/Entity/Contrats.php @@ -82,9 +82,16 @@ class Contrats #[ORM\Column] private ?\DateTimeImmutable $endAt = null; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: ContratsLine::class, mappedBy: 'contrat')] + private Collection $contratsLines; + public function __construct() { $this->contratsPayments = new ArrayCollection(); + $this->contratsLines = new ArrayCollection(); } public function getId(): ?int @@ -361,4 +368,34 @@ class Contrats return $this; } + + /** + * @return Collection + */ + public function getContratsLines(): Collection + { + return $this->contratsLines; + } + + public function addContratsLine(ContratsLine $contratsLine): static + { + if (!$this->contratsLines->contains($contratsLine)) { + $this->contratsLines->add($contratsLine); + $contratsLine->setContrat($this); + } + + return $this; + } + + public function removeContratsLine(ContratsLine $contratsLine): static + { + if ($this->contratsLines->removeElement($contratsLine)) { + // set the owning side to null (unless already changed) + if ($contratsLine->getContrat() === $this) { + $contratsLine->setContrat(null); + } + } + + return $this; + } } diff --git a/src/Entity/ContratsLine.php b/src/Entity/ContratsLine.php new file mode 100644 index 0000000..dcf6573 --- /dev/null +++ b/src/Entity/ContratsLine.php @@ -0,0 +1,110 @@ +id; + } + + public function getContrat(): ?Contrats + { + return $this->contrat; + } + + public function setContrat(?Contrats $contrat): static + { + $this->contrat = $contrat; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getPrice1DayHt(): ?float + { + return $this->price1DayHt; + } + + public function setPrice1DayHt(float $price1DayHt): static + { + $this->price1DayHt = $price1DayHt; + + return $this; + } + + public function getPriceSupDayHt(): ?float + { + return $this->priceSupDayHt; + } + + public function setPriceSupDayHt(float $priceSupDayHt): static + { + $this->priceSupDayHt = $priceSupDayHt; + + return $this; + } + + public function getCaution(): ?float + { + return $this->caution; + } + + public function setCaution(float $caution): static + { + $this->caution = $caution; + + return $this; + } + + public function getType(): ?string + { + return $this->type; + } + + public function setType(string $type): static + { + $this->type = $type; + + return $this; + } +} diff --git a/src/Repository/ContratsLineRepository.php b/src/Repository/ContratsLineRepository.php new file mode 100644 index 0000000..a35b696 --- /dev/null +++ b/src/Repository/ContratsLineRepository.php @@ -0,0 +1,43 @@ + + */ +class ContratsLineRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, ContratsLine::class); + } + + // /** + // * @return ContratsLine[] Returns an array of ContratsLine objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('c') + // ->andWhere('c.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('c.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?ContratsLine + // { + // return $this->createQueryBuilder('c') + // ->andWhere('c.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/templates/dashboard/contrats/add.twig b/templates/dashboard/contrats/add.twig index 0583700..88f1ed4 100644 --- a/templates/dashboard/contrats/add.twig +++ b/templates/dashboard/contrats/add.twig @@ -13,7 +13,7 @@ {% endblock %} {% block body %} -
+
{{ form_start(form) }}
@@ -43,14 +43,12 @@ {{ form_widget(form.notes, { 'attr': { 'class': 'w-full bg-rose-500/5 border border-rose-500/10 rounded-2xl text-rose-100 placeholder:text-rose-500/30 focus:ring-rose-500/20 focus:border-rose-500/40 transition-all py-3.5 px-5 text-xs italic', - 'placeholder': 'Ex: Client exigeant, chien, accès compliqué...', + 'placeholder': 'Client exigeant, chien, accès compliqué...', 'rows': '6' } }) }} -
- - - +
+
@@ -61,7 +59,7 @@ {# --- BLOC 02 & 03 : ADRESSE & TECHNIQUE --- #}
- {# LIEU DE L'ÉVÉNEMENT #} + {# LIEU DE L'ÉVÉNEMENT (Tous les champs d'adresse) #}

02 @@ -75,11 +73,11 @@

{{ form_label(form.address2Event, 'Complément d\'adresse 1', {'label_attr': {'class': 'text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }} - {{ form_widget(form.address2Event, {'attr': {'class': 'w-full bg-slate-950/50 border-white/5 rounded-2xl text-white focus:ring-blue-500/20 focus:border-blue-500 transition-all py-3.5 px-5'}}) }} + {{ form_widget(form.address2Event, {'attr': {'class': 'w-full bg-slate-950/50 border-white/5 rounded-2xl text-white focus:ring-blue-500/20 focus:border-blue-500 transition-all py-3.5 px-5', 'placeholder': 'Bâtiment, étage...'}}) }}
{{ form_label(form.address3Event, 'Complément d\'adresse 2', {'label_attr': {'class': 'text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }} - {{ form_widget(form.address3Event, {'attr': {'class': 'w-full bg-slate-950/50 border-white/5 rounded-2xl text-white focus:ring-blue-500/20 focus:border-blue-500 transition-all py-3.5 px-5'}}) }} + {{ form_widget(form.address3Event, {'attr': {'class': 'w-full bg-slate-950/50 border-white/5 rounded-2xl text-white focus:ring-blue-500/20 focus:border-blue-500 transition-all py-3.5 px-5', 'placeholder': 'Code, interphone...'}}) }}
{{ form_label(form.zipCodeEvent, 'Code Postal', {'label_attr': {'class': 'text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }} @@ -90,13 +88,13 @@ {{ form_widget(form.townEvent, {'attr': {'class': 'w-full bg-slate-950/50 border-white/5 rounded-2xl text-white focus:ring-blue-500/20 focus:border-blue-500 transition-all py-3.5 px-5'}}) }}
- {{ form_label(form.details, 'Précisions de livraison', {'label_attr': {'class': 'text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }} - {{ form_widget(form.details, {'attr': {'class': 'w-full bg-slate-950/50 border-white/5 rounded-2xl text-white focus:ring-blue-500/20 focus:border-blue-500 transition-all py-3.5 px-5', 'rows': '2'}}) }} + {{ form_label(form.details, 'Précisions de livraison / Notes d\'accès', {'label_attr': {'class': 'text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }} + {{ form_widget(form.details, {'attr': {'class': 'w-full bg-slate-950/50 border-white/5 rounded-2xl text-white focus:ring-blue-500/20 focus:border-blue-500 transition-all py-3.5 px-5', 'rows': '2', 'placeholder': 'Ex: Portail étroit, se garer dans la cour...'}}) }}
- {# CONTRAINTES TECHNIQUES #} + {# TECHNIQUE (Bloc 03) #}

03 @@ -116,7 +114,7 @@ {{ form_widget(form.access, {'attr': {'class': 'w-full bg-slate-950/50 border-white/5 rounded-2xl text-white focus:ring-amber-500/20 focus:border-amber-500 transition-all py-3.5 px-5'}}) }}

- {{ form_label(form.distancePower, 'Point électrique', {'label_attr': {'class': 'text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }} + {{ form_label(form.distancePower, 'Distance Point électrique', {'label_attr': {'class': 'text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }} {{ form_widget(form.distancePower, {'attr': {'class': 'w-full bg-slate-950/50 border-white/5 rounded-2xl text-white focus:ring-amber-500/20 focus:border-amber-500 transition-all py-3.5 px-5'}}) }}
@@ -124,31 +122,98 @@ - {# --- BLOC 04 : PÉRIODE DE LOCATION (Nouveau bloc Bento horizontal) --- #} + {# --- BLOC 04 : PÉRIODE --- #}

04 Période de location

-
+
{{ form_label(form.dateAt, 'Début de l\'événement', {'label_attr': {'class': 'text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] ml-1 mb-2 block text-center'}}) }} - {{ form_widget(form.dateAt, {'attr': {'class': 'w-full bg-slate-950/50 border-white/5 rounded-2xl text-white text-center focus:ring-blue-500/40 focus:border-blue-500 transition-all py-5 px-5 text-lg font-black'}}) }} + {{ form_widget(form.dateAt, {'attr': {'class': 'w-full bg-slate-950/50 border-white/5 rounded-2xl text-white text-center focus:ring-blue-500/40 focus:border-blue-500 transition-all py-4 px-5 font-black'}}) }}
-
+
{{ form_label(form.endAt, 'Fin de l\'événement', {'label_attr': {'class': 'text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] ml-1 mb-2 block text-center'}}) }} - {{ form_widget(form.endAt, {'attr': {'class': 'w-full bg-slate-950/50 border-white/5 rounded-2xl text-white text-center focus:ring-blue-500/40 focus:border-blue-500 transition-all py-5 px-5 text-lg font-black'}}) }} + {{ form_widget(form.endAt, {'attr': {'class': 'w-full bg-slate-950/50 border-white/5 rounded-2xl text-white text-center focus:ring-blue-500/40 focus:border-blue-500 transition-all py-4 px-5 font-black'}}) }}
- {# --- BARRE D'ACTIONS --- #} -
+ {# --- BLOC 05 : REPEATER PRESTATIONS --- #} +
+
+

+ 05 + Détail des prestations +

+
+ +
    + {% for key, line in lines %} +
  1. + +
    +
    + + {# 1. PRODUIT AVEC BOUTON RECHERCHE #} +
    + +
    + + + {# BOUTON RECHERCHER #} + +
    +
    + + {# 2. PRIX 1J #} +
    + + +
    + + {# 3. PRIX SUP #} +
    + + +
    + + {# 4. CAUTION #} +
    + + +
    + + {# 5. SUPPRIMER #} +
    + +
    +
    +
    +
  2. + {% endfor %} +
+ +
+ +
+
+ + {# --- BARRE D'ACTIONS FINALE --- #} +