feat: page process echeancier + bouton signature unique + email details

- Page publique /echeancier/process/{id} avec detail complet avant signature
  (motif, resume, tableau echeances, conditions)
- Redirige vers page signed si deja signe/actif/termine
- Bouton unique "Envoyer pour signature" / "Renvoyer signature" selon state
- Suppression bouton "Envoyer proposition" (remplace par signature directe)
- Email signature : ajout bouton "Voir les details" + lien processUrl

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-08 20:09:24 +02:00
parent 3c5d9c0f94
commit fe630317d3
5 changed files with 129 additions and 13 deletions

View File

@@ -294,10 +294,15 @@ class EcheancierController extends AbstractController
$slug = $docuSeal->getSubmitterSlug($submitterId); $slug = $docuSeal->getSubmitterSlug($submitterId);
$signUrl = null !== $slug ? rtrim($docuSealUrl, '/').'/s/'.$slug : null; $signUrl = null !== $slug ? rtrim($docuSealUrl, '/').'/s/'.$slug : null;
$processUrl = $urlGenerator->generate('app_echeancier_process', [
'id' => $echeancier->getId(),
], UrlGeneratorInterface::ABSOLUTE_URL);
$html = $twig->render('emails/echeancier_signature.html.twig', [ $html = $twig->render('emails/echeancier_signature.html.twig', [
'customer' => $customer, 'customer' => $customer,
'echeancier' => $echeancier, 'echeancier' => $echeancier,
'signUrl' => $signUrl, 'signUrl' => $signUrl,
'processUrl' => $processUrl,
]); ]);
$mailer->sendEmail( $mailer->sendEmail(

View File

@@ -15,6 +15,30 @@ class EcheancierProcessController extends AbstractController
) { ) {
} }
/**
* Page de detail echeancier avant signature (publique).
*/
#[Route('/echeancier/process/{id}', name: 'app_echeancier_process', requirements: ['id' => '\d+'])]
public function process(int $id): Response
{
$echeancier = $this->em->getRepository(Echeancier::class)->find($id);
if (null === $echeancier) {
throw $this->createNotFoundException('Echeancier introuvable.');
}
if (\in_array($echeancier->getState(), [Echeancier::STATE_SIGNED, Echeancier::STATE_ACTIVE, Echeancier::STATE_COMPLETED], true)) {
return $this->render('echeancier/signed.html.twig', [
'echeancier' => $echeancier,
'customer' => $echeancier->getCustomer(),
]);
}
return $this->render('echeancier/process.html.twig', [
'echeancier' => $echeancier,
'customer' => $echeancier->getCustomer(),
]);
}
/** /**
* Callback DocuSeal apres signature du client. * Callback DocuSeal apres signature du client.
*/ */

View File

@@ -84,19 +84,9 @@
Voir PDF Voir PDF
</a> </a>
{% endif %} {% 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 proposition</button>
</form>
{% endif %}
{% if echeancier.state in ['draft', 'send'] and echeancier.pdfUnsigned %} {% 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 ?"> <form method="post" action="{{ path('app_admin_echeancier_send_signature', {id: echeancier.id}) }}" data-confirm="{{ echeancier.state == 'send' ? 'Renvoyer le lien de signature au client ?' : '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> <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">{{ echeancier.state == 'send' ? 'Renvoyer signature' : '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' %}

View File

@@ -0,0 +1,87 @@
{% extends 'base.html.twig' %}
{% block title %}Echeancier de paiement - 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-2xl overflow-hidden">
<div class="glass-dark text-white px-8 py-6">
<div class="flex items-center gap-3">
<img src="/logo.jpg" alt="E-Cosplay" class="h-10 w-auto">
<div>
<h1 class="text-lg font-bold uppercase tracking-widest">Echeancier de paiement</h1>
<p class="text-xs text-white/60">Association E-Cosplay</p>
</div>
</div>
</div>
<div class="p-8">
<p class="text-sm text-gray-600 mb-6">
{% if customer.raisonSociale %}Chez {{ customer.raisonSociale }}{% else %}Bonjour {{ customer.firstName }}{% endif %},
veuillez trouver ci-dessous le detail de votre echeancier de paiement.
</p>
{# Motif #}
<div class="glass p-4 mb-6">
<h2 class="text-[9px] font-bold uppercase tracking-wider text-gray-400 mb-1">Motif</h2>
<p class="text-sm font-bold">{{ echeancier.description }}</p>
</div>
{# Resume #}
<div class="grid grid-cols-3 gap-3 mb-6">
<div class="glass p-3 text-center">
<p class="text-[9px] font-bold uppercase tracking-wider text-gray-400">Total HT</p>
<p class="text-lg font-bold mt-1">{{ echeancier.totalAmountHt|number_format(2, ',', ' ') }} &euro;</p>
</div>
<div class="glass p-3 text-center">
<p class="text-[9px] font-bold uppercase tracking-wider text-gray-400">Echeances</p>
<p class="text-lg font-bold mt-1">{{ echeancier.nbLines }} mois</p>
</div>
<div class="glass p-3 text-center">
<p class="text-[9px] font-bold uppercase tracking-wider text-gray-400">Mensualite</p>
<p class="text-lg font-bold mt-1" style="color: #fabf04;">{{ echeancier.monthlyAmount|number_format(2, ',', ' ') }} &euro;</p>
</div>
</div>
{# Tableau echeances #}
<div class="glass overflow-hidden mb-6">
<table class="w-full text-sm">
<thead>
<tr class="glass-dark text-white">
<th class="px-4 py-2 text-left font-bold uppercase text-[10px] tracking-widest">N</th>
<th class="px-4 py-2 text-left font-bold uppercase text-[10px] tracking-widest">Date prevue</th>
<th class="px-4 py-2 text-right font-bold uppercase text-[10px] tracking-widest">Montant</th>
</tr>
</thead>
<tbody>
{% for line in echeancier.lines %}
<tr class="border-b border-white/20 {{ loop.index is odd ? 'bg-white/30' : '' }}">
<td class="px-4 py-2 font-bold">{{ line.position }}</td>
<td class="px-4 py-2 text-xs">{{ line.scheduledAt|date('d/m/Y') }}</td>
<td class="px-4 py-2 text-right font-bold">{{ line.amount|number_format(2, ',', ' ') }} &euro;</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# Conditions #}
<div class="text-xs text-gray-500 mb-6 space-y-1">
<p class="font-bold uppercase text-[9px] tracking-wider text-gray-400 mb-2">Conditions</p>
<p>1. Le prelevement sera effectue automatiquement a chaque date prevue via Stripe.</p>
<p>2. En cas d'echec de prelevement, une relance sera envoyee par email.</p>
<p>3. Apres 2 echecs consecutifs, l'echeancier sera considere en defaut.</p>
<p>4. Majoration de 5% du montant total conformement aux CGV (article 11).</p>
</div>
<p class="text-sm text-gray-600 mb-4">
En signant ce document, vous autorisez le prelevement automatique mensuel du montant indique.
</p>
<p class="text-center text-xs text-gray-400 mt-6">
Pour toute question : <a href="mailto:contact@e-cosplay.fr" class="font-bold" style="color: #fabf04;">contact@e-cosplay.fr</a>
</p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -26,8 +26,18 @@
</tr> </tr>
</table> </table>
{% if processUrl is defined and processUrl %}
<table cellpadding="0" cellspacing="0" style="margin: 16px auto;">
<tr>
<td style="background-color: #111827; padding: 12px 28px;">
<a href="{{ processUrl }}" style="font-family: Arial, Helvetica, sans-serif; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #ffffff; text-decoration: none;">Voir les details</a>
</td>
</tr>
</table>
{% endif %}
{% if signUrl %} {% if signUrl %}
<table cellpadding="0" cellspacing="0" style="margin: 24px auto;"> <table cellpadding="0" cellspacing="0" style="margin: 16px auto;">
<tr> <tr>
<td style="background-color: #fabf04; padding: 14px 32px;"> <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> <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>