feat(ansible): Ajoute le dossier PDF aux droits d'écriture et modifie l'URL API
✨ feat(.env): Met à jour les URLs de signature et Stripe pour Ngrok ✨ feat(SignatureController): Ajoute le contrôleur de signature ✨ feat(DevisController): Intègre DocuSeal et la gestion des adresses client 🐛 fix(DevisManager.js): Corrige la sélection et la synchronisation des adresses ✨ feat(vich_uploader.yaml): Configure le stockage des fichiers PDF ✨ feat(initTomSelect.js): Améliore la gestion des prix des produits ✨ feat(DevisPdfService): Intègre la signature DocuSeal et améliore le pied de page ✨ feat(Client.php): Crée une soumission Docuseal pour les devis
This commit is contained in:
4
.env
4
.env
@@ -83,8 +83,8 @@ STRIPE_PK=pk_test_51SUA22173W4aeFB1nO6oFfDZ12HOTffDKtCshhZ8rkUg6kUO2ZaQC0tK72rhE
|
||||
STRIPE_SK=sk_test_51SUA22173W4aeFB16EB2LxGI0hNvNJzFshDI98zRImWBIhSfzqOGAz5TlPxSpUWbj3x4COm6kmSsaal9FpQR1A7M0022DvjbbR
|
||||
STRIPE_WEBHOOKS_SECRET=
|
||||
|
||||
SIGN_URL=https://e3358705e82c.ngrok-free.app
|
||||
STRIPE_BASEURL=https://e3358705e82c.ngrok-free.app
|
||||
SIGN_URL=https://e2221f1f9e85.ngrok-free.app
|
||||
STRIPE_BASEURL=https://e2221f1f9e85.ngrok-free.app
|
||||
|
||||
MINIO_S3_URL=
|
||||
MINIO_S3_CLIENT_ID=
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -53,3 +53,4 @@ backup/*.sql
|
||||
/public/images/**/*.jpeg
|
||||
/public/images/**/*.webp
|
||||
/public/images/*/*.png
|
||||
/public/pdf/**/*.pdf
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
- "{{ path }}/var/log" # Specific for log, though var/log might be created by composer later
|
||||
- "{{ path }}/public/media" # For uploads
|
||||
- "{{ path }}/public/images" # For uploads
|
||||
- "{{ path }}/public/pdf" # For uploads
|
||||
- "{{ path }}/public/tmp-sign" # For upload
|
||||
- "{{ path }}/sauvegarde"
|
||||
|
||||
@@ -245,5 +246,6 @@
|
||||
- "{{ path }}/public/media"
|
||||
- "{{ path }}/sauvegarde"
|
||||
- "{{ path }}/public/images" # For uploads
|
||||
- "{{ path }}/public/pdf"
|
||||
- "{{ path }}/public/tmp-sign" # For uploads
|
||||
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
export class DevisManager extends HTMLDivElement {
|
||||
connectedCallback() {
|
||||
this.customerSelect = this.querySelector('select');
|
||||
// On s'assure de bien cibler les éléments dans le DOM
|
||||
this.billAddress = this.parentElement.parentElement.querySelector('#billAddress');
|
||||
this.shipAddress = this.parentElement.parentElement.querySelector('#shipAddress');
|
||||
|
||||
// Initialisation du message par défaut au chargement
|
||||
this.setAddressPlaceholder("Sélectionnez un client...");
|
||||
|
||||
if (this.customerSelect) {
|
||||
this.customerSelect.addEventListener('change', (e) => this.updateCustomerInfo(e.target.value));
|
||||
// Utiliser TomSelect si présent sur le client, sinon l'event natif
|
||||
if (this.customerSelect.tomselect) {
|
||||
this.customerSelect.tomselect.on('change', (value) => this.updateCustomerInfo(value));
|
||||
} else {
|
||||
this.customerSelect.addEventListener('change', (e) => this.updateCustomerInfo(e.target.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le texte d'aide dans les champs TomSelect
|
||||
*/
|
||||
setAddressPlaceholder(text) {
|
||||
[this.billAddress, this.shipAddress].forEach(el => {
|
||||
if (el && el.tomselect) {
|
||||
el.tomselect.settings.placeholder = text;
|
||||
el.tomselect.inputState(); // Force la mise à jour visuelle
|
||||
el.tomselect.inputState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async updateCustomerInfo(customerId) {
|
||||
// Purge et message d'attente
|
||||
// Vider les options actuelles proprement
|
||||
[this.billAddress, this.shipAddress].forEach(el => {
|
||||
if (el.tomselect) {
|
||||
if (el && el.tomselect) {
|
||||
el.tomselect.clear();
|
||||
el.tomselect.clearOptions();
|
||||
// On désactive pendant le chargement pour éviter une saisie erronée
|
||||
el.tomselect.disable();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -38,25 +42,38 @@ export class DevisManager extends HTMLDivElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pendant le chargement
|
||||
this.setAddressPlaceholder("Chargement des adresses...");
|
||||
|
||||
try {
|
||||
const resp = await fetch("/crm/customer/address/" + customerId);
|
||||
const data = await resp.json();
|
||||
|
||||
[this.billAddress, this.shipAddress].forEach(el => el?.tomselect?.enable());
|
||||
|
||||
if (data.addressList && data.addressList.length > 0) {
|
||||
this.setAddressPlaceholder("Choisir une adresse...");
|
||||
|
||||
data.addressList.forEach(itemList => {
|
||||
const option = { value: itemList.id, text: itemList.label };
|
||||
const option = { value: itemList.id, text: itemList.label }; // Adapté aux valueField/labelField par défaut
|
||||
const option2 = { value: itemList.id, text: itemList.label }; // Adapté aux valueField/labelField par défaut
|
||||
if (this.billAddress.tomselect) this.billAddress.tomselect.addOption(option);
|
||||
if (this.shipAddress.tomselect) this.shipAddress.tomselect.addOption(option);
|
||||
if (this.shipAddress.tomselect) this.shipAddress.tomselect.addOption(option2);
|
||||
});
|
||||
|
||||
// Optionnel : Sélectionner la première adresse par défaut
|
||||
this.billAddress.tomselect.setValue(data.addressList[0].id);
|
||||
this.shipAddress.tomselect.setValue(data.addressList[0].id);
|
||||
// --- SÉLECTION ET SYNCHRONISATION ---
|
||||
const firstId = data.addressList[0].id;
|
||||
|
||||
if (this.billAddress.tomselect) {
|
||||
|
||||
this.billAddress.tomselect.setValue(firstId);
|
||||
this.billAddress.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
if (this.shipAddress.tomselect) {
|
||||
this.shipAddress.tomselect.setValue(firstId);
|
||||
this.shipAddress.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
} else {
|
||||
this.setAddressPlaceholder("Aucune adresse trouvée");
|
||||
}
|
||||
@@ -64,6 +81,7 @@ export class DevisManager extends HTMLDivElement {
|
||||
} catch (error) {
|
||||
console.error("Erreur adresses:", error);
|
||||
this.setAddressPlaceholder("Erreur de chargement");
|
||||
[this.billAddress, this.shipAddress].forEach(el => el?.tomselect?.enable());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,20 +23,31 @@ export function initTomSelect(parent = document) {
|
||||
// Dans admin.js, section onChange de TomSelect :
|
||||
onChange: (id) => {
|
||||
if (!id) return;
|
||||
const product = data.find(p => p.id == id);
|
||||
|
||||
// On s'assure de trouver le produit (id peut être string ou int)
|
||||
const product = data.find(p => String(p.id) === String(id));
|
||||
|
||||
if (product) {
|
||||
// On remonte au parent le plus proche (le bloc de ligne du devis)
|
||||
const row = el.closest('.form-repeater__row') || el.closest('fieldset');
|
||||
if (!row) return;
|
||||
|
||||
// Remplir Prix J1
|
||||
// Ciblage précis des inputs
|
||||
const priceInput = row.querySelector('input[name*="[price_ht]"]');
|
||||
if (priceInput) priceInput.value = product.price1day;
|
||||
|
||||
// Remplir Prix Sup (Nouveau champ)
|
||||
const priceSupInput = row.querySelector('input[name*="[price_sup_ht]"]');
|
||||
if (priceSupInput) priceSupInput.value = product.priceSup;
|
||||
|
||||
// Déclencher les events
|
||||
[priceInput, priceSupInput].forEach(i => i?.dispatchEvent(new Event('change', { bubbles: true })));
|
||||
if (priceInput) {
|
||||
priceInput.value = product.price1day;
|
||||
// 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 }));
|
||||
}
|
||||
|
||||
if (priceSupInput) {
|
||||
priceSupInput.value = product.priceSup;
|
||||
priceSupInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
priceSupInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
},
|
||||
render: {
|
||||
|
||||
@@ -6,18 +6,18 @@ vich_uploader:
|
||||
upload_destination: '%kernel.project_dir%/public/images/image_product'
|
||||
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
|
||||
devis_file:
|
||||
uri_prefix: /images/devis_file
|
||||
upload_destination: '%kernel.project_dir%/public/images/devis_file'
|
||||
uri_prefix: /pdf/devis_file
|
||||
upload_destination: '%kernel.project_dir%/public/pdf/devis_file'
|
||||
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
|
||||
devis_docuseal:
|
||||
uri_prefix: /images/devis_docusign
|
||||
upload_destination: '%kernel.project_dir%/public/images/devis_docusign'
|
||||
uri_prefix: /pdf/devis_docusign
|
||||
upload_destination: '%kernel.project_dir%/public/pdf/devis_docusign'
|
||||
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
|
||||
devis_signed:
|
||||
uri_prefix: /images/devis_signed
|
||||
upload_destination: '%kernel.project_dir%/public/images/devis_signed'
|
||||
uri_prefix: /pdf/devis_signed
|
||||
upload_destination: '%kernel.project_dir%/public/pdf/devis_signed'
|
||||
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
|
||||
devis_audit:
|
||||
uri_prefix: /images/devis_audit
|
||||
upload_destination: '%kernel.project_dir%/public/images/devis_audit'
|
||||
uri_prefix: /pdf/devis_audit
|
||||
upload_destination: '%kernel.project_dir%/public/pdf/devis_audit'
|
||||
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
|
||||
|
||||
0
public/pdf/.gitignore
vendored
Normal file
0
public/pdf/.gitignore
vendored
Normal file
@@ -2,19 +2,24 @@
|
||||
|
||||
namespace App\Controller\Dashboard;
|
||||
|
||||
use App\Entity\CustomerAddress;
|
||||
use App\Entity\Devis;
|
||||
use App\Entity\DevisLine;
|
||||
use App\Form\NewDevisType;
|
||||
use App\Logger\AppLogger;
|
||||
use App\Repository\AccountRepository;
|
||||
use App\Repository\CustomerAddressRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\DevisRepository;
|
||||
use App\Repository\ProductRepository;
|
||||
use App\Service\Pdf\DevisPdfService;
|
||||
use App\Service\Signature\Client;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Knp\Bundle\PaginatorBundle\KnpPaginatorBundle;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
@@ -26,21 +31,15 @@ class DevisController extends AbstractController
|
||||
* Liste des administrateurs
|
||||
*/
|
||||
#[Route(path: '/crm/devis', name: 'app_crm_devis', options: ['sitemap' => false], methods: ['GET'])]
|
||||
public function devis(KernelInterface $kernel,DevisRepository $devisRepository,AppLogger $appLogger,PaginatorInterface $paginator,Request $request): Response
|
||||
public function devis(Client $client,EntityManagerInterface $entityManager,KernelInterface $kernel,DevisRepository $devisRepository,AppLogger $appLogger,PaginatorInterface $paginator,Request $request): Response
|
||||
{
|
||||
$d = $devisRepository->findAll()[0];
|
||||
$f = new DevisPdfService($kernel,$d);
|
||||
$f->generate();
|
||||
$f->Output('I');
|
||||
|
||||
|
||||
$appLogger->record('VIEW', 'Consultation de la liste des devis');
|
||||
return $this->render('dashboard/devis/list.twig',[
|
||||
'quotes' => $paginator->paginate($devisRepository->findBy([],['createA'=>'asc']),$request->get('page', 1),20),
|
||||
]);
|
||||
}
|
||||
#[Route(path: '/crm/devis/add', name: 'app_crm_devis_add', options: ['sitemap' => false], methods: ['GET','POST'])]
|
||||
public function devisAdd(ProductRepository $productRepository,EntityManagerInterface $entityManager,CustomerRepository $customerRepository,DevisRepository $devisRepository, AppLogger $appLogger,Request $request): Response
|
||||
public function devisAdd(Client $client,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');
|
||||
@@ -53,6 +52,8 @@ class DevisController extends AbstractController
|
||||
|
||||
$form = $this->createForm(NewDevisType::class,$devis);
|
||||
if($request->isMethod('POST')){
|
||||
$devis->setBillAddress($customerAddress->find($_POST['devis']['bill_address']));
|
||||
$devis->setAddressShip($customerAddress->find($_POST['devis']['ship_address']));
|
||||
$devis->setCustomer($customerRepository->find($_POST['new_devis']['customer']));
|
||||
foreach ($_POST['lines'] as $cd=>$line) {
|
||||
$rLine = new DevisLine();
|
||||
@@ -67,8 +68,33 @@ class DevisController extends AbstractController
|
||||
$entityManager->persist($rLine);
|
||||
}
|
||||
$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);
|
||||
|
||||
$fileDocuseal = new UploadedFile($tmpPathDocuseal,'dc_'.$devis->getNum() . '.pdf','application/pdf',0,true);
|
||||
$devis->setDevisDocuSealFile($fileDocuseal);
|
||||
|
||||
|
||||
$devisService = new DevisPdfService($kernel, $devis, false);
|
||||
$contentDevis = $devisService->generate();
|
||||
|
||||
$tmpPathDevis = sys_get_temp_dir() . '/devis_' . uniqid() . '.pdf';
|
||||
file_put_contents($tmpPathDevis, $contentDevis);
|
||||
|
||||
$fileDevis = new UploadedFile($tmpPathDevis,'devis_'.$devis->getNum() . '.pdf','application/pdf',0,true);
|
||||
$devis->setDevisFile($fileDevis);
|
||||
|
||||
|
||||
$devis->setState("created_waitsign");
|
||||
$devis->setUpdateAt(new \DateTimeImmutable());
|
||||
$entityManager->flush();
|
||||
$client->createSubmissionDevis($devis);
|
||||
return $this->redirectToRoute('app_crm_devis');
|
||||
}
|
||||
return $this->render('dashboard/devis/add.twig',[
|
||||
'form' => $form->createView(),
|
||||
|
||||
34
src/Controller/SignatureController.php
Normal file
34
src/Controller/SignatureController.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Account;
|
||||
use App\Entity\AccountResetPasswordRequest;
|
||||
use App\Form\RequestPasswordConfirmType;
|
||||
use App\Form\RequestPasswordRequestType;
|
||||
use App\Logger\AppLogger;
|
||||
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
|
||||
use App\Service\ResetPassword\Event\ResetPasswordEvent;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
|
||||
class SignatureController extends AbstractController
|
||||
{
|
||||
|
||||
#[Route('/signature/complete', name: 'app_sign_complete')]
|
||||
public function appSignComplete()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,11 +11,13 @@ class DevisPdfService extends Fpdf
|
||||
private Devis $devis;
|
||||
private string $logo;
|
||||
private bool $isExtraPage = false;
|
||||
private bool $isIntegrateDocusealFields;
|
||||
|
||||
public function __construct(KernelInterface $kernel, Devis $devis, $orientation = 'P', $unit = 'mm', $size = 'A4')
|
||||
public function __construct(KernelInterface $kernel, Devis $devis,bool $isIntegrateDocusealFields = false, $orientation = 'P', $unit = 'mm', $size = 'A4')
|
||||
{
|
||||
parent::__construct($orientation, $unit, $size);
|
||||
$this->devis = $devis;
|
||||
$this->isIntegrateDocusealFields = $isIntegrateDocusealFields; // Stockage
|
||||
$this->logo = $kernel->getProjectDir()."/public/provider/images/favicon.png";
|
||||
|
||||
$this->AliasNbPages();
|
||||
@@ -318,45 +320,49 @@ class DevisPdfService extends Fpdf
|
||||
|
||||
foreach ($checkPoints as $label => $role) {
|
||||
$currentY = $this->GetY();
|
||||
$this->Rect(15, $currentY + 1, 5, 5);
|
||||
$this->Rect(15, $currentY + 1, 5, 5); // Le carré reste visible pour tout le monde
|
||||
$this->SetX(25);
|
||||
$this->MultiCell(0, 7, $this->clean($label), 0, 'L');
|
||||
|
||||
// Balise DocuSeal Check
|
||||
$this->SetXY(15, $currentY + 1);
|
||||
$this->SetTextColor(255, 255, 255);
|
||||
$this->SetFont('Arial', '', 4);
|
||||
$this->Cell(5, 5, '{{Check;required=true;role=Client;name='.$role.'}}', 0, 0, 'C');
|
||||
// AJOUT CONDITIONNEL DOCUSEAL
|
||||
if ($this->isIntegrateDocusealFields) {
|
||||
$this->SetXY(15, $currentY + 1);
|
||||
$this->SetTextColor(255, 255, 255); // Blanc (invisible)
|
||||
$this->SetFont('Arial', '', 4);
|
||||
$this->Cell(5, 5, '{{Check;required=true;role=Client;name='.$role.'}}', 0, 0, 'C');
|
||||
|
||||
$this->SetTextColor(0, 0, 0);
|
||||
$this->SetFont('Arial', '', 10);
|
||||
}
|
||||
|
||||
$this->SetTextColor(0, 0, 0);
|
||||
$this->SetFont('Arial', '', 10);
|
||||
$this->Ln(4);
|
||||
}
|
||||
|
||||
$this->Ln(15);
|
||||
$ySign = $this->GetY();
|
||||
|
||||
// --- BLOCS DE SIGNATURE ---
|
||||
|
||||
// Cadre Prestataire (Lilian SEGARD)
|
||||
// --- BLOC PRESTATAIRE ---
|
||||
$this->SetXY(15, $ySign);
|
||||
$this->SetFont('Arial', 'B', 10);
|
||||
$this->Cell(85, 8, $this->clean("Le Prestataire"), 0, 1, 'C');
|
||||
$this->SetX(15);
|
||||
$this->Cell(85, 45, "", 1, 0); // Rectangle de signature
|
||||
$this->Cell(85, 45, "", 1, 0);
|
||||
|
||||
// Texte à l'intérieur du rectangle du prestataire
|
||||
$this->SetXY(17, $ySign + 12);
|
||||
$this->SetFont('Arial', 'I', 8);
|
||||
$this->SetTextColor(100, 100, 100);
|
||||
$mention = "Signée par Lilian SEGARD - Ludikevent - Le ".date('d/m/Y')." par signature numérique validée";
|
||||
$this->MultiCell(81, 5, $this->clean($mention), 0, 'C');
|
||||
|
||||
// Balise Signature Ludikevent (Cachée pour DocuSeal)
|
||||
$this->SetXY(15, $ySign + 25);
|
||||
$this->SetTextColor(255, 255, 255);
|
||||
$this->SetFont('Arial', '', 4);
|
||||
$this->Cell(85, 5, '{{Sign;type=signature;role=Ludikevent}}', 0, 0, 'C');
|
||||
// AJOUT CONDITIONNEL SIGNATURE PRESTATAIRE
|
||||
if ($this->isIntegrateDocusealFields) {
|
||||
$this->SetXY(15, $ySign + 25);
|
||||
$this->SetTextColor(255, 255, 255);
|
||||
$this->SetFont('Arial', '', 4);
|
||||
$this->Cell(85, 5, '{{Sign;type=signature;role=Ludikevent}}', 0, 0, 'C');
|
||||
}
|
||||
|
||||
// Cadre Client
|
||||
// --- BLOC CLIENT ---
|
||||
$this->SetTextColor(0, 0, 0);
|
||||
$this->SetXY(110, $ySign);
|
||||
$this->SetFont('Arial', 'B', 10);
|
||||
@@ -364,11 +370,13 @@ class DevisPdfService extends Fpdf
|
||||
$this->SetX(110);
|
||||
$this->Cell(85, 45, "", 1, 0);
|
||||
|
||||
// Balise Signature Client (Cachée pour DocuSeal)
|
||||
$this->SetXY(110, $ySign + 20);
|
||||
$this->SetTextColor(255, 255, 255);
|
||||
$this->SetFont('Arial', '', 4);
|
||||
$this->Cell(85, 5, '{{Sign;type=signature;role=Client}}', 0, 0, 'C');
|
||||
// AJOUT CONDITIONNEL SIGNATURE CLIENT
|
||||
if ($this->isIntegrateDocusealFields) {
|
||||
$this->SetXY(110, $ySign + 20);
|
||||
$this->SetTextColor(255, 255, 255);
|
||||
$this->SetFont('Arial', '', 4);
|
||||
$this->Cell(85, 5, '{{Sign;type=signature;role=Client}}', 0, 0, 'C');
|
||||
}
|
||||
}
|
||||
|
||||
private function renderAddressBlock(string $label, $address, string $align, int $x, int $y)
|
||||
@@ -382,19 +390,28 @@ class DevisPdfService extends Fpdf
|
||||
foreach ($lines as $line) { if ($line) { $this->SetX($x); $this->Cell(90, 5, $this->clean($line), 0, 1, $align); } }
|
||||
}
|
||||
|
||||
public function Footer()
|
||||
public function Footer(): void
|
||||
{
|
||||
// Positionnement à 1,5 cm du bas
|
||||
$this->SetY(-15);
|
||||
$this->SetFont('Arial', 'I', 7);
|
||||
$this->SetTextColor(150, 150, 150);
|
||||
|
||||
// BALISE DOCUSEAL CACHÉE (INITIALES)
|
||||
$this->SetFont('Arial', '', 4);
|
||||
$this->SetTextColor(255, 255, 255);
|
||||
$this->Cell(40, 10, '{{Photo;type=initials;role=Client}}', 0, 0, 'L');
|
||||
// Texte gauche : Identification du devis
|
||||
$this->Cell(0, 10, $this->clean('Devis Ludik Event - Lilian SEGARD - SIRET 93048840800012'), 0, 0, 'L');
|
||||
|
||||
// TEXTE VISIBLE
|
||||
// --- AJOUT DU CHAMP DOCUSEAL DANS LE FOOTER ---
|
||||
if ($this->isIntegrateDocusealFields) {
|
||||
$this->SetX(-60); // On se place vers la droite
|
||||
$this->SetTextColor(255, 255, 255); // Invisible
|
||||
$this->SetFont('Arial', '', 4);
|
||||
// Utilisation d'un champ "Initials" qui est souvent utilisé en bas de page
|
||||
$this->Cell(30, 10, '{{Initials;role=Client;name=paraphe}}', 0, 0, 'R');
|
||||
}
|
||||
|
||||
// Numérotation des pages à droite
|
||||
$this->SetTextColor(150, 150, 150);
|
||||
$this->SetFont('Arial', 'I', 8);
|
||||
$this->SetTextColor(128, 128, 128);
|
||||
$this->Cell(110, 10, $this->clean('Devis N° ' . $this->devis->getNum() . ' | Page ' . $this->PageNo() . '/{nb}'), 0, 0, 'C');
|
||||
$this->Cell(0, 10, 'Lilian SEGARD - Ludikevent - 930 488 408', 0, 0, 'R');
|
||||
$this->Cell(0, 10, 'Page ' . $this->PageNo() . '/{nb}', 0, 0, 'R');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ class Client
|
||||
) {
|
||||
// Configuration via les variables d'environnement
|
||||
$key = $_ENV['ESYSIGN_APIEY'] ?? '';
|
||||
$this->baseUrl = "https://signature.esy-web.dev";
|
||||
$this->baseUrl = $_ENV['SIGN_URL'];
|
||||
|
||||
// L'URL API est le point d'entrée pour le SDK Docuseal
|
||||
$apiUrl = rtrim($this->baseUrl, '/') . '/api';
|
||||
$apiUrl = rtrim("https://signature.esy-web.dev", '/') . '/api';
|
||||
$this->docuseal = new \Docuseal\Api($key, $apiUrl);
|
||||
}
|
||||
|
||||
@@ -60,6 +60,11 @@ class Client
|
||||
],
|
||||
],
|
||||
'submitters' => [
|
||||
[
|
||||
'role' => 'Ludikevent',
|
||||
'email' => 'contact@ludikevent.fr',
|
||||
'completed' => true,
|
||||
],
|
||||
[
|
||||
'role' => 'Client',
|
||||
'email' => $devis->getCustomer()->getEmail(),
|
||||
@@ -73,7 +78,7 @@ class Client
|
||||
]);
|
||||
|
||||
// Stockage de l'ID submitter de Docuseal dans ton entité
|
||||
$devis->setSignatureId($submission['submitters'][0]['id']);
|
||||
$devis->setSignatureId($submission['submitters'][1]['id']);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
|
||||
@@ -63,19 +63,17 @@
|
||||
{# STATUT DYNAMIQUE #}
|
||||
<td class="px-6 py-4">
|
||||
{% set statusClasses = {
|
||||
'brouillon': 'text-slate-400 bg-slate-500/10 border-slate-500/20',
|
||||
'crée': 'text-indigo-400 bg-indigo-500/10 border-indigo-500/20',
|
||||
'envoyée': 'text-blue-400 bg-blue-500/10 border-blue-500/20',
|
||||
'en attends de signature': 'text-amber-400 bg-amber-500/10 border-amber-500/20',
|
||||
'draft': 'text-slate-400 bg-slate-500/10 border-slate-500/20',
|
||||
'created_waitsign': 'text-amber-400 bg-amber-500/10 border-amber-500/20',
|
||||
'refusée': 'text-rose-400 bg-rose-500/10 border-rose-500/20',
|
||||
'signée': 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20'
|
||||
} %}
|
||||
|
||||
{% set statusLabels = {
|
||||
'brouillon': 'Brouillon',
|
||||
'draft': 'Brouillon',
|
||||
'crée': 'Créé',
|
||||
'envoyée': 'Envoyé',
|
||||
'en attends de signature': 'Attente Signature',
|
||||
'created_waitsign': 'Attente Signature',
|
||||
'refusée': 'Refusé',
|
||||
'signée': 'Signé'
|
||||
} %}
|
||||
|
||||
Reference in New Issue
Block a user