```
✨ feat(Devis): Ajoute options, dates début/fin et améliore affichage PDF
Ajoute les champs date de début et fin au devis. Permet l'ajout d'options au devis. Améliore l'affichage du PDF.
```
This commit is contained in:
@@ -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) => `
|
||||
<div class="flex items-center gap-3 py-2 px-3 border-b border-slate-800/50">
|
||||
<img src="${escape(data.image)}" class="w-8 h-8 object-cover rounded shadow-sm">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-[13px] font-bold text-white">${escape(data.name)}</div>
|
||||
<div class="text-[10px] text-slate-400">${escape(data.price)}€</div>
|
||||
</div>
|
||||
</div>`,
|
||||
item: (data, escape) => `
|
||||
<div class="text-blue-400 font-bold flex items-center gap-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-blue-500"></span>
|
||||
${escape(data.name)}
|
||||
</div>`
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 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, {
|
||||
|
||||
42
migrations/Version20260122085820.php
Normal file
42
migrations/Version20260122085820.php
Normal 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 Version20260122085820 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('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)\'');
|
||||
}
|
||||
}
|
||||
36
migrations/Version20260122092253.php
Normal file
36
migrations/Version20260122092253.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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 Version20260122092253 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('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)');
|
||||
}
|
||||
}
|
||||
36
migrations/Version20260122092321.php
Normal file
36
migrations/Version20260122092321.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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 Version20260122092321 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('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');
|
||||
}
|
||||
}
|
||||
36
migrations/Version20260122092540.php
Normal file
36
migrations/Version20260122092540.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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 Version20260122092540 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('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)');
|
||||
}
|
||||
}
|
||||
38
migrations/Version20260122092618.php
Normal file
38
migrations/Version20260122092618.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?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 Version20260122092618 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 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');
|
||||
}
|
||||
}
|
||||
@@ -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,6 +94,9 @@ 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']));
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<int, Options>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Options::class, mappedBy: 'devis')]
|
||||
private Collection $options;
|
||||
|
||||
/**
|
||||
* @var Collection<int, DevisOptions>
|
||||
*/
|
||||
#[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<int, DevisOptions>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
65
src/Entity/DevisOptions.php
Normal file
65
src/Entity/DevisOptions.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\DevisOptionsRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: DevisOptionsRepository::class)]
|
||||
class DevisOptions
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'devisOptions')]
|
||||
private ?Devis $devis = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'devisOptions')]
|
||||
private ?Options $option = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?float $priceHt = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->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;
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,6 @@ class Options
|
||||
#[ORM\Column]
|
||||
private ?float $priceHt = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Devis>
|
||||
*/
|
||||
#[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<int, DevisOptions>
|
||||
*/
|
||||
#[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<int, Devis>
|
||||
*/
|
||||
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<int, DevisOptions>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
43
src/Repository/DevisOptionsRepository.php
Normal file
43
src/Repository/DevisOptionsRepository.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\DevisOptions;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<DevisOptions>
|
||||
*/
|
||||
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()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
{{ form_label(form.customer, null, {'label_attr': {'class': label_class}}) }}
|
||||
{{ form_widget(form.customer, {'attr': {'class': input_class}}) }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{# SECTION ADRESSES #}
|
||||
@@ -70,6 +71,17 @@
|
||||
<select id="shipAddress" name="devis[ship_address]" class="{{ input_class }}"></select>
|
||||
</div>
|
||||
</div>
|
||||
{# SECTION ADRESSES #}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div class="space-y-3">
|
||||
{{ form_label(form.startAt, null, {'label_attr': {'class': label_class}}) }}
|
||||
{{ form_widget(form.startAt, {'attr': {'class': input_class}}) }}
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
{{ form_label(form.endAt, null, {'label_attr': {'class': label_class}}) }}
|
||||
{{ form_widget(form.endAt, {'attr': {'class': input_class}}) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-white/5">
|
||||
|
||||
@@ -86,7 +98,7 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-5 items-end">
|
||||
|
||||
{# 1. PRODUIT #}
|
||||
<div class="lg:col-span-3">
|
||||
<div class="lg:col-span-5">
|
||||
<label class="{{ label_class }}">Produit / Prestation</label>
|
||||
<select data-load="product" name="lines[0][product_id]" class="{{ input_class }}" required>
|
||||
<option value="">Sélectionner...</option>
|
||||
@@ -94,7 +106,7 @@
|
||||
</div>
|
||||
|
||||
{# 2. DURÉE #}
|
||||
<div class="lg:col-span-1">
|
||||
<div class="lg:col-span-2">
|
||||
<label class="{{ label_class }}">Jours</label>
|
||||
<input type="number" name="lines[0][days]" min="1" placeholder="1" class="{{ input_class }} [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" required />
|
||||
</div>
|
||||
@@ -106,22 +118,11 @@
|
||||
</div>
|
||||
|
||||
{# 4. PRIX HT SUP #}
|
||||
<div class="lg:col-span-1">
|
||||
<div class="lg:col-span-2">
|
||||
<label class="{{ label_class }}">HT Sup</label>
|
||||
<input type="number" step="0.01" name="lines[0][price_sup_ht]" placeholder="0.00" class="{{ input_class }} text-right font-mono" required />
|
||||
</div>
|
||||
|
||||
{# 5. DATES #}
|
||||
<div class="lg:col-span-2">
|
||||
<label class="{{ label_class }}">Date Début</label>
|
||||
<input type="date" name="lines[0][date_start]" class="{{ input_class }} [color-scheme:dark]" required />
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-2">
|
||||
<label class="{{ label_class }}">Date Fin</label>
|
||||
<input type="date" name="lines[0][date_end]" class="{{ input_class }} [color-scheme:dark]" required />
|
||||
</div>
|
||||
|
||||
{# 6. SUPPRIMER #}
|
||||
<div class="lg:col-span-1 flex justify-center pb-1">
|
||||
<button type="button" data-ref="removeButton" class="p-4 bg-red-500/10 hover:bg-red-500 text-red-500 hover:text-white rounded-2xl transition-all duration-300 shadow-lg hover:shadow-red-500/20">
|
||||
@@ -143,6 +144,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-repeater" data-component="repeater2" is="repeat-line">
|
||||
<div class="flex items-center justify-between mb-6 px-4">
|
||||
<h4 class="text-sm font-black text-white uppercase tracking-widest">Détail des options</h4>
|
||||
</div>
|
||||
<ol class="form-repeater__rows space-y-4" data-ref="rows">
|
||||
<li class="form-repeater__row group animate-in slide-in-from-right-5 duration-300">
|
||||
<fieldset class="backdrop-blur-md bg-white/5 border border-white/10 rounded-[2.5rem] p-8 hover:border-blue-500/30 transition-all shadow-xl">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-5 items-end">
|
||||
|
||||
{# 1. PRODUIT #}
|
||||
<div class="lg:col-span-9">
|
||||
<label class="{{ label_class }}">Options</label>
|
||||
<select data-load="options" name="options[0][product_id]" class="{{ input_class }}" required>
|
||||
<option value="">Sélectionner...</option>
|
||||
</select>
|
||||
</div>
|
||||
{# 3. PRIX HT J1 #}
|
||||
<div class="lg:col-span-2">
|
||||
<label class="{{ label_class }}">Prix Ht (€)</label>
|
||||
<input type="number" step="0.01" name="options[0][price_ht]" placeholder="0.00" class="{{ input_class }} text-right font-mono" required />
|
||||
</div>
|
||||
{# 6. SUPPRIMER #}
|
||||
<div class="lg:col-span-1 flex justify-center pb-1">
|
||||
<button type="button" data-ref="removeButton" class="p-4 bg-red-500/10 hover:bg-red-500 text-red-500 hover:text-white rounded-2xl transition-all duration-300 shadow-lg hover:shadow-red-500/20">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="mt-6 px-4">
|
||||
<button type="button" data-ref="addButton" class="w-full py-4 border-2 border-dashed border-white/10 hover:border-purple-500/50 bg-white/5 hover:bg-purple-500/10 rounded-3xl text-purple-400 text-[10px] font-black uppercase tracking-[0.4em] transition-all flex items-center justify-center space-x-3 group">
|
||||
<span class="text-xl group-hover:rotate-90 transition-transform duration-300">+</span>
|
||||
<span>Ajouter une option</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{# VALIDATION #}
|
||||
<div class="pt-8 px-4">
|
||||
<button type="submit" class="w-full py-6 bg-blue-600 hover:bg-blue-500 text-white text-[11px] font-black uppercase tracking-[0.5em] rounded-[2rem] shadow-2xl shadow-blue-600/30 transition-all hover:scale-[1.01] active:scale-95 flex items-center justify-center group">
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 5px 0; color: #64748b;">Montant HT :</td>
|
||||
<td style="padding: 5px 0; text-align: right; font-weight: bold; color: #2563eb;">{{ datas.devis.totalHt|number_format(2, ',', ' ') }} €</td>
|
||||
<td style="padding: 5px 0; text-align: right; font-weight: bold; color: #2563eb;">{{ (datas.devis|totalQuoto)|number_format(2, ',', ' ') }} €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 5px 0; color: #64748b;">Date de signature :</td>
|
||||
|
||||
Reference in New Issue
Block a user