From 7dc29780943f74ded8073112e9bd3d408fd95456 Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Thu, 22 Jan 2026 10:36:26 +0100 Subject: [PATCH] =?UTF-8?q?```=20=E2=9C=A8=20feat(Devis):=20Ajoute=20optio?= =?UTF-8?q?ns,=20dates=20d=C3=A9but/fin=20et=20am=C3=A9liore=20affichage?= =?UTF-8?q?=20PDF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute les champs date de début et fin au devis. Permet l'ajout d'options au devis. Améliore l'affichage du PDF. ``` --- assets/libs/initTomSelect.js | 57 +++++++++++++ migrations/Version20260122085820.php | 42 ++++++++++ migrations/Version20260122092253.php | 36 +++++++++ migrations/Version20260122092321.php | 36 +++++++++ migrations/Version20260122092540.php | 36 +++++++++ migrations/Version20260122092618.php | 38 +++++++++ src/Controller/Dashboard/DevisController.php | 23 ++++-- .../Dashboard/ProductController.php | 27 ++++++- src/Controller/SignatureController.php | 4 +- src/Entity/Devis.php | 80 +++++++++++++++++-- src/Entity/DevisLine.php | 29 ------- src/Entity/DevisOptions.php | 65 +++++++++++++++ src/Entity/Options.php | 74 ++++++++--------- src/Form/NewDevisType.php | 11 +++ src/Repository/DevisOptionsRepository.php | 43 ++++++++++ src/Service/Pdf/DevisPdfService.php | 70 +++++++++++++--- src/Service/Signature/Client.php | 1 - src/Twig/StripeExtension.php | 13 ++- templates/dashboard/devis/add.twig | 70 ++++++++++++---- templates/mails/sign/signed_notification.twig | 2 +- 20 files changed, 646 insertions(+), 111 deletions(-) create mode 100644 migrations/Version20260122085820.php create mode 100644 migrations/Version20260122092253.php create mode 100644 migrations/Version20260122092321.php create mode 100644 migrations/Version20260122092540.php create mode 100644 migrations/Version20260122092618.php create mode 100644 src/Entity/DevisOptions.php create mode 100644 src/Repository/DevisOptionsRepository.php diff --git a/assets/libs/initTomSelect.js b/assets/libs/initTomSelect.js index bee27cb..1e792c5 100644 --- a/assets/libs/initTomSelect.js +++ b/assets/libs/initTomSelect.js @@ -80,6 +80,63 @@ export function initTomSelect(parent = document) { }); } } + else if (el.getAttribute('data-load') === "options") { + const setupSelect = (data) => { + new TomSelect(el, { + valueField: 'id', + labelField: 'name', + searchField: 'name', + options: data, + maxOptions: null, + // LORSQU'ON SÉLECTIONNE UN PRODUIT + // Dans admin.js, section onChange de TomSelect : + onChange: (id) => { + if (!id) return; + + // On s'assure de trouver le produit (id peut être string ou int) + const product = data.find(p => String(p.id) === String(id)); + const row = el.closest('.form-repeater__row') || el.closest('fieldset'); + if (!row) return; + const priceInput = row.querySelector('input[name*="[price_ht]"]'); + + if (priceInput) { + priceInput.value = product.price; + // Indispensable pour que d'autres scripts (calcul totaux) voient le changement + priceInput.dispatchEvent(new Event('input', { bubbles: true })); + priceInput.dispatchEvent(new Event('change', { bubbles: true })); + } + + }, + render: { + option: (data, escape) => ` +
+ +
+
${escape(data.name)}
+
${escape(data.price)}€
+
+
`, + item: (data, escape) => ` +
+ + ${escape(data.name)} +
` + } + }); + }; + + // Utilisation du cache ou fetch + if (productCache) { + setupSelect(productCache); + } else { + fetch("/crm/options/json") + .then(r => r.json()) + .then(data => { + productCache = data; + setupSelect(data); + }); + } + } // --- AUTRES SELECTS --- else { new TomSelect(el, { diff --git a/migrations/Version20260122085820.php b/migrations/Version20260122085820.php new file mode 100644 index 0000000..f36ba0d --- /dev/null +++ b/migrations/Version20260122085820.php @@ -0,0 +1,42 @@ +addSql('ALTER TABLE devis ADD start_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL'); + $this->addSql('ALTER TABLE devis ADD end_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL'); + $this->addSql('COMMENT ON COLUMN devis.start_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN devis.end_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE devis_line DROP start_at'); + $this->addSql('ALTER TABLE devis_line DROP end_at'); + } + + 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 devis DROP start_at'); + $this->addSql('ALTER TABLE devis DROP end_at'); + $this->addSql('ALTER TABLE devis_line ADD start_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL'); + $this->addSql('ALTER TABLE devis_line ADD end_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL'); + $this->addSql('COMMENT ON COLUMN devis_line.start_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN devis_line.end_at IS \'(DC2Type:datetime_immutable)\''); + } +} diff --git a/migrations/Version20260122092253.php b/migrations/Version20260122092253.php new file mode 100644 index 0000000..2340326 --- /dev/null +++ b/migrations/Version20260122092253.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE devis DROP CONSTRAINT fk_8b27c52b3adb05f1'); + $this->addSql('DROP INDEX idx_8b27c52b3adb05f1'); + $this->addSql('ALTER TABLE devis DROP options_id'); + } + + 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 devis ADD options_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE devis ADD CONSTRAINT fk_8b27c52b3adb05f1 FOREIGN KEY (options_id) REFERENCES options (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX idx_8b27c52b3adb05f1 ON devis (options_id)'); + } +} diff --git a/migrations/Version20260122092321.php b/migrations/Version20260122092321.php new file mode 100644 index 0000000..6af0826 --- /dev/null +++ b/migrations/Version20260122092321.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE options ADD devis_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE options ADD CONSTRAINT FK_D035FA8741DEFADA FOREIGN KEY (devis_id) REFERENCES devis (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_D035FA8741DEFADA ON options (devis_id)'); + } + + 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 options DROP CONSTRAINT FK_D035FA8741DEFADA'); + $this->addSql('DROP INDEX IDX_D035FA8741DEFADA'); + $this->addSql('ALTER TABLE options DROP devis_id'); + } +} diff --git a/migrations/Version20260122092540.php b/migrations/Version20260122092540.php new file mode 100644 index 0000000..609ed93 --- /dev/null +++ b/migrations/Version20260122092540.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE options DROP CONSTRAINT fk_d035fa8741defada'); + $this->addSql('DROP INDEX idx_d035fa8741defada'); + $this->addSql('ALTER TABLE options DROP devis_id'); + } + + 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 options ADD devis_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE options ADD CONSTRAINT fk_d035fa8741defada FOREIGN KEY (devis_id) REFERENCES devis (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX idx_d035fa8741defada ON options (devis_id)'); + } +} diff --git a/migrations/Version20260122092618.php b/migrations/Version20260122092618.php new file mode 100644 index 0000000..eb52e02 --- /dev/null +++ b/migrations/Version20260122092618.php @@ -0,0 +1,38 @@ +addSql('CREATE TABLE devis_options (id SERIAL NOT NULL, devis_id INT DEFAULT NULL, option_id INT DEFAULT NULL, price_ht DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_42DB61DB41DEFADA ON devis_options (devis_id)'); + $this->addSql('CREATE INDEX IDX_42DB61DBA7C41D6F ON devis_options (option_id)'); + $this->addSql('ALTER TABLE devis_options ADD CONSTRAINT FK_42DB61DB41DEFADA FOREIGN KEY (devis_id) REFERENCES devis (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE devis_options ADD CONSTRAINT FK_42DB61DBA7C41D6F FOREIGN KEY (option_id) REFERENCES options (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 devis_options DROP CONSTRAINT FK_42DB61DB41DEFADA'); + $this->addSql('ALTER TABLE devis_options DROP CONSTRAINT FK_42DB61DBA7C41D6F'); + $this->addSql('DROP TABLE devis_options'); + } +} diff --git a/src/Controller/Dashboard/DevisController.php b/src/Controller/Dashboard/DevisController.php index 4a05521..697178a 100644 --- a/src/Controller/Dashboard/DevisController.php +++ b/src/Controller/Dashboard/DevisController.php @@ -5,6 +5,7 @@ namespace App\Controller\Dashboard; use App\Entity\CustomerAddress; use App\Entity\Devis; use App\Entity\DevisLine; +use App\Entity\DevisOptions; use App\Event\Signature\DevisSend; use App\Form\NewDevisType; use App\Logger\AppLogger; @@ -12,9 +13,11 @@ use App\Repository\AccountRepository; use App\Repository\CustomerAddressRepository; use App\Repository\CustomerRepository; use App\Repository\DevisRepository; +use App\Repository\OptionsRepository; use App\Repository\ProductRepository; use App\Service\Pdf\DevisPdfService; use App\Service\Signature\Client; +use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; use Knp\Bundle\PaginatorBundle\KnpPaginatorBundle; use Knp\Component\Pager\PaginatorInterface; @@ -40,6 +43,7 @@ class DevisController extends AbstractController AppLogger $appLogger, PaginatorInterface $paginator, Request $request, + KernelInterface $kernel, ): Response { @@ -77,7 +81,7 @@ class DevisController extends AbstractController ]); } #[Route(path: '/crm/devis/add', name: 'app_crm_devis_add', options: ['sitemap' => false], methods: ['GET','POST'])] - public function devisAdd(Client $client,EventDispatcherInterface $eventDispatcher,KernelInterface $kernel,CustomerAddressRepository $customerAddress,ProductRepository $productRepository,EntityManagerInterface $entityManager,CustomerRepository $customerRepository,DevisRepository $devisRepository, AppLogger $appLogger,Request $request): Response + public function devisAdd(Client $client,OptionsRepository $optionsRepository,EventDispatcherInterface $eventDispatcher,KernelInterface $kernel,CustomerAddressRepository $customerAddress,ProductRepository $productRepository,EntityManagerInterface $entityManager,CustomerRepository $customerRepository,DevisRepository $devisRepository, AppLogger $appLogger,Request $request): Response { $devisNumber ="DEVIS-".sprintf('%05d',$devisRepository->count()+1); $appLogger->record('VIEW', 'Consultation de la création d\'un devis'); @@ -90,9 +94,12 @@ class DevisController extends AbstractController $form = $this->createForm(NewDevisType::class,$devis); if($request->isMethod('POST')){ + + $devis->setStartAt( new DateTimeImmutable($_POST['new_devis']['startAt'])); + $devis->setEndAt( new DateTimeImmutable($_POST['new_devis']['endAt'])); $devis->setBillAddress($customerAddress->find($_POST['devis']['bill_address'])); $devis->setAddressShip($customerAddress->find($_POST['devis']['ship_address'])); - $devis->setCustomer($customerRepository->find($_POST['new_devis']['customer'])); + $devis->setCustomer($customerRepository->find($_POST['new_devis']['customer'])); foreach ($_POST['lines'] as $cd=>$line) { $rLine = new DevisLine(); $rLine->setDevi($devis); @@ -101,16 +108,22 @@ class DevisController extends AbstractController $rLine->setDay($line['days']); $rLine->setPriceHt(floatval($line['price_ht'])); $rLine->setPriceHtSup(floatval($line['price_sup_ht'])); - $rLine->setStartAt(\DateTimeImmutable::createFromFormat('Y-m-d',$line['date_start'])); - $rLine->setEndAt(\DateTimeImmutable::createFromFormat('Y-m-d',$line['date_end'])); $entityManager->persist($rLine); } + foreach ($_POST['options'] as $line) { + $rLineOptions = new DevisOptions(); + $rLineOptions->setDevis($devis); + $rLineOptions->setOption($optionsRepository->find($line['product_id'])); + $rLineOptions->setPriceHt(floatval($line['price_ht'])); + $entityManager->persist($rLineOptions); + } $entityManager->persist($devis); - + $entityManager->flush(); $docusealService = new DevisPdfService($kernel, $devis, true); $contentDocuseal = $docusealService->generate(); + $tmpPathDocuseal = sys_get_temp_dir() . '/docuseal_' . uniqid() . '.pdf'; file_put_contents($tmpPathDocuseal, $contentDocuseal); diff --git a/src/Controller/Dashboard/ProductController.php b/src/Controller/Dashboard/ProductController.php index f82bb2d..123730b 100644 --- a/src/Controller/Dashboard/ProductController.php +++ b/src/Controller/Dashboard/ProductController.php @@ -37,14 +37,18 @@ use Vich\UploaderBundle\Templating\Helper\UploaderHelper; class ProductController extends AbstractController { #[Route(path: '/crm/products/json', name: 'app_crm_product_json', options: ['sitemap' => false], methods: ['GET'])] - public function productsJson(ProductRepository $productRepository,UploaderHelper $uploaderHelper): Response + public function productsJson(ProductRepository $productRepository, UploaderHelper $uploaderHelper): Response { $products = []; foreach ($productRepository->findAll() as $product) { + // On récupère le chemin de l'image + $imagePath = $uploaderHelper->asset($product, 'imageFile'); + $products[] = [ 'id' => $product->getId(), 'name' => $product->getName(), - 'image' => $uploaderHelper->asset($product, 'imageFile'), + // On s'assure que si Vich ne trouve rien, on renvoie null proprement + 'image' => $imagePath ?: "/provider/images/favicon.png", 'price1day' => $product->getPriceDay(), 'priceSup' => $product->getPriceSup(), 'caution' => $product->getCaution(), @@ -53,6 +57,25 @@ class ProductController extends AbstractController return $this->json($products); } + + #[Route(path: '/crm/options/json', name: 'app_crm_options_json', options: ['sitemap' => false], methods: ['GET'])] + public function optionsJson(OptionsRepository $optionsRepository, UploaderHelper $uploaderHelper): Response + { + $options = []; + foreach ($optionsRepository->findAll() as $option) { + // Vérification identique pour les options + $imagePath = $uploaderHelper->asset($option, 'imageFile'); + + $options[] = [ + 'id' => $option->getId(), + 'name' => $option->getName(), + 'image' => $imagePath ?: "/provider/images/favicon.png", + 'price' => $option->getPriceHt(), + ]; + } + + return $this->json($options); + } #[Route(path: '/crm/products', name: 'app_crm_product', options: ['sitemap' => false], methods: ['GET'])] public function products(OptionsRepository $optionsRepository, ProductRepository $productRepository, AppLogger $appLogger, PaginatorInterface $paginator, Request $request): Response { diff --git a/src/Controller/SignatureController.php b/src/Controller/SignatureController.php index 8bbe7c7..91b2c46 100644 --- a/src/Controller/SignatureController.php +++ b/src/Controller/SignatureController.php @@ -93,8 +93,8 @@ class SignatureController extends AbstractController $productReserve = new ProductReserve(); $productReserve->setProduct($product); $productReserve->setCustomer($devis->getCustomer()); - $productReserve->setStartAt($line->getStartAt()); - $productReserve->setEndAt($line->getEndAt()); + $productReserve->setStartAt($devis->getStartAt()); + $productReserve->setEndAt($devis->getEndAt()); $productReserve->setDevis($devis); $entityManager->persist($productReserve); } diff --git a/src/Entity/Devis.php b/src/Entity/Devis.php index c1dfe09..57e06c7 100644 --- a/src/Entity/Devis.php +++ b/src/Entity/Devis.php @@ -83,12 +83,30 @@ class Devis #[ORM\OneToOne(mappedBy: 'devis', cascade: ['persist', 'remove'])] private ?Contrats $contrats = null; - #[ORM\ManyToOne(inversedBy: 'devis')] - private ?Options $options = null; + + #[ORM\Column] + private ?\DateTimeImmutable $startAt = null; + + #[ORM\Column] + private ?\DateTimeImmutable $endAt = null; + + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Options::class, mappedBy: 'devis')] + private Collection $options; + + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: DevisOptions::class, mappedBy: 'devis')] + private Collection $devisOptions; public function __construct() { $this->devisLines = new ArrayCollection(); + $this->options = new ArrayCollection(); + $this->devisOptions = new ArrayCollection(); } public function getId(): ?int @@ -478,14 +496,64 @@ class Devis return $this; } - public function getOptions(): ?Options + /** + * @param \DateTimeImmutable|null $startAt + */ + public function setStartAt(?\DateTimeImmutable $startAt): void { - return $this->options; + $this->startAt = $startAt; } - public function setOptions(?Options $options): static + /** + * @return \DateTimeImmutable|null + */ + public function getStartAt(): ?\DateTimeImmutable { - $this->options = $options; + return $this->startAt; + } + + /** + * @param \DateTimeImmutable|null $endAt + */ + public function setEndAt(?\DateTimeImmutable $endAt): void + { + $this->endAt = $endAt; + } + + /** + * @return \DateTimeImmutable|null + */ + public function getEndAt(): ?\DateTimeImmutable + { + return $this->endAt; + } + + /** + * @return Collection + */ + public function getDevisOptions(): Collection + { + return $this->devisOptions; + } + + public function addDevisOption(DevisOptions $devisOption): static + { + if (!$this->devisOptions->contains($devisOption)) { + $this->devisOptions->add($devisOption); + $devisOption->setDevis($this); + } + + return $this; + } + + public function removeDevisOption(DevisOptions $devisOption): static + { + if ($this->devisOptions->removeElement($devisOption)) { + // set the owning side to null (unless already changed) + if ($devisOption->getDevis() === $this) { + $devisOption->setDevis(null); + } + } return $this; } diff --git a/src/Entity/DevisLine.php b/src/Entity/DevisLine.php index 3998f35..2c8a317 100644 --- a/src/Entity/DevisLine.php +++ b/src/Entity/DevisLine.php @@ -29,11 +29,6 @@ class DevisLine #[ORM\Column] private ?int $day = null; - #[ORM\Column] - private ?\DateTimeImmutable $startAt = null; - - #[ORM\Column] - private ?\DateTimeImmutable $endAt = null; #[ORM\ManyToOne(inversedBy: 'devisLines')] private ?Product $product = null; @@ -104,30 +99,6 @@ class DevisLine return $this; } - public function getStartAt(): ?\DateTimeImmutable - { - return $this->startAt; - } - - public function setStartAt(\DateTimeImmutable $startAt): static - { - $this->startAt = $startAt; - - return $this; - } - - public function getEndAt(): ?\DateTimeImmutable - { - return $this->endAt; - } - - public function setEndAt(\DateTimeImmutable $endAt): static - { - $this->endAt = $endAt; - - return $this; - } - public function getProduct(): ?Product { return $this->product; diff --git a/src/Entity/DevisOptions.php b/src/Entity/DevisOptions.php new file mode 100644 index 0000000..73255e2 --- /dev/null +++ b/src/Entity/DevisOptions.php @@ -0,0 +1,65 @@ +id; + } + + public function getDevis(): ?Devis + { + return $this->devis; + } + + public function setDevis(?Devis $devis): static + { + $this->devis = $devis; + + return $this; + } + + public function getOption(): ?Options + { + return $this->option; + } + + public function setOption(?Options $option): static + { + $this->option = $option; + + return $this; + } + + public function getPriceHt(): ?float + { + return $this->priceHt; + } + + public function setPriceHt(float $priceHt): static + { + $this->priceHt = $priceHt; + + return $this; + } +} diff --git a/src/Entity/Options.php b/src/Entity/Options.php index 5fcb664..aa60a0e 100644 --- a/src/Entity/Options.php +++ b/src/Entity/Options.php @@ -26,11 +26,6 @@ class Options #[ORM\Column] private ?float $priceHt = null; - /** - * @var Collection - */ - #[ORM\OneToMany(targetEntity: Devis::class, mappedBy: 'options')] - private Collection $devis; #[ORM\Column(length: 255)] private ?string $stripeId = null; @@ -46,11 +41,18 @@ class Options #[ORM\Column(nullable: true)] private ?\DateTimeImmutable $updatedAt = null; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: DevisOptions::class, mappedBy: 'option')] + private Collection $devisOptions; + public function __construct() { - $this->devis = new ArrayCollection(); + $this->devisOptions = new ArrayCollection(); } + public function getId(): ?int { return $this->id; @@ -80,36 +82,6 @@ class Options return $this; } - /** - * @return Collection - */ - public function getDevis(): Collection - { - return $this->devis; - } - - public function addDevi(Devis $devi): static - { - if (!$this->devis->contains($devi)) { - $this->devis->add($devi); - $devi->setOptions($this); - } - - return $this; - } - - public function removeDevi(Devis $devi): static - { - if ($this->devis->removeElement($devi)) { - // set the owning side to null (unless already changed) - if ($devi->getOptions() === $this) { - $devi->setOptions(null); - } - } - - return $this; - } - public function getStripeId(): ?string { return $this->stripeId; @@ -180,4 +152,34 @@ class Options return$s->slugify($this->id."-".$this->name); } + + /** + * @return Collection + */ + public function getDevisOptions(): Collection + { + return $this->devisOptions; + } + + public function addDevisOption(DevisOptions $devisOption): static + { + if (!$this->devisOptions->contains($devisOption)) { + $this->devisOptions->add($devisOption); + $devisOption->setOption($this); + } + + return $this; + } + + public function removeDevisOption(DevisOptions $devisOption): static + { + if ($this->devisOptions->removeElement($devisOption)) { + // set the owning side to null (unless already changed) + if ($devisOption->getOption() === $this) { + $devisOption->setOption(null); + } + } + + return $this; + } } diff --git a/src/Form/NewDevisType.php b/src/Form/NewDevisType.php index 60c9fd2..320883e 100644 --- a/src/Form/NewDevisType.php +++ b/src/Form/NewDevisType.php @@ -6,6 +6,7 @@ use App\Entity\Customer; use App\Entity\Devis; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; @@ -31,6 +32,16 @@ class NewDevisType extends AbstractType 'readonly' => true, ] ]) + ->add('startAt',DateTimeType::class,[ + 'label' =>'Date de début de \'événément', + 'required' => true, + 'widget' => 'single_text', + ]) + ->add('endAt',DateTimeType::class,[ + 'label' =>'Date de fin de \'événément', + 'required' => true, + 'widget' => 'single_text', + ]) ->add('customer', EntityType::class, [ 'label' => 'Client', 'required' => true, diff --git a/src/Repository/DevisOptionsRepository.php b/src/Repository/DevisOptionsRepository.php new file mode 100644 index 0000000..f8d21b4 --- /dev/null +++ b/src/Repository/DevisOptionsRepository.php @@ -0,0 +1,43 @@ + + */ +class DevisOptionsRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, DevisOptions::class); + } + + // /** + // * @return DevisOptions[] Returns an array of DevisOptions objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('d') + // ->andWhere('d.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('d.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?DevisOptions + // { + // return $this->createQueryBuilder('d') + // ->andWhere('d.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Service/Pdf/DevisPdfService.php b/src/Service/Pdf/DevisPdfService.php index 329533c..e219b4b 100644 --- a/src/Service/Pdf/DevisPdfService.php +++ b/src/Service/Pdf/DevisPdfService.php @@ -102,10 +102,28 @@ class DevisPdfService extends Fpdf $this->renderAddressBlock('ADRESSE DE FACTURATION', $this->devis->getBillAddress(), 'L', 10, $yAddress); $this->renderAddressBlock('ADRESSE DE PRESTATION', $this->devis->getAddressShip(), 'R', 110, $yAddress); - $this->SetY($yAddress + 35); - $this->Ln(10); + // --- AJOUT : BLOC DATES DE L'ÉVÉNEMENT --- + $this->SetY($yAddress + 25); + $this->SetDrawColor(230, 230, 230); + $this->SetFillColor(250, 250, 250); + $this->Rect(10, $this->GetY(), 190, 12, 2, 'DF'); // Petit encadré gris clair + $this->SetY($this->GetY() + 3.5); + $this->SetFont('Arial', 'B', 9); + $this->SetTextColor(80, 80, 80); + $this->Cell(45, 5, $this->clean('DATES DE PRESTATION :'), 0, 0, 'L'); + $this->SetFont('Arial', '', 10); + $this->SetTextColor(0, 0, 0); + + $dateStart = $this->devis->getStartAt() ? $this->devis->getStartAt()->format('d/m/Y à H:i') : 'N/C'; + $dateEnd = $this->devis->getEndAt() ? $this->devis->getEndAt()->format('d/m/Y à H:i') : 'N/C'; + + $this->Cell(0, 5, $this->clean("Du $dateStart au $dateEnd"), 0, 1, 'L'); + + $this->SetY($this->GetY() + 8); + + // --- TABLEAU DES PRODUITS --- $this->SetFont('Arial', 'B', 8); $this->SetFillColor(245, 247, 250); $this->Cell(70, 10, $this->clean('Désignation'), 1, 0, 'L', true); @@ -113,7 +131,7 @@ class DevisPdfService extends Fpdf $this->Cell(25, 10, $this->clean('Tarif J1'), 1, 0, 'R', true); $this->Cell(25, 10, $this->clean('Tarif Sup'), 1, 0, 'R', true); $this->Cell(15, 10, $this->clean('TVA'), 1, 0, 'C', true); - $this->Cell(40, 10, $this->clean('Total HT'), 1, 1, 'R', true); // Total HT car TVA 0% + $this->Cell(40, 10, $this->clean('Total HT'), 1, 1, 'R', true); $this->SetFont('Arial', '', 9); $this->SetTextColor(0, 0, 0); @@ -132,22 +150,13 @@ class DevisPdfService extends Fpdf $productName = $line->getProduct()->getName(); $ref = $line->getProduct()->getRef(); - $dateStart = $line->getStartAt() ? $line->getStartAt()->format('d/m/Y') : ''; - $dateEnd = $line->getEndAt() ? $line->getEndAt()->format('d/m/Y') : ''; $currentY = $this->GetY(); // --- COLONNE DÉSIGNATION (NOM + REF / DATES) --- - $this->SetXY(10, $currentY); + $this->SetXY(10, $currentY+2.5); $this->SetFont('Arial', 'B', 8); $this->Cell(70, 5, $this->clean($productName . ' (Ref: ' . $ref . ')'), 0, 0, 'L'); - - $this->SetXY(10, $currentY + 5); - $this->SetFont('Arial', 'I', 7); - $this->SetTextColor(100, 100, 100); - $this->Cell(70, 4, $this->clean("Période : du $dateStart au $dateEnd"), 0, 0, 'L'); - $this->SetTextColor(0, 0, 0); - // --- COLONNES NUMÉRIQUES --- $this->SetXY(80, $currentY); $this->SetFont('Arial', '', 8); @@ -166,6 +175,41 @@ class DevisPdfService extends Fpdf } } + //options +// --- 2. TABLEAU DES OPTIONS (SÉPARÉ) --- + if (count($this->devis->getDevisOptions()) > 0) { + $this->Ln(5); // Espace entre les deux tableaux + + // En-tête du tableau des options + $this->SetFont('Arial', 'B', 8); + $this->SetFillColor(230, 235, 245); // Bleu très léger pour différencier + $this->Cell(150, 8, $this->clean('Options & Services additionnels'), 1, 0, 'L', true); + $this->Cell(40, 8, $this->clean('Total HT'), 1, 1, 'R', true); + + $this->SetFont('Arial', '', 9); + $this->SetTextColor(0, 0, 0); + + foreach ($this->devis->getDevisOptions() as $devisOption) { + $option = $devisOption->getOption(); + $priceHT = $option->getPriceHt(); + $totalHT += $priceHT; // On l'ajoute au total général + + $currentY = $this->GetY(); + + // Colonne Désignation + $this->SetXY(10, $currentY); + $this->Cell(150, 8, $this->clean($option->getName()), 'LRB', 0, 'L'); + + // Colonne Prix + $this->Cell(40, 8, number_format($priceHT, 2, ',', ' ') . $this->euro(), 'RB', 1, 'R'); + + if ($this->GetY() > 260) { + $this->AddPage(); + } + } + } + + // --- BLOC TOTAUX --- $this->Ln(5); $this->SetFont('Arial', 'B', 10); diff --git a/src/Service/Signature/Client.php b/src/Service/Signature/Client.php index 68070a0..ef0be2e 100644 --- a/src/Service/Signature/Client.php +++ b/src/Service/Signature/Client.php @@ -91,7 +91,6 @@ class Client // Stockage de l'ID submitter de Docuseal dans ton entité $devis->setSignatureId($submission['submitters'][1]['id']); - dd($this->getLinkSign($devis->getSignatureId())); $this->entityManager->flush(); } diff --git a/src/Twig/StripeExtension.php b/src/Twig/StripeExtension.php index 83a9bbb..6378474 100644 --- a/src/Twig/StripeExtension.php +++ b/src/Twig/StripeExtension.php @@ -28,18 +28,27 @@ class StripeExtension extends AbstractExtension { $totalHT = 0; + // 1. Calcul des lignes de produits (Location) foreach ($devis->getDevisLines() as $line) { $price1Day = $line->getPriceHt() ?? 0; $priceSupHT = $line->getPriceHtSup() ?? 0; $nbDays = $line->getDay() ?? 1; - // Formule : Le premier jour est au prix plein, les suivants au prix Sup - // J1 + ( (Total Jours - 1) * Prix Sup ) + // Calcul : J1 + (Jours supplémentaires * Prix Sup) $lineTotalHT = $price1Day + (max(0, $nbDays - 1) * $priceSupHT); $totalHT += $lineTotalHT; } + // 2. Calcul des options additionnelles + foreach ($devis->getDevisOptions() as $devisOption) { + // On récupère l'entité Option liée à la ligne de liaison + $option = $devisOption->getOption(); + if ($option) { + $totalHT += $option->getPriceHt() ?? 0; + } + } + return (float) $totalHT; } diff --git a/templates/dashboard/devis/add.twig b/templates/dashboard/devis/add.twig index 632087b..b802710 100644 --- a/templates/dashboard/devis/add.twig +++ b/templates/dashboard/devis/add.twig @@ -57,6 +57,7 @@ {{ form_label(form.customer, null, {'label_attr': {'class': label_class}}) }} {{ form_widget(form.customer, {'attr': {'class': input_class}}) }} + {# SECTION ADRESSES #} @@ -70,6 +71,17 @@ + {# SECTION ADRESSES #} +
+
+ {{ form_label(form.startAt, null, {'label_attr': {'class': label_class}}) }} + {{ form_widget(form.startAt, {'attr': {'class': input_class}}) }} +
+
+ {{ form_label(form.endAt, null, {'label_attr': {'class': label_class}}) }} + {{ form_widget(form.endAt, {'attr': {'class': input_class}}) }} +
+

@@ -86,7 +98,7 @@
{# 1. PRODUIT #} -
+
@@ -106,22 +118,11 @@
{# 4. PRIX HT SUP #} -
+
- {# 5. DATES #} -
- - -
- -
- - -
- {# 6. SUPPRIMER #}
+
+
+

Détail des options

+
+
    +
  1. +
    + +
    + + {# 1. PRODUIT #} +
    + + +
    + {# 3. PRIX HT J1 #} +
    + + +
    + {# 6. SUPPRIMER #} +
    + +
    +
    +
    +
  2. +
+
+ +
+
{# VALIDATION #}