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:
Serreau Jovann
2026-04-08 19:53:10 +02:00
parent 978fcb9156
commit 46ddb5786a
8 changed files with 641 additions and 3 deletions

View 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');
}
}

View File

@@ -7,7 +7,10 @@ use App\Entity\Echeancier;
use App\Entity\EcheancierLine;
use App\Service\DocuSealService;
use App\Service\MailerService;
use App\Service\Pdf\EcheancierPdf;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request;
@@ -132,6 +135,183 @@ class EcheancierController extends AbstractController
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).
*/

View 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(),
]);
}
}

View File

@@ -349,7 +349,7 @@ class DocuSealService
}
}
private function getLogoBase64(): string
public function getLogoBase64(): string
{
$logoPath = $this->projectDir.'/public/logo.jpg';

View 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');
}
}

View File

@@ -66,10 +66,31 @@
</div>
{# Actions #}
<div class="flex gap-2 mb-6">
<div class="flex flex-wrap gap-2 mb-6">
{% 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 ?">
<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>
{% endif %}
{% 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>
</form>
{% 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'] %}
<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>

View 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, ',', ' ') }} &euro;</p>
<p class="text-xs text-gray-500"><strong>Echeances :</strong> {{ echeancier.nbLines }} x {{ echeancier.monthlyAmount|number_format(2, ',', ' ') }} &euro;/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 %}

View 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 }} &euro;</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, ',', ' ') }} &euro;/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 %}