feat: PDF echeancier + signature DocuSeal + email + page client
EcheancierPdf :
- PDF FPDF avec bloc legal, description, tableau echeances, conditions
- 2 champs signature DocuSeal : Company (auto-signe E-Cosplay) + First Party (client)
Controller :
- generate-pdf : genere le PDF via EcheancierPdf + Vich upload
- send-signature : envoie PDF a DocuSeal (2 parties), email avec bouton signer
- resend : renvoie email proposition
- DocuSealService.getLogoBase64 rendu public
EcheancierProcessController (public) :
- /echeancier/signed/{id} : callback post-signature, passe state a signed
Templates :
- echeancier/signed.html.twig : page confirmation signature client
- emails/echeancier_signature.html.twig : email avec bouton signer
- admin/echeancier/show : boutons generer PDF, voir PDF, envoyer proposition,
envoyer signature, renvoyer, PDF signe, activer Stripe, annuler
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
35
migrations/Version20260408174704.php
Normal file
35
migrations/Version20260408174704.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?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 Version20260408174704 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 advert_payment ADD echeancier_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE advert_payment ADD CONSTRAINT FK_C766C45B8C858AF2 FOREIGN KEY (echeancier_id) REFERENCES echeancier (id) ON DELETE SET NULL NOT DEFERRABLE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_C766C45B8C858AF2 ON advert_payment (echeancier_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE advert_payment DROP CONSTRAINT FK_C766C45B8C858AF2');
|
||||||
|
$this->addSql('DROP INDEX IDX_C766C45B8C858AF2');
|
||||||
|
$this->addSql('ALTER TABLE advert_payment DROP echeancier_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,10 @@ use App\Entity\Echeancier;
|
|||||||
use App\Entity\EcheancierLine;
|
use App\Entity\EcheancierLine;
|
||||||
use App\Service\DocuSealService;
|
use App\Service\DocuSealService;
|
||||||
use App\Service\MailerService;
|
use App\Service\MailerService;
|
||||||
|
use App\Service\Pdf\EcheancierPdf;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
use Symfony\Component\HttpKernel\KernelInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -132,6 +135,183 @@ class EcheancierController extends AbstractController
|
|||||||
return $this->redirectToRoute('app_admin_echeancier_show', ['id' => $id]);
|
return $this->redirectToRoute('app_admin_echeancier_show', ['id' => $id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renvoie l'email de proposition au client.
|
||||||
|
*/
|
||||||
|
#[Route('/{id}/resend', name: 'resend', requirements: ['id' => '\d+'], methods: ['POST'])]
|
||||||
|
public function resend(int $id, MailerService $mailer, Environment $twig): Response
|
||||||
|
{
|
||||||
|
$echeancier = $this->em->getRepository(Echeancier::class)->find($id);
|
||||||
|
if (null === $echeancier) {
|
||||||
|
throw $this->createNotFoundException(self::MSG_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$customer = $echeancier->getCustomer();
|
||||||
|
if (null === $customer->getEmail()) {
|
||||||
|
$this->addFlash('error', 'Email client introuvable.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_admin_echeancier_show', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = $twig->render('emails/echeancier_proposition.html.twig', [
|
||||||
|
'customer' => $customer,
|
||||||
|
'echeancier' => $echeancier,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mailer->sendEmail(
|
||||||
|
$customer->getEmail(),
|
||||||
|
'Rappel - Proposition d\'echeancier de paiement',
|
||||||
|
$html,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Rappel envoye a '.$customer->getEmail().'.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_admin_echeancier_show', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Genere le PDF de l'echeancier.
|
||||||
|
*/
|
||||||
|
#[Route('/{id}/generate-pdf', name: 'generate_pdf', requirements: ['id' => '\d+'], methods: ['POST'])]
|
||||||
|
public function generatePdf(int $id, KernelInterface $kernel): Response
|
||||||
|
{
|
||||||
|
$echeancier = $this->em->getRepository(Echeancier::class)->find($id);
|
||||||
|
if (null === $echeancier) {
|
||||||
|
throw $this->createNotFoundException(self::MSG_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdf = new EcheancierPdf($kernel, $echeancier);
|
||||||
|
$pdf->generate();
|
||||||
|
|
||||||
|
$tmpPath = tempnam(sys_get_temp_dir(), 'echeancier_').'.pdf';
|
||||||
|
$pdf->Output('F', $tmpPath);
|
||||||
|
|
||||||
|
$echeancier->setPdfUnsignedFile(new UploadedFile(
|
||||||
|
$tmpPath,
|
||||||
|
'echeancier-'.$echeancier->getId().'.pdf',
|
||||||
|
'application/pdf',
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
$echeancier->setUpdatedAt(new \DateTimeImmutable());
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
@unlink($tmpPath);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'PDF echeancier genere.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_admin_echeancier_show', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie le PDF pour signature via DocuSeal (2 parties : Company auto-signe + Client signe).
|
||||||
|
*/
|
||||||
|
#[Route('/{id}/send-signature', name: 'send_signature', requirements: ['id' => '\d+'], methods: ['POST'])]
|
||||||
|
public function sendSignature(
|
||||||
|
int $id,
|
||||||
|
DocuSealService $docuSeal,
|
||||||
|
MailerService $mailer,
|
||||||
|
Environment $twig,
|
||||||
|
UrlGeneratorInterface $urlGenerator,
|
||||||
|
#[Autowire(env: 'DOCUSEAL_URL')] string $docuSealUrl = '',
|
||||||
|
#[Autowire('%kernel.project_dir%')] string $projectDir = '',
|
||||||
|
): Response {
|
||||||
|
$echeancier = $this->em->getRepository(Echeancier::class)->find($id);
|
||||||
|
if (null === $echeancier) {
|
||||||
|
throw $this->createNotFoundException(self::MSG_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$customer = $echeancier->getCustomer();
|
||||||
|
if (null === $echeancier->getPdfUnsigned() || null === $customer->getEmail()) {
|
||||||
|
$this->addFlash('error', null === $echeancier->getPdfUnsigned() ? 'Le PDF doit etre genere avant l\'envoi.' : 'Email client introuvable.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_admin_echeancier_show', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdfPath = $projectDir.'/public/uploads/echeanciers/'.$echeancier->getPdfUnsigned();
|
||||||
|
if (!file_exists($pdfPath)) {
|
||||||
|
$this->addFlash('error', 'Fichier PDF introuvable.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_admin_echeancier_show', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$signedRedirectUrl = $urlGenerator->generate('app_echeancier_signed', [
|
||||||
|
'id' => $echeancier->getId(),
|
||||||
|
], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdfBase64 = base64_encode(file_get_contents($pdfPath));
|
||||||
|
|
||||||
|
$result = $docuSeal->getApi()->createSubmissionFromPdf([
|
||||||
|
'name' => 'Echeancier - '.$customer->getFullName(),
|
||||||
|
'send_email' => false,
|
||||||
|
'flatten' => true,
|
||||||
|
'documents' => [
|
||||||
|
[
|
||||||
|
'name' => 'echeancier-'.$echeancier->getId().'.pdf',
|
||||||
|
'file' => 'data:application/pdf;base64,'.$pdfBase64,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'submitters' => [
|
||||||
|
[
|
||||||
|
'email' => 'contact@e-cosplay.fr',
|
||||||
|
'name' => 'Association E-Cosplay',
|
||||||
|
'role' => 'Company',
|
||||||
|
'completed' => true,
|
||||||
|
'send_email' => false,
|
||||||
|
'values' => ['Sign' => $docuSeal->getLogoBase64()],
|
||||||
|
'metadata' => ['doc_type' => 'echeancier', 'echeancier_id' => $echeancier->getId()],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'email' => $customer->getEmail(),
|
||||||
|
'name' => $customer->getFullName(),
|
||||||
|
'role' => 'First Party',
|
||||||
|
'send_email' => false,
|
||||||
|
'completed_redirect_url' => $signedRedirectUrl,
|
||||||
|
'metadata' => ['doc_type' => 'echeancier', 'echeancier_id' => $echeancier->getId()],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$submitterId = $result['submitters'][1]['id'] ?? ($result[1]['id'] ?? null);
|
||||||
|
if (null !== $submitterId) {
|
||||||
|
$echeancier->setSubmissionId((string) $submitterId);
|
||||||
|
$echeancier->setState(Echeancier::STATE_SEND);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
// Envoyer email au client avec lien de signature
|
||||||
|
$slug = $docuSeal->getSubmitterSlug($submitterId);
|
||||||
|
$signUrl = null !== $slug ? rtrim($docuSealUrl, '/').'/s/'.$slug : null;
|
||||||
|
|
||||||
|
$html = $twig->render('emails/echeancier_signature.html.twig', [
|
||||||
|
'customer' => $customer,
|
||||||
|
'echeancier' => $echeancier,
|
||||||
|
'signUrl' => $signUrl,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mailer->sendEmail(
|
||||||
|
$customer->getEmail(),
|
||||||
|
'Echeancier a signer - '.$customer->getFullName(),
|
||||||
|
$html,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Echeancier envoye pour signature a '.$customer->getEmail().'.');
|
||||||
|
} else {
|
||||||
|
$this->addFlash('error', 'Erreur DocuSeal : aucun submitter retourne.');
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->addFlash('error', 'Erreur DocuSeal : '.$e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_admin_echeancier_show', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Annule un echeancier (et la subscription Stripe si active).
|
* Annule un echeancier (et la subscription Stripe si active).
|
||||||
*/
|
*/
|
||||||
|
|||||||
39
src/Controller/EcheancierProcessController.php
Normal file
39
src/Controller/EcheancierProcessController.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Echeancier;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
class EcheancierProcessController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManagerInterface $em,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback DocuSeal apres signature du client.
|
||||||
|
*/
|
||||||
|
#[Route('/echeancier/signed/{id}', name: 'app_echeancier_signed', requirements: ['id' => '\d+'])]
|
||||||
|
public function signed(int $id): Response
|
||||||
|
{
|
||||||
|
$echeancier = $this->em->getRepository(Echeancier::class)->find($id);
|
||||||
|
if (null === $echeancier) {
|
||||||
|
throw $this->createNotFoundException('Echeancier introuvable.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Echeancier::STATE_SIGNED !== $echeancier->getState()) {
|
||||||
|
$echeancier->setState(Echeancier::STATE_SIGNED);
|
||||||
|
$this->em->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('echeancier/signed.html.twig', [
|
||||||
|
'echeancier' => $echeancier,
|
||||||
|
'customer' => $echeancier->getCustomer(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -349,7 +349,7 @@ class DocuSealService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getLogoBase64(): string
|
public function getLogoBase64(): string
|
||||||
{
|
{
|
||||||
$logoPath = $this->projectDir.'/public/logo.jpg';
|
$logoPath = $this->projectDir.'/public/logo.jpg';
|
||||||
|
|
||||||
|
|||||||
276
src/Service/Pdf/EcheancierPdf.php
Normal file
276
src/Service/Pdf/EcheancierPdf.php
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service\Pdf;
|
||||||
|
|
||||||
|
use App\Entity\Echeancier;
|
||||||
|
use setasign\Fpdi\Fpdi;
|
||||||
|
use Symfony\Component\HttpKernel\KernelInterface;
|
||||||
|
|
||||||
|
if (!\defined('EURO')) {
|
||||||
|
\define('EURO', \chr(128)); // @codeCoverageIgnore
|
||||||
|
}
|
||||||
|
|
||||||
|
class EcheancierPdf extends Fpdi
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly KernelInterface $kernel,
|
||||||
|
private readonly Echeancier $echeancier,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
$this->SetTitle($this->enc('Echeancier de paiement - '.$this->echeancier->getCustomer()->getFullName()));
|
||||||
|
$this->SetAuthor($this->enc('Association E-Cosplay'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generate(): void
|
||||||
|
{
|
||||||
|
$this->AliasNbPages();
|
||||||
|
$this->AddPage();
|
||||||
|
|
||||||
|
$this->writeHeader();
|
||||||
|
$this->writeContextBlock();
|
||||||
|
$this->writeEcheancesTable();
|
||||||
|
$this->writeConditions();
|
||||||
|
$this->writeSignatures();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @codeCoverageIgnore */
|
||||||
|
public function Header(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @codeCoverageIgnore */
|
||||||
|
public function Footer(): void
|
||||||
|
{
|
||||||
|
$this->SetY(-22);
|
||||||
|
$this->SetDrawColor(253, 140, 4);
|
||||||
|
$this->Line(15, $this->GetY(), 195, $this->GetY());
|
||||||
|
$this->Ln(3);
|
||||||
|
$this->SetFont('Arial', '', 7);
|
||||||
|
$this->SetTextColor(0, 0, 0);
|
||||||
|
$this->Cell(190, 3, $this->enc('42 rue de Saint-Quentin - 02800 BEAUTOR - Tel: 06 79 34 88 02 - contact@e-cosplay.fr'), 0, 1, 'C');
|
||||||
|
$this->Cell(190, 3, $this->enc('Association E-Cosplay - N SIRET 943 121 517 00011 - CODE APE 9329Z - RNA W022006988'), 0, 1, 'C');
|
||||||
|
$this->SetFont('Arial', 'I', 7);
|
||||||
|
$this->SetTextColor(150, 150, 150);
|
||||||
|
$this->Cell(190, 3, $this->enc('Page ').$this->PageNo().' / {nb}', 0, 0, 'C');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @codeCoverageIgnore */
|
||||||
|
private function writeHeader(): void
|
||||||
|
{
|
||||||
|
$logo = $this->kernel->getProjectDir().'/public/logo.jpg';
|
||||||
|
if (file_exists($logo)) {
|
||||||
|
$this->Image($logo, 10, 8, 45);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->SetFont('Arial', 'B', 16);
|
||||||
|
$this->SetXY(60, 10);
|
||||||
|
$this->Cell(0, 8, $this->enc('ECHEANCIER DE PAIEMENT'), 0, 1, 'L');
|
||||||
|
|
||||||
|
$formatter = new \IntlDateFormatter(
|
||||||
|
'fr_FR',
|
||||||
|
\IntlDateFormatter::FULL,
|
||||||
|
\IntlDateFormatter::NONE,
|
||||||
|
'Europe/Paris',
|
||||||
|
\IntlDateFormatter::GREGORIAN
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->SetFont('Arial', '', 10);
|
||||||
|
$this->SetXY(60, 19);
|
||||||
|
$this->Cell(0, 5, $this->enc('Emis a Beautor, le '.$formatter->format($this->echeancier->getCreatedAt())), 0, 1, 'L');
|
||||||
|
|
||||||
|
// Client
|
||||||
|
$this->SetFont('Arial', 'B', 11);
|
||||||
|
$customer = $this->echeancier->getCustomer();
|
||||||
|
$y = 35;
|
||||||
|
$this->SetXY(120, $y);
|
||||||
|
$name = $customer->getRaisonSociale() ?: $customer->getFullName();
|
||||||
|
$this->Cell(0, 5, $this->enc($name), 0, 1, 'L');
|
||||||
|
|
||||||
|
if ($address = $customer->getAddress()) {
|
||||||
|
$y += 5;
|
||||||
|
$this->SetXY(120, $y);
|
||||||
|
$this->SetFont('Arial', '', 10);
|
||||||
|
$this->Cell(0, 5, $this->enc($address), 0, 1, 'L');
|
||||||
|
}
|
||||||
|
|
||||||
|
$y += 5;
|
||||||
|
$this->SetXY(120, $y);
|
||||||
|
$this->SetFont('Arial', '', 10);
|
||||||
|
$cityLine = ($customer->getZipCode() ?? '').' '.($customer->getCity() ?? '');
|
||||||
|
$this->Cell(0, 5, $this->enc(trim($cityLine)), 0, 1, 'L');
|
||||||
|
|
||||||
|
if ($customer->getEmail()) {
|
||||||
|
$y += 5;
|
||||||
|
$this->SetXY(120, $y);
|
||||||
|
$this->Cell(0, 5, $this->enc($customer->getEmail()), 0, 1, 'L');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->Ln(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @codeCoverageIgnore */
|
||||||
|
private function writeContextBlock(): void
|
||||||
|
{
|
||||||
|
$this->SetY(65);
|
||||||
|
|
||||||
|
$this->SetDrawColor(200, 200, 200);
|
||||||
|
$this->Cell(0, 0.5, '', 'T', 1, 'L');
|
||||||
|
$this->Ln(3);
|
||||||
|
|
||||||
|
$this->SetFont('Arial', 'B', 11);
|
||||||
|
$this->Cell(0, 6, $this->enc('OBJET DE L\'ECHEANCIER'), 0, 1, 'L');
|
||||||
|
$this->Ln(2);
|
||||||
|
|
||||||
|
$this->SetFont('Arial', '', 10);
|
||||||
|
$this->MultiCell(0, 5, $this->enc($this->echeancier->getDescription()), 0, 'L');
|
||||||
|
$this->Ln(3);
|
||||||
|
|
||||||
|
$labelW = 55;
|
||||||
|
|
||||||
|
$this->SetFont('Arial', '', 10);
|
||||||
|
$this->Cell($labelW, 6, $this->enc('Montant total HT :'), 0, 0, 'L');
|
||||||
|
$this->SetFont('Arial', 'B', 10);
|
||||||
|
$this->Cell(0, 6, number_format((float) $this->echeancier->getTotalAmountHt(), 2, ',', ' ').' '.EURO, 0, 1, 'L');
|
||||||
|
|
||||||
|
$this->SetFont('Arial', '', 10);
|
||||||
|
$this->Cell($labelW, 6, $this->enc('Nombre d\'echeances :'), 0, 0, 'L');
|
||||||
|
$this->SetFont('Arial', 'B', 10);
|
||||||
|
$this->Cell(0, 6, (string) $this->echeancier->getNbLines().' mois', 0, 1, 'L');
|
||||||
|
|
||||||
|
$this->SetFont('Arial', '', 10);
|
||||||
|
$this->Cell($labelW, 6, $this->enc('Mensualite :'), 0, 0, 'L');
|
||||||
|
$this->SetFont('Arial', 'B', 10);
|
||||||
|
$this->Cell(0, 6, number_format($this->echeancier->getMonthlyAmount(), 2, ',', ' ').' '.EURO.'/mois', 0, 1, 'L');
|
||||||
|
|
||||||
|
$this->Ln(2);
|
||||||
|
$this->Cell(0, 0.5, '', 'T', 1, 'L');
|
||||||
|
$this->Ln(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @codeCoverageIgnore */
|
||||||
|
private function writeEcheancesTable(): void
|
||||||
|
{
|
||||||
|
$this->SetFont('Arial', 'B', 11);
|
||||||
|
$this->Cell(0, 6, $this->enc('TABLEAU DES ECHEANCES'), 0, 1, 'L');
|
||||||
|
$this->Ln(2);
|
||||||
|
|
||||||
|
// En-tete tableau
|
||||||
|
$this->SetFont('Arial', 'B', 9);
|
||||||
|
$this->SetFillColor(35, 35, 35);
|
||||||
|
$this->SetTextColor(255, 255, 255);
|
||||||
|
$this->Cell(15, 7, $this->enc('N'), 1, 0, 'C', true);
|
||||||
|
$this->Cell(55, 7, $this->enc('Date de prelevement'), 1, 0, 'C', true);
|
||||||
|
$this->Cell(50, 7, $this->enc('Montant HT'), 1, 0, 'C', true);
|
||||||
|
$this->Cell(50, 7, $this->enc('Statut'), 1, 1, 'C', true);
|
||||||
|
$this->SetTextColor(0, 0, 0);
|
||||||
|
|
||||||
|
$this->SetFont('Arial', '', 9);
|
||||||
|
$fill = false;
|
||||||
|
$months = [1 => 'Janvier', 'Fevrier', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Aout', 'Septembre', 'Octobre', 'Novembre', 'Decembre'];
|
||||||
|
|
||||||
|
foreach ($this->echeancier->getLines() as $line) {
|
||||||
|
$this->SetFillColor(245, 245, 240);
|
||||||
|
|
||||||
|
$monthName = $months[(int) $line->getScheduledAt()->format('n')] ?? '';
|
||||||
|
$dateLabel = $line->getScheduledAt()->format('d').' '.$monthName.' '.$line->getScheduledAt()->format('Y');
|
||||||
|
|
||||||
|
$this->Cell(15, 6, (string) $line->getPosition(), 'B', 0, 'C', $fill);
|
||||||
|
$this->Cell(55, 6, $this->enc($dateLabel), 'B', 0, 'L', $fill);
|
||||||
|
$this->Cell(50, 6, number_format((float) $line->getAmount(), 2, ',', ' ').' '.EURO, 'B', 0, 'R', $fill);
|
||||||
|
$this->Cell(50, 6, $this->enc('A prelever'), 'B', 1, 'C', $fill);
|
||||||
|
|
||||||
|
$fill = !$fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total
|
||||||
|
$this->SetFont('Arial', 'B', 10);
|
||||||
|
$this->SetFillColor(253, 191, 4);
|
||||||
|
$this->Cell(70, 8, $this->enc(' TOTAL'), 0, 0, 'L', true);
|
||||||
|
$this->Cell(50, 8, number_format((float) $this->echeancier->getTotalAmountHt(), 2, ',', ' ').' '.EURO, 0, 0, 'R', true);
|
||||||
|
$this->Cell(50, 8, '', 0, 1, 'C', true);
|
||||||
|
|
||||||
|
$this->Ln(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @codeCoverageIgnore */
|
||||||
|
private function writeConditions(): void
|
||||||
|
{
|
||||||
|
$this->SetFont('Arial', 'B', 10);
|
||||||
|
$this->Cell(0, 6, $this->enc('CONDITIONS'), 0, 1, 'L');
|
||||||
|
$this->Ln(1);
|
||||||
|
|
||||||
|
$this->SetFont('Arial', '', 9);
|
||||||
|
$conditions = [
|
||||||
|
'Le prelevement sera effectue automatiquement a chaque date prevue via Stripe.',
|
||||||
|
'En cas d\'echec de prelevement, une relance sera envoyee par email.',
|
||||||
|
'Apres 2 echecs consecutifs, l\'echeancier sera considere en defaut.',
|
||||||
|
'Le client peut contacter contact@e-cosplay.fr pour toute question.',
|
||||||
|
'Majoration de 5% du montant total conformement aux CGV (article 11).',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($conditions as $i => $condition) {
|
||||||
|
$this->Cell(5, 5, '', 0, 0);
|
||||||
|
$this->Cell(5, 5, ($i + 1).'.', 0, 0, 'R');
|
||||||
|
$this->Cell(0, 5, $this->enc(' '.$condition), 0, 1, 'L');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->Ln(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @codeCoverageIgnore */
|
||||||
|
private function writeSignatures(): void
|
||||||
|
{
|
||||||
|
// S'assurer qu'on a assez de place
|
||||||
|
if ($this->GetY() + 50 > $this->GetPageHeight() - 25) {
|
||||||
|
$this->AddPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->SetDrawColor(200, 200, 200);
|
||||||
|
$this->Cell(0, 0.5, '', 'T', 1, 'L');
|
||||||
|
$this->Ln(3);
|
||||||
|
|
||||||
|
$formatter = new \IntlDateFormatter(
|
||||||
|
'fr_FR',
|
||||||
|
\IntlDateFormatter::LONG,
|
||||||
|
\IntlDateFormatter::NONE,
|
||||||
|
'Europe/Paris',
|
||||||
|
\IntlDateFormatter::GREGORIAN
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->SetFont('Arial', '', 9);
|
||||||
|
$this->Cell(0, 5, $this->enc('Fait a Beautor, le '.$formatter->format(new \DateTime())), 0, 1, 'L');
|
||||||
|
$this->Ln(3);
|
||||||
|
|
||||||
|
// 2 colonnes de signature
|
||||||
|
$colWidth = 85;
|
||||||
|
$signY = $this->GetY();
|
||||||
|
|
||||||
|
// Signature E-Cosplay (gauche) - auto-signee
|
||||||
|
$this->SetFont('Arial', 'B', 9);
|
||||||
|
$this->SetXY(15, $signY);
|
||||||
|
$this->Cell($colWidth, 5, $this->enc('Pour Association E-Cosplay :'), 0, 1, 'L');
|
||||||
|
$this->SetXY(15, $signY + 7);
|
||||||
|
$this->SetFont('Arial', '', 10);
|
||||||
|
$this->Cell($colWidth, 20, '{{Sign;type=signature;role=Company}}', 0, 0, 'L');
|
||||||
|
|
||||||
|
// Signature Client (droite)
|
||||||
|
$this->SetFont('Arial', 'B', 9);
|
||||||
|
$this->SetXY(110, $signY);
|
||||||
|
$customerName = $this->echeancier->getCustomer()->getRaisonSociale() ?: $this->echeancier->getCustomer()->getFullName();
|
||||||
|
$this->Cell($colWidth, 5, $this->enc('Pour '.$customerName.' :'), 0, 1, 'L');
|
||||||
|
$this->SetXY(110, $signY + 7);
|
||||||
|
$this->SetFont('Arial', '', 10);
|
||||||
|
$this->Cell($colWidth, 20, '{{SignClient;type=signature;role=First Party}}', 0, 0, 'L');
|
||||||
|
|
||||||
|
$this->SetY($signY + 35);
|
||||||
|
$this->SetFont('Arial', 'I', 8);
|
||||||
|
$this->SetTextColor(150, 150, 150);
|
||||||
|
$this->Cell(0, 4, $this->enc('Signature electronique via DocuSeal - Valeur juridique (reglement eIDAS, art. 1367 Code civil)'), 0, 1, 'C');
|
||||||
|
$this->SetTextColor(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function enc(string $text): string
|
||||||
|
{
|
||||||
|
return mb_convert_encoding($text, 'Windows-1252', 'UTF-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,10 +66,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Actions #}
|
{# Actions #}
|
||||||
<div class="flex gap-2 mb-6">
|
<div class="flex flex-wrap gap-2 mb-6">
|
||||||
{% if echeancier.state == 'draft' %}
|
{% if echeancier.state == 'draft' %}
|
||||||
|
<form method="post" action="{{ path('app_admin_echeancier_generate_pdf', {id: echeancier.id}) }}">
|
||||||
|
<button type="submit" class="px-4 py-2 bg-gray-900 text-white font-bold uppercase text-[10px] tracking-wider hover:bg-[#fabf04] hover:text-gray-900 transition-all">Generer PDF</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% if echeancier.pdfUnsigned %}
|
||||||
|
<a href="{{ vich_uploader_asset(echeancier, 'pdfUnsignedFile') }}" target="_blank"
|
||||||
|
class="px-4 py-2 bg-gray-900 text-white font-bold uppercase text-[10px] tracking-wider hover:bg-[#fabf04] hover:text-gray-900 transition-all">
|
||||||
|
Voir PDF
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if echeancier.state == 'draft' and echeancier.pdfUnsigned %}
|
||||||
<form method="post" action="{{ path('app_admin_echeancier_send', {id: echeancier.id}) }}" data-confirm="Envoyer la proposition d'echeancier au client ?">
|
<form method="post" action="{{ path('app_admin_echeancier_send', {id: echeancier.id}) }}" data-confirm="Envoyer la proposition d'echeancier au client ?">
|
||||||
<button type="submit" class="btn-gold px-4 py-2 font-bold uppercase text-[10px] tracking-wider text-gray-900">Envoyer au client</button>
|
<button type="submit" class="btn-gold px-4 py-2 font-bold uppercase text-[10px] tracking-wider text-gray-900">Envoyer proposition</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% if echeancier.state in ['draft', 'send'] and echeancier.pdfUnsigned %}
|
||||||
|
<form method="post" action="{{ path('app_admin_echeancier_send_signature', {id: echeancier.id}) }}" data-confirm="Envoyer le PDF pour signature au client via DocuSeal ?">
|
||||||
|
<button type="submit" class="px-4 py-2 bg-purple-500/20 text-purple-700 hover:bg-purple-500 hover:text-white font-bold uppercase text-[10px] tracking-wider transition-all">Envoyer pour signature</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% if echeancier.state == 'send' %}
|
||||||
|
<form method="post" action="{{ path('app_admin_echeancier_resend', {id: echeancier.id}) }}" data-confirm="Renvoyer l'email de proposition au client ?">
|
||||||
|
<button type="submit" class="px-4 py-2 bg-purple-500/20 text-purple-700 hover:bg-purple-500 hover:text-white font-bold uppercase text-[10px] tracking-wider transition-all">Renvoyer email</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if echeancier.state == 'signed' %}
|
{% if echeancier.state == 'signed' %}
|
||||||
@@ -77,6 +98,12 @@
|
|||||||
<button type="submit" class="px-4 py-2 bg-green-600 text-white font-bold uppercase text-[10px] tracking-wider hover:bg-green-700 transition-all">Activer Stripe</button>
|
<button type="submit" class="px-4 py-2 bg-green-600 text-white font-bold uppercase text-[10px] tracking-wider hover:bg-green-700 transition-all">Activer Stripe</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if echeancier.pdfSigned %}
|
||||||
|
<a href="{{ vich_uploader_asset(echeancier, 'pdfSignedFile') }}" target="_blank"
|
||||||
|
class="px-4 py-2 bg-green-500/20 text-green-700 font-bold uppercase text-[10px] tracking-wider hover:bg-green-500 hover:text-white transition-all">
|
||||||
|
PDF signe
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% if echeancier.state in ['draft', 'send', 'signed', 'active'] %}
|
{% if echeancier.state in ['draft', 'send', 'signed', 'active'] %}
|
||||||
<form method="post" action="{{ path('app_admin_echeancier_cancel', {id: echeancier.id}) }}" data-confirm="Annuler cet echeancier ? La subscription Stripe sera annulee.">
|
<form method="post" action="{{ path('app_admin_echeancier_cancel', {id: echeancier.id}) }}" data-confirm="Annuler cet echeancier ? La subscription Stripe sera annulee.">
|
||||||
<button type="submit" class="px-4 py-2 bg-red-500/20 text-red-700 hover:bg-red-500 hover:text-white font-bold uppercase text-[10px] tracking-wider transition-all">Annuler</button>
|
<button type="submit" class="px-4 py-2 bg-red-500/20 text-red-700 hover:bg-red-500 hover:text-white font-bold uppercase text-[10px] tracking-wider transition-all">Annuler</button>
|
||||||
|
|||||||
32
templates/echeancier/signed.html.twig
Normal file
32
templates/echeancier/signed.html.twig
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Echeancier signe - Association E-Cosplay{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="min-h-screen flex items-center justify-center p-4" style="background: linear-gradient(135deg, #f5f5f0 0%, #e8e8e0 100%);">
|
||||||
|
<div class="glass-heavy w-full max-w-lg overflow-hidden">
|
||||||
|
<div class="glass-dark text-white px-8 py-6 text-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-3 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<h1 class="text-xl font-bold uppercase tracking-widest">Echeancier signe</h1>
|
||||||
|
</div>
|
||||||
|
<div class="p-8 text-center">
|
||||||
|
<p class="text-sm text-gray-600 mb-4">
|
||||||
|
Merci <strong>{{ customer.firstName }}</strong>, votre echeancier de paiement a ete signe avec succes.
|
||||||
|
</p>
|
||||||
|
<div class="glass p-4 mb-4 text-left">
|
||||||
|
<p class="text-xs text-gray-500"><strong>Montant total :</strong> {{ echeancier.totalAmountHt|number_format(2, ',', ' ') }} €</p>
|
||||||
|
<p class="text-xs text-gray-500"><strong>Echeances :</strong> {{ echeancier.nbLines }} x {{ echeancier.monthlyAmount|number_format(2, ',', ' ') }} €/mois</p>
|
||||||
|
<p class="text-xs text-gray-500"><strong>Motif :</strong> {{ echeancier.description }}</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-400">
|
||||||
|
Le prelevement automatique sera active par notre equipe. Vous recevrez un email de confirmation a chaque echeance.
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-400 mt-4">
|
||||||
|
Pour toute question : <a href="mailto:contact@e-cosplay.fr" class="text-[#fabf04] font-bold">contact@e-cosplay.fr</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
49
templates/emails/echeancier_signature.html.twig
Normal file
49
templates/emails/echeancier_signature.html.twig
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{% extends 'email/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<table width="600" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 32px;">
|
||||||
|
{% set greeting = customer.raisonSociale ? 'Chez ' ~ customer.raisonSociale : 'Bonjour ' ~ customer.firstName %}
|
||||||
|
<h1 style="font-family: Arial, Helvetica, sans-serif; font-size: 22px; font-weight: 700; color: #111827; margin: 0 0 16px;">{{ greeting }},</h1>
|
||||||
|
|
||||||
|
<p style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; color: #374151; line-height: 22px; margin: 0 0 16px;">
|
||||||
|
Votre echeancier de paiement est pret a etre signe. Veuillez cliquer sur le bouton ci-dessous pour signer electroniquement le document.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 20px 0; border: 1px solid #e5e7eb;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px 16px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #9ca3af; background: #f9fafb; width: 35%;">Motif</td>
|
||||||
|
<td style="padding: 10px 16px; font-size: 13px; font-weight: 700;">{{ echeancier.description }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px 16px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #9ca3af; background: #f9fafb;">Montant total</td>
|
||||||
|
<td style="padding: 10px 16px; font-family: monospace; font-size: 16px; font-weight: 700;">{{ echeancier.totalAmountHt }} €</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px 16px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #9ca3af; background: #f9fafb;">Mensualite</td>
|
||||||
|
<td style="padding: 10px 16px; font-family: monospace; font-size: 14px; font-weight: 700; color: #fabf04;">{{ echeancier.monthlyAmount|number_format(2, ',', ' ') }} €/mois x {{ echeancier.nbLines }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if signUrl %}
|
||||||
|
<table cellpadding="0" cellspacing="0" style="margin: 24px auto;">
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #fabf04; padding: 14px 32px;">
|
||||||
|
<a href="{{ signUrl }}" style="font-family: Arial, Helvetica, sans-serif; font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #111827; text-decoration: none;">Signer l'echeancier</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; color: #374151; line-height: 22px; margin: 16px 0;">
|
||||||
|
En signant ce document, vous autorisez le prelevement automatique mensuel du montant indique via Stripe.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="font-family: Arial, Helvetica, sans-serif; font-size: 12px; color: #9ca3af; margin: 16px 0 0;">
|
||||||
|
Pour toute question : <a href="mailto:contact@e-cosplay.fr" style="color: #fabf04;">contact@e-cosplay.fr</a>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user