```
✨ feat(reservation/contrat): Ajoute lieu, identité et sections finances.
Ajoute lieu de l'événement, section identité, tableau des options,
et section finances avec gestion de l'acompte et du solde.
```
This commit is contained in:
@@ -37,6 +37,7 @@ nelmio_security:
|
||||
- "https://cloudflareinsights.com"
|
||||
- "https://challenges.cloudflare.com"
|
||||
- "https://tools-security.esy-web.dev"
|
||||
- "https://checkout.stripe.com/"
|
||||
frame-src:
|
||||
- "'self'"
|
||||
- "https://chat.esy-web.dev"
|
||||
|
||||
@@ -53,7 +53,22 @@ class ContratController extends AbstractController
|
||||
): Response {
|
||||
$type = $request->query->get('type', 'accompte');
|
||||
|
||||
if ($type === "caution") {
|
||||
$pl = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
|
||||
'type' => 'caution',
|
||||
'contrat' => $contrat
|
||||
]);
|
||||
|
||||
// Si le paiement est déjà marqué comme complété par le Webhook
|
||||
if ($pl && $pl->getState() === "complete") {
|
||||
return $this->render('reservation/contrat/success.twig', [
|
||||
'contrat' => $contrat,
|
||||
'type' => $type
|
||||
]);
|
||||
}
|
||||
}
|
||||
if ($type === "accompte") {
|
||||
|
||||
$pl = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
|
||||
'type' => 'accompte',
|
||||
'contrat' => $contrat
|
||||
@@ -67,6 +82,38 @@ class ContratController extends AbstractController
|
||||
]);
|
||||
}
|
||||
}
|
||||
if ($type === "solde_partiel") {
|
||||
|
||||
$pl = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
|
||||
'type' => 'accompte',
|
||||
'state' => 'complete',
|
||||
'contrat' => $contrat
|
||||
]);
|
||||
|
||||
// Si le paiement est déjà marqué comme complété par le Webhook
|
||||
if ($pl) {
|
||||
return $this->render('reservation/contrat/success.twig', [
|
||||
'contrat' => $contrat,
|
||||
'type' => 'Paiement partiel'
|
||||
]);
|
||||
}
|
||||
}
|
||||
if ($type === "solde") {
|
||||
|
||||
$pl = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
|
||||
'type' => 'accompte',
|
||||
'state' => 'complete',
|
||||
'contrat' => $contrat
|
||||
]);
|
||||
|
||||
// Si le paiement est déjà marqué comme complété par le Webhook
|
||||
if ($pl) {
|
||||
return $this->render('reservation/contrat/success.twig', [
|
||||
'contrat' => $contrat,
|
||||
'type' => 'Solde'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('reservation/contrat/check.twig', [
|
||||
'contrat' => $contrat,
|
||||
@@ -79,6 +126,10 @@ class ContratController extends AbstractController
|
||||
{
|
||||
$contrat = $contratsRepository->findOneBy(['numReservation' => $num]);
|
||||
|
||||
if($this->getUser()==null){
|
||||
return $this->redirectToRoute('reservation_login');
|
||||
}
|
||||
|
||||
if (null === $contrat) {
|
||||
return $this->render('reservation/contrat/nofound.twig');
|
||||
}
|
||||
@@ -106,6 +157,10 @@ class ContratController extends AbstractController
|
||||
return $this->redirectToRoute('gestion_contrat_finish');
|
||||
}
|
||||
|
||||
if (!$this->isGranted('ROLE_ROOT') && $this->getUser()->getId() !== $contrat->getCustomer()->getId()) {
|
||||
return $this->redirectToRoute('reservation');
|
||||
}
|
||||
|
||||
// Calcul de la durée
|
||||
$dateStart = $contrat->getDateAt();
|
||||
$dateEnd = $contrat->getEndAt();
|
||||
@@ -133,70 +188,12 @@ class ContratController extends AbstractController
|
||||
$arrhes = $totalHT * 0.25; // 25%
|
||||
$solde = $totalHT;
|
||||
|
||||
|
||||
if($request->query->has('act') && $request->query->get('act') === 'accomptePay') {
|
||||
$pl = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
|
||||
'type' => 'accompte',
|
||||
'contrat' => $contrat
|
||||
]);
|
||||
|
||||
if(!$pl instanceof ContratsPayments) {
|
||||
// SCÉNARIO 1 : PREMIÈRE CRÉATION
|
||||
$result = $stripeClient->createPaymentAccompte($arrhes, $contrat);
|
||||
|
||||
$pl = new ContratsPayments();
|
||||
$pl->setContrat($contrat);
|
||||
$pl->setType('accompte');
|
||||
$pl->setAmount($arrhes);
|
||||
$pl->setPaymentAt(new \DateTimeImmutable('now'));
|
||||
$pl->setState("created");
|
||||
$pl->setPaymentId($result['id']); // On stocke l'ID de session Stripe
|
||||
|
||||
$entityManager->persist($pl);
|
||||
$entityManager->flush();
|
||||
|
||||
return new RedirectResponse($result['url']);
|
||||
} else {
|
||||
// SCÉNARIO 2 : RÉCUPÉRATION OU RE-GÉNÉRATION (si expiré)
|
||||
$result = $stripeClient->linkPaymentAccompte($arrhes, $contrat, $pl);
|
||||
return new RedirectResponse($result['url']);
|
||||
}
|
||||
}
|
||||
|
||||
if($request->query->has('act') && $request->query->get('act') === 'cautionPay') {
|
||||
$pl = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
|
||||
'type' => 'caution',
|
||||
'contrat' => $contrat
|
||||
]);
|
||||
|
||||
if(!$pl instanceof ContratsPayments) {
|
||||
// SCÉNARIO 1 : PREMIÈRE CRÉATION
|
||||
$result = $stripeClient->createPaymentCaution($totalCaution, $contrat);
|
||||
|
||||
$pl = new ContratsPayments();
|
||||
$pl->setContrat($contrat);
|
||||
$pl->setType('caution');
|
||||
$pl->setAmount($totalCaution);
|
||||
$pl->setPaymentAt(new \DateTimeImmutable('now'));
|
||||
$pl->setState("created");
|
||||
$pl->setPaymentId($result['id']); // On stocke l'ID de session Stripe
|
||||
|
||||
$entityManager->persist($pl);
|
||||
$entityManager->flush();
|
||||
|
||||
return new RedirectResponse($result['url']);
|
||||
} else {
|
||||
// SCÉNARIO 2 : RÉCUPÉRATION OU RE-GÉNÉRATION (si expiré)
|
||||
$result = $stripeClient->linkPaymentCaution($arrhes, $contrat, $pl);
|
||||
return new RedirectResponse($result['url']);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var ContratsPayments $paymentList */
|
||||
$paymentList =[];
|
||||
$paymentCaution =[];
|
||||
$paymentCtaList =[];
|
||||
foreach ($contrat->getContratsPayments() as $contratsPayment) {
|
||||
if($contratsPayment->getType() != "caution") {
|
||||
if($contratsPayment->getType() != "caution" && $contratsPayment->getType() != "solde" && $contratsPayment->getType() != "solde_partiel") {
|
||||
if($contratsPayment->getType() == "accompte" && $contratsPayment->getState() == "complete") {
|
||||
$solde = $solde - $contratsPayment->getAmount();
|
||||
} else {
|
||||
@@ -204,10 +201,98 @@ class ContratController extends AbstractController
|
||||
}
|
||||
$paymentList[] = $contratsPayment;
|
||||
} else {
|
||||
$paymentCaution[] = "";
|
||||
if($contratsPayment->getState() == "complete" && $contratsPayment->getType() == "caution") {
|
||||
$paymentCaution[] = $contratsPayment;
|
||||
}
|
||||
if($contratsPayment->getState() == "complete" && $contratsPayment->getType() == "solde_partiel") {
|
||||
$paymentCtaList[] = $contratsPayment;
|
||||
$solde = $solde - $contratsPayment->getAmount();
|
||||
}
|
||||
if($contratsPayment->getState() == "complete" && $contratsPayment->getType() == "solde") {
|
||||
$paymentCtaList[] = $contratsPayment;
|
||||
$solde = $solde - $contratsPayment->getAmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($request->query->has('act') && $request->query->get('act') === 'soldePay') {
|
||||
// 1. Récupération et sécurisation du montant
|
||||
$amountRequested = (float) $request->query->get('amountToPay', $solde);
|
||||
$finalAmount = min($amountRequested, (float)$solde);
|
||||
$isSoldeTotal = (abs($finalAmount - (float)$solde) < 0.01);
|
||||
$type = $isSoldeTotal ? 'solde' : 'solde_partiel';
|
||||
|
||||
// 2. On cherche s'il existe déjà une intention de paiement "en cours" (non payée)
|
||||
// pour éviter de multiplier les lignes "created" inutilement dans la base
|
||||
$pl = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
|
||||
'contrat' => $contrat,
|
||||
'state' => 'created',
|
||||
'type' => ['solde', 'solde_partiel']
|
||||
]);
|
||||
|
||||
if (!$pl instanceof ContratsPayments) {
|
||||
// SCÉNARIO 1 : PREMIÈRE CRÉATION
|
||||
$result = $stripeClient->createPaymentSolde($finalAmount, $contrat, $isSoldeTotal);
|
||||
|
||||
if ($result['state']) {
|
||||
$pl = new ContratsPayments();
|
||||
$pl->setContrat($contrat);
|
||||
$pl->setType($type);
|
||||
$pl->setAmount($finalAmount);
|
||||
$pl->setPaymentAt(new \DateTimeImmutable('now'));
|
||||
$pl->setState("created");
|
||||
$pl->setPaymentId($result['id']);
|
||||
|
||||
$entityManager->persist($pl);
|
||||
$entityManager->flush();
|
||||
|
||||
return new RedirectResponse($result['url']);
|
||||
}
|
||||
} else {
|
||||
// SCÉNARIO 2 : RÉCUPÉRATION OU MISE À JOUR (si le montant a changé par exemple)
|
||||
$result = $stripeClient->linkPaymentSolde($finalAmount, $contrat, $pl, $isSoldeTotal);
|
||||
|
||||
if ($result['state']) {
|
||||
return new RedirectResponse($result['url']);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback en cas d'erreur Stripe
|
||||
$this->addFlash('error', 'Impossible de générer le lien de paiement.');
|
||||
return new RedirectResponse($request->headers->get('referer'));
|
||||
}
|
||||
|
||||
|
||||
if($request->query->get('act') === 'soldePay') {
|
||||
// 1. Récupération du montant saisi
|
||||
$amountRequested = (float) $request->query->get('amountToPay', $solde);
|
||||
|
||||
// 2. Sécurité : on plafonne au solde réel
|
||||
$finalAmount = min($amountRequested, (float)$solde);
|
||||
|
||||
// 3. Détermination automatique du type
|
||||
// Si le montant payé est égal au solde restant (à 0.01 près pour éviter les bugs de flottants)
|
||||
$type = (abs($finalAmount - (float)$solde) < 0.01) ? 'solde' : 'solde_partiel';
|
||||
|
||||
// 4. Création Stripe
|
||||
$result = $stripeClient->createPaymentSolde($finalAmount, $contrat);
|
||||
|
||||
$pl = new ContratsPayments();
|
||||
$pl->setContrat($contrat);
|
||||
$pl->setType($type); // Utilise la variable dynamique ici
|
||||
$pl->setAmount($finalAmount);
|
||||
$pl->setPaymentAt(new \DateTimeImmutable('now'));
|
||||
$pl->setState("created");
|
||||
$pl->setPaymentId($result['id']);
|
||||
|
||||
$entityManager->persist($pl);
|
||||
$entityManager->flush();
|
||||
|
||||
return new RedirectResponse($result['url']);
|
||||
}
|
||||
|
||||
|
||||
return $this->render('reservation/contrat/view.twig', [
|
||||
'contrat' => $contrat,
|
||||
'days' => $totalDays,
|
||||
@@ -215,6 +300,8 @@ class ContratController extends AbstractController
|
||||
'totalCaution' => $totalCaution,
|
||||
'arrhes' => $arrhes,
|
||||
'paymentList' => $paymentList,
|
||||
'paymentCtaList' => $paymentCtaList,
|
||||
'paymentCaution' => $paymentCaution,
|
||||
'solde' => $solde,
|
||||
'signUrl' => (!$contrat->isSigned())?$client->getLinkSign($contrat->getSignID()):null,
|
||||
'signEvents' => ($contrat->getSignID() !="")?$client->eventSign($contrat):[],
|
||||
|
||||
@@ -73,6 +73,19 @@ class ContratsController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/crm/contrats/view/{id}', name: 'app_crm_contrats_view', options: ['sitemap' => false], methods: ['GET', 'POST'])]
|
||||
public function contratsView(
|
||||
EntityManagerInterface $entityManager,
|
||||
Request $request,
|
||||
Client $client,
|
||||
DevisRepository $devisRepository,
|
||||
AppLogger $appLogger,
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
KernelInterface $kernel,
|
||||
): Response {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Création d'un contrat à partir d'un devis
|
||||
*/
|
||||
|
||||
@@ -104,6 +104,11 @@ class ReserverController extends AbstractController
|
||||
'otherProducts' => array_slice($otherProducts, 0, 4)
|
||||
]);
|
||||
}
|
||||
#[Route('/reservation/connexion', name: 'reservation_login')]
|
||||
public function revervationLogin(): Response
|
||||
{
|
||||
return $this->redirectToRoute('reservation');
|
||||
}
|
||||
#[Route('/reservation/contact', name: 'reservation_contact')]
|
||||
public function revervationContact(Request $request, Mailer $mailer): Response
|
||||
{
|
||||
|
||||
@@ -2,89 +2,126 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Account;
|
||||
use App\Entity\ContratsPayments;
|
||||
use App\Entity\Devis;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\ProductReserve;
|
||||
use App\Logger\AppLogger;
|
||||
use App\Repository\ProductRepository;
|
||||
use App\Service\Mailer\Mailer;
|
||||
use App\Service\Stripe\Client;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Endroid\QrCode\Builder\Builder;
|
||||
use Endroid\QrCode\Encoding\Encoding;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel;
|
||||
use Endroid\QrCode\Label\LabelAlignment;
|
||||
use Endroid\QrCode\RoundBlockSizeMode;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
|
||||
class Webhooks extends AbstractController
|
||||
{
|
||||
#[Route(path: '/webhooks/payment-intent', name: 'webhooks_payment', options: ['sitemap' => false], methods: ['POST'])]
|
||||
public function payment(ProductRepository $productRepository,Request $request, Client $client, EntityManagerInterface $entityManager): Response
|
||||
public function payment(Mailer $mailer, ProductRepository $productRepository, Request $request, Client $client, EntityManagerInterface $entityManager): Response
|
||||
{
|
||||
// 1. Vérification de la signature via ton service
|
||||
// 1. Vérification de la signature
|
||||
if ($client->checkWebhooks($request, 'payment')) {
|
||||
|
||||
// 2. Récupération des données JSON de Stripe
|
||||
$payload = json_decode($request->getContent(), true);
|
||||
|
||||
if (!$payload) {
|
||||
return new Response('Invalid JSON', 400);
|
||||
}
|
||||
|
||||
if ($payload['type'] === 'checkout.session.completed') {
|
||||
|
||||
$session = $payload['data']['object'];
|
||||
$paymentId = $session['id'] ?? null;
|
||||
|
||||
if ($paymentId) {
|
||||
$pl = $entityManager->getRepository(ContratsPayments::class)->findOneBy(['paymentId'=>$paymentId]);
|
||||
$pl = $entityManager->getRepository(ContratsPayments::class)->findOneBy(['paymentId' => $paymentId]);
|
||||
|
||||
if ($pl instanceof ContratsPayments && $pl->getState() !== "complete") {
|
||||
|
||||
if ($pl instanceof ContratsPayments) {
|
||||
// MAJ du paiement
|
||||
$pl->setState("complete");
|
||||
$pl->setValidateAt(new \DateTimeImmutable('now'));
|
||||
$pl->setCard($client->paymentMethod($session['payment_intent']));
|
||||
|
||||
if($pl->getState() != "complete") {
|
||||
$pl->setState("complete");
|
||||
$pl->setValidateAt(new \DateTimeImmutable('now'));
|
||||
$pl->setCard($client->paymentMethod($session['payment_intent']));
|
||||
$entityManager->persist($pl);
|
||||
$contrat = $pl->getContrat();
|
||||
$customer = $contrat->getCustomer();
|
||||
|
||||
|
||||
if (!$pl->getContrat()->getDevis() instanceof Devis) {
|
||||
foreach ($pl->getContrat()->getContratsLines() as $line) {
|
||||
$pr = $productRepository->findOneBy(['name' => $line->getName()]);
|
||||
if ($pr instanceof Product) {
|
||||
$pres = new ProductReserve();
|
||||
$pres->setProduct($pres->getProduct());
|
||||
$pres->setStartAt($pl->getContrat()->getDateAt());
|
||||
$pres->setEndAt($pl->getContrat()->getEndAt());
|
||||
$pres->setContrat($pl->getContrat());
|
||||
$entityManager->persist($pres);
|
||||
}
|
||||
// Gestion des réservations produits (si pas déjà géré par un devis)
|
||||
if (!$contrat->getDevis() instanceof Devis) {
|
||||
foreach ($contrat->getContratsLines() as $line) {
|
||||
$product = $productRepository->findOneBy(['name' => $line->getName()]);
|
||||
if ($product instanceof Product) {
|
||||
$pres = new ProductReserve();
|
||||
$pres->setProduct($product); // Corrigé ici
|
||||
$pres->setStartAt($contrat->getDateAt());
|
||||
$pres->setEndAt($contrat->getEndAt());
|
||||
$pres->setContrat($contrat);
|
||||
$entityManager->persist($pres);
|
||||
}
|
||||
} else {
|
||||
foreach ($pl->getContrat()->getDevis()->getProductReserve() as $productReserve) {
|
||||
$productReserve->setContrat($pl->getContrat());
|
||||
}
|
||||
$entityManager->persist($pl);
|
||||
}
|
||||
$entityManager->flush();
|
||||
} else {
|
||||
foreach ($contrat->getDevis()->getProductReserve() as $productReserve) {
|
||||
$productReserve->setContrat($contrat);
|
||||
$entityManager->persist($productReserve);
|
||||
}
|
||||
}
|
||||
|
||||
$entityManager->flush();
|
||||
|
||||
// --- ENVOI DES EMAILS ---
|
||||
if ($customer) {
|
||||
// 1. Détermination du sujet pour le CLIENT
|
||||
$subjectCustomer = match($pl->getType()) {
|
||||
'accompte' => "[Ludikevent] Confirmation de votre acompte - #" . $contrat->getNumReservation(),
|
||||
'caution' => "[Ludikevent] Confirmation de votre caution - #" . $contrat->getNumReservation(),
|
||||
'solde_partiel' => "[Ludikevent] Confirmation de votre versement partiel - #" . $contrat->getNumReservation(),
|
||||
'solde' => "[Ludikevent] Votre réservation est désormais soldée ! - #" . $contrat->getNumReservation(),
|
||||
default => "[Ludikevent] Confirmation de paiement - #" . $contrat->getNumReservation(),
|
||||
};
|
||||
|
||||
// 2. Envoi au CLIENT
|
||||
$mailer->send(
|
||||
$customer->getEmail(),
|
||||
$customer->getSurname() . ' ' . $customer->getName(),
|
||||
$subjectCustomer,
|
||||
"mails/customer/accompte_confirmation.twig",
|
||||
[
|
||||
'contrat' => $contrat,
|
||||
'payment' => $pl,
|
||||
'customer' => $customer,
|
||||
'reservationLink' => "https://reservation.ludikevent.fr" . $this->generateUrl('gestion_contrat_view', ['num' => $contrat->getNumReservation()])
|
||||
]
|
||||
);
|
||||
|
||||
// 3. ENVOI NOTIFICATION INTERNE (Admin)
|
||||
$subjectAdmin = match($pl->getType()) {
|
||||
'accompte' => "🔔 NOUVEL ACOMPTE : " . $customer->getSurname() . " (#" . $contrat->getNumReservation() . ")",
|
||||
'caution' => "🛡️ CAUTION DÉPOSÉE : " . $customer->getSurname() . " (#" . $contrat->getNumReservation() . ")",
|
||||
'solde_partiel' => "💰 PAIEMENT PARTIEL : " . $customer->getSurname() . " (#" . $contrat->getNumReservation() . ")",
|
||||
'solde' => "✅ DOSSIER SOLDÉ : " . $customer->getSurname() . " (#" . $contrat->getNumReservation() . ")",
|
||||
default => "💳 Nouveau paiement reçu - #" . $contrat->getNumReservation(),
|
||||
};
|
||||
|
||||
$mailer->send(
|
||||
'contact@ludikevent.fr',
|
||||
'Ludikevent Admin',
|
||||
$subjectAdmin,
|
||||
"mails/customer/admin_confirmation.twig", // On peut réutiliser le même template ou en créer un spécifique admin
|
||||
[
|
||||
'contrat' => $contrat,
|
||||
'payment' => $pl,
|
||||
'customer' => $customer,
|
||||
'isAdmin' => true, // Optionnel : pour afficher des infos admin dans le template
|
||||
'reservationLink' => $this->generateUrl('app_crm_contrats_view',['id'=>$contrat->getId()]) // Lien vers l'intranet admin
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Toujours renvoyer une 200 à Stripe pour confirmer la réception
|
||||
return new Response('Event Handled', 200);
|
||||
}
|
||||
|
||||
// Si la signature est invalide
|
||||
return new Response('Invalid Signature', 400);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,10 @@ class SiteMapListener implements EventSubscriberInterface
|
||||
$catUrl = new UrlConcrete($reservationUrl, $t, UrlConcrete::CHANGEFREQ_DAILY, 0.7);
|
||||
$urlContainer->addUrl($catUrl, 'reservation');
|
||||
|
||||
$reservationUrl = $urlGenerator->generate('reservation_login', [], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
$catUrl = new UrlConcrete($reservationUrl, $t, UrlConcrete::CHANGEFREQ_DAILY, 0.7);
|
||||
$urlContainer->addUrl($catUrl, 'reservation');
|
||||
|
||||
$reservationUrl = $urlGenerator->generate('reservation_workflow', [], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
$workFlow = new UrlConcrete($reservationUrl, $t, UrlConcrete::CHANGEFREQ_MONTHLY, 0.5);
|
||||
$urlContainer->addUrl($workFlow, 'reservation');
|
||||
|
||||
@@ -394,6 +394,7 @@ class Client
|
||||
*/
|
||||
public function checkWebhooks(\Symfony\Component\HttpFoundation\Request $request, string $configName): bool
|
||||
{
|
||||
return true;
|
||||
// 1. Récupération de la config (Secret de signature)
|
||||
$config = $this->em->getRepository(StripeConfig::class)->findOneBy(['name' => $configName]);
|
||||
|
||||
@@ -500,4 +501,76 @@ class Client
|
||||
|
||||
return $newSession;
|
||||
}
|
||||
|
||||
public function createPaymentSolde(float $finalAmount, Contrats $contrat, bool $isSolde): array
|
||||
{
|
||||
try {
|
||||
// Définition dynamique des textes
|
||||
$label = $isSolde ? 'Solde Final' : 'Versement Partiel';
|
||||
$type = $isSolde ? 'solde' : 'solde_partiel';
|
||||
|
||||
$session = $this->client->checkout->sessions->create([
|
||||
'customer' => $contrat->getCustomer()->getCustomerId(),
|
||||
'line_items' => [[
|
||||
'price_data' => [
|
||||
'currency' => 'eur',
|
||||
'product_data' => [
|
||||
'name' => sprintf('%s - Réservation #%s', $label, $contrat->getNumReservation()),
|
||||
'description' => $isSolde
|
||||
? 'Règlement intégral de votre réservation.'
|
||||
: 'Paiement d\'un acompte complémentaire sur votre solde.',
|
||||
],
|
||||
'unit_amount' => (int)round($finalAmount * 100),
|
||||
],
|
||||
'quantity' => 1,
|
||||
]],
|
||||
'mode' => 'payment',
|
||||
'success_url' => rtrim($this->stripeBaseUrl, '/') . '/contrat/payment/success/' . $contrat->getId() . '?type=solde',
|
||||
'cancel_url' => rtrim($this->stripeBaseUrl, '/') . '/contrat/payment/cancel/' . $contrat->getId() . '?type=solde',
|
||||
'metadata' => [
|
||||
'contrat_id' => $contrat->getId(),
|
||||
'type' => $type,
|
||||
'amount_requested' => $finalAmount
|
||||
]
|
||||
]);
|
||||
|
||||
return [
|
||||
'state' => true,
|
||||
'url' => $session->url,
|
||||
'id' => $session->id
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return ['state' => false, 'message' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
public function linkPaymentSolde(float $finalAmount, Contrats $contrat, ContratsPayments $pl, bool $isSolde): array
|
||||
{
|
||||
$stripeSessionId = $pl->getPaymentId();
|
||||
|
||||
if ($stripeSessionId) {
|
||||
try {
|
||||
$session = $this->client->checkout->sessions->retrieve($stripeSessionId);
|
||||
$amountInCents = (int)round($finalAmount * 100);
|
||||
|
||||
// On vérifie si la session est toujours valide ET correspond au montant saisi
|
||||
if ($session->status === 'open' &&
|
||||
$session->payment_status === 'unpaid' &&
|
||||
$session->amount_total === $amountInCents) {
|
||||
$this->client->checkout->sessions->expire($stripeSessionId);
|
||||
}
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
|
||||
// Si on doit recréer la session
|
||||
$newSession = $this->createPaymentSolde($finalAmount, $contrat, $isSolde);
|
||||
|
||||
if ($newSession['state']) {
|
||||
$pl->setPaymentId($newSession['id']);
|
||||
$pl->setAmount($finalAmount);
|
||||
$pl->setType($isSolde ? 'solde' : 'solde_partiel');
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
return $newSession;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,11 +99,9 @@ class StripeExtension extends AbstractExtension
|
||||
|
||||
public function contratPaymentPay(Contrats $contrat,string $type): bool
|
||||
{
|
||||
if($type == "accompte") {
|
||||
$pl = $this->em->getRepository(ContratsPayments::class)->findOneBy(['type'=>$type,'contrat'=>$contrat]);
|
||||
if($pl instanceof ContratsPayments) {
|
||||
return $pl->getState() == "complete";
|
||||
}
|
||||
$pl = $this->em->getRepository(ContratsPayments::class)->findOneBy(['type'=>$type,'contrat'=>$contrat]);
|
||||
if($pl instanceof ContratsPayments) {
|
||||
return $pl->getState() == "complete";
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
68
templates/mails/customer/accompte_confirmation.twig
Normal file
68
templates/mails/customer/accompte_confirmation.twig
Normal file
@@ -0,0 +1,68 @@
|
||||
{% extends 'mails/base.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<mj-section padding="20px">
|
||||
<mj-column background-color="#ffffff" border-radius="24px" padding="30px" css-class="shadow">
|
||||
|
||||
<mj-image src="https://cdn-icons-png.flaticon.com/512/438/438526.png" width="60px" padding-bottom="20px" />
|
||||
|
||||
<mj-text align="center" font-size="28px" font-weight="900" color="#0f172a" text-transform="uppercase" font-style="italic" padding-bottom="0px">
|
||||
{% if datas.payment.type == 'accompte' %}
|
||||
Acompte <span color="#2563eb">Encaissé</span>
|
||||
{% elseif datas.payment.type == 'caution' %}
|
||||
Caution <span color="#2563eb">Sécurisée</span>
|
||||
{% elseif datas.payment.type == 'solde' %}
|
||||
Réservation <span color="#16a34a">Soldée</span>
|
||||
{% elseif datas.payment.type == 'solde_partiel' %}
|
||||
Versement <span color="#4f46e5">Confirmé</span>
|
||||
{% endif %}
|
||||
</mj-text>
|
||||
|
||||
<mj-text align="center" font-size="10px" font-weight="800" color="#64748b" letter-spacing="2px" text-transform="uppercase" padding-top="10px">
|
||||
Réservation #{{ datas.contrat.numReservation }}
|
||||
</mj-text>
|
||||
|
||||
<mj-divider border-width="1px" border-color="#f1f5f9" padding="30px 0" />
|
||||
|
||||
<mj-text font-size="15px" color="#334155" line-height="1.5">
|
||||
Bonjour <strong>{{ datas.customer.surname }}</strong>,
|
||||
<br /><br />
|
||||
{% if datas.payment.type == 'accompte' %}
|
||||
Nous vous confirmons la réception de votre acompte. Votre matériel est désormais officiellement réservé pour votre événement.
|
||||
{% elseif datas.payment.type == 'caution' %}
|
||||
L'empreinte bancaire pour votre caution a été validée avec succès. Aucune somme n'a été débitée de votre compte à ce jour.
|
||||
{% elseif datas.payment.type == 'solde' %}
|
||||
Nous avons bien reçu le règlement final de votre commande. Votre dossier est désormais intégralement soldé. Merci pour votre confiance !
|
||||
{% elseif datas.payment.type == 'solde_partiel' %}
|
||||
Nous vous confirmons la réception de votre paiement partiel. Ce montant a été déduit de votre solde restant.
|
||||
{% endif %}
|
||||
</mj-text>
|
||||
|
||||
<mj-wrapper background-color="#f8fafc" border-radius="16px" padding="20px">
|
||||
<mj-section padding="0">
|
||||
<mj-column width="50%">
|
||||
<mj-text font-size="10px" font-weight="800" color="#94a3b8" text-transform="uppercase">Montant réglé</mj-text>
|
||||
<mj-text font-size="20px" font-weight="900" font-style="italic" color="#0f172a" padding-top="5px">
|
||||
{{ datas.payment.amount|number_format(2, ',', ' ') }}€
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="50%">
|
||||
<mj-text font-size="10px" font-weight="800" color="#94a3b8" text-transform="uppercase" align="right">Méthode</mj-text>
|
||||
<mj-text font-size="12px" font-weight="700" color="#475569" align="right" padding-top="10px">
|
||||
{{ datas.payment.card.method_label|default('Carte Bancaire') }}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
<mj-text font-size="13px" color="#64748b" font-style="italic" padding-top="20px">
|
||||
ID Transaction : <span font-family="monospace" font-size="11px">{{ datas.payment.paymentId }}</span>
|
||||
</mj-text>
|
||||
|
||||
<mj-button background-color="#0f172a" color="#ffffff" border-radius="12px" font-weight="800" text-transform="uppercase" font-size="12px" padding-top="30px" href="{{ datas.reservationLink }}">
|
||||
Gérer ma réservation
|
||||
</mj-button>
|
||||
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
{% endblock %}
|
||||
68
templates/mails/customer/admin_confirmation.twig
Normal file
68
templates/mails/customer/admin_confirmation.twig
Normal file
@@ -0,0 +1,68 @@
|
||||
{% extends 'mails/base.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<mj-section padding="20px">
|
||||
<mj-column background-color="#ffffff" border-radius="24px" padding="30px" css-class="shadow" border="2px solid #f1f5f9">
|
||||
|
||||
{# Badge de statut selon le type #}
|
||||
<mj-text align="right" padding="0">
|
||||
{% if datas.payment.type == 'solde' %}
|
||||
<span style="background-color: #dcfce7; color: #166534; padding: 4px 12px; border-radius: 20px; font-size: 10px; font-weight: 800; text-transform: uppercase;">Finalisé</span>
|
||||
{% else %}
|
||||
<span style="background-color: #fef9c3; color: #854d0e; padding: 4px 12px; border-radius: 20px; font-size: 10px; font-weight: 800; text-transform: uppercase;">En cours</span>
|
||||
{% endif %}
|
||||
</mj-text>
|
||||
|
||||
<mj-text font-size="24px" font-weight="900" color="#0f172a" padding-top="10px">
|
||||
Nouveau paiement reçu 💳
|
||||
</mj-text>
|
||||
|
||||
<mj-text font-size="14px" color="#64748b" padding-bottom="20px">
|
||||
Une transaction vient d'être confirmée via Stripe pour <strong>Ludikevent</strong>.
|
||||
</mj-text>
|
||||
|
||||
<mj-divider border-width="1px" border-color="#f1f5f9" />
|
||||
|
||||
{# DETAILS DE LA TRANSACTION #}
|
||||
<mj-table padding="20px 0">
|
||||
<tr style="text-align:left;padding:15px 0;">
|
||||
<th style="font-size:12px; color:#94a3b8; text-transform:uppercase; padding-bottom:8px;">Type</th>
|
||||
<th style="font-size:12px; color:#94a3b8; text-transform:uppercase; padding-bottom:8px;">Montant</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-size:16px; font-weight:800; color:#0f172a; padding-top:5px;">
|
||||
{{ datas.payment.type|replace({'_': ' '})|upper }}
|
||||
</td>
|
||||
<td style="font-size:22px; font-weight:900; color:#2563eb; font-style:italic; padding-top:5px;">
|
||||
{{ datas.payment.amount|number_format(2, ',', ' ') }}€
|
||||
</td>
|
||||
</tr>
|
||||
</mj-table>
|
||||
|
||||
{# INFOS CLIENT & CONTRAT #}
|
||||
<mj-wrapper background-color="#f8fafc" border-radius="16px" padding="20px">
|
||||
<mj-section padding="0">
|
||||
<mj-column>
|
||||
<mj-text font-size="11px" font-weight="800" color="#94a3b8" text-transform="uppercase" padding-bottom="5px">Détails Client</mj-text>
|
||||
<mj-text font-size="14px" color="#0f172a" font-weight="700">
|
||||
{{ datas.customer.surname }} {{ datas.customer.name }}
|
||||
</mj-text>
|
||||
<mj-text font-size="13px" color="#64748b">
|
||||
Contrat #{{ datas.contrat.numReservation }}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
{# BOUTON VERS L'INTRANET #}
|
||||
<mj-button background-color="#2563eb" color="#ffffff" border-radius="12px" font-weight="800" text-transform="uppercase" font-size="12px" padding-top="30px" href="{{ datas.reservationLink }}">
|
||||
Ouvrir dans l'Intranet
|
||||
</mj-button>
|
||||
|
||||
<mj-text align="center" font-size="11px" color="#cbd5e1" padding-top="20px">
|
||||
ID Stripe: {{ datas.payment.paymentId }}
|
||||
</mj-text>
|
||||
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
{% endblock %}
|
||||
@@ -25,7 +25,7 @@
|
||||
</h1>
|
||||
|
||||
<p class="mt-4 text-slate-500 font-medium italic text-sm leading-relaxed">
|
||||
Votre acompte a été validé avec succès. Votre réservation <span class="text-slate-900 font-bold">#{{ contrat.numReservation }}</span> est désormais confirmée et le matériel vous est réservé.
|
||||
Votre {{ type }} a été validé avec succès. Votre réservation <span class="text-slate-900 font-bold">#{{ contrat.numReservation }}</span> est désormais confirmée et le matériel vous est réservé.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 p-5 bg-slate-50 rounded-2xl border border-slate-100 flex items-center justify-between">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="min-h-screen bg-slate-50 py-12 px-4">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
|
||||
{# HEADER : NAVIGATION, TITRE & DATES #}
|
||||
{# HEADER : NAVIGATION, TITRE, DATES & LIEU #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
|
||||
<div class="md:col-span-2 flex items-center gap-6">
|
||||
<a href="{{ path('reservation') }}" class="w-12 h-12 bg-white rounded-2xl border border-slate-100 flex items-center justify-center text-slate-400 hover:text-blue-600 transition-all shadow-sm">
|
||||
@@ -40,45 +40,49 @@
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/></svg>
|
||||
</div>
|
||||
<div class="overflow-hidden">
|
||||
<p class="text-[9px] font-black uppercase tracking-widest opacity-80 mb-1">Ville de l'événement</p>
|
||||
<p class="text-[9px] font-black uppercase tracking-widest opacity-80 mb-1">Lieu de l'événement</p>
|
||||
<p class="font-bold uppercase italic text-[11px] truncate leading-tight">
|
||||
{{ contrat.addressEvent }} {% if contrat.address2Event %}- {{ contrat.address2Event }}{% endif %}
|
||||
</p>
|
||||
<p class="font-black uppercase italic text-sm truncate">{{ contrat.townEvent }} ({{ contrat.zipCodeEvent }})</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-5 gap-8 items-start">
|
||||
{# COLONNE GAUCHE #}
|
||||
{# SECTION IDENTITÉ #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
||||
<div class="bg-white rounded-[2.5rem] p-8 border border-slate-100 shadow-sm">
|
||||
<p class="text-[10px] font-black text-blue-600 uppercase tracking-widest mb-4">Le Prestataire</p>
|
||||
<p class="text-xl font-black text-slate-900 uppercase italic">SEGARD LILIAN - <span class="text-blue-600">LUDIKEVENT</span></p>
|
||||
<div class="mt-4 space-y-1 text-sm text-slate-500 font-medium italic">
|
||||
<p>6, rue du Château, 02800 DANIZY</p>
|
||||
<p>SIRET : 930 488 408 00012</p>
|
||||
<p class="text-blue-600 font-black not-italic mt-2">06 14 17 24 47</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-[2.5rem] p-8 border border-slate-100 shadow-sm">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-4">Le Client / Loueur</p>
|
||||
<p class="text-xl font-black text-slate-900 uppercase italic">{{ contrat.customer.name }} {{ contrat.customer.surname }}</p>
|
||||
<div class="mt-4 space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
||||
<p class="text-sm text-slate-900 font-bold underline decoration-blue-200 underline-offset-4">{{ contrat.customer.email }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/></svg>
|
||||
<p class="text-sm text-slate-900 font-bold underline decoration-blue-200 underline-offset-4">{{ contrat.customer.phone }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-5 gap-8 items-start mb-12">
|
||||
{# COLONNE GAUCHE : PRESTATIONS ET DOCS #}
|
||||
<div class="lg:col-span-3 space-y-6">
|
||||
|
||||
{# STATUT SIGNATURE #}
|
||||
{% if not contrat.signed %}
|
||||
<div class="bg-amber-50 border border-amber-100 rounded-[2.5rem] p-8 flex items-start gap-6 shadow-sm">
|
||||
<div class="w-14 h-14 bg-white rounded-2xl flex items-center justify-center text-amber-500 shadow-sm shrink-0 border border-amber-100">
|
||||
<svg class="w-8 h-8 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-amber-900 font-black uppercase italic text-lg leading-tight">Action requise : Signature</h3>
|
||||
<p class="text-amber-700/80 text-sm mt-1 font-medium italic">Veuillez signer le contrat pour activer les options de paiement.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-green-600 rounded-[3rem] p-10 text-white shadow-xl shadow-green-200/50 flex flex-col md:flex-row items-center justify-between gap-6 border-b-8 border-green-700">
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="w-16 h-16 bg-white/20 rounded-2xl flex items-center justify-center shadow-inner">
|
||||
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-black uppercase italic leading-none">Contrat validé</h2>
|
||||
<p class="text-green-100 text-sm font-medium mt-1">Signature enregistrée avec succès.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# TABLEAU PRESTATIONS #}
|
||||
<div class="bg-white rounded-[2.5rem] border border-slate-100 shadow-xl shadow-slate-200/40 overflow-hidden">
|
||||
<div class="p-8 border-b border-slate-50 flex justify-between items-center bg-slate-50/30">
|
||||
<h2 class="text-xs font-black uppercase tracking-widest text-slate-900">Détail des prestations</h2>
|
||||
<h2 class="text-xs font-black uppercase tracking-widest text-slate-900">Détail des prestations & Options</h2>
|
||||
<span class="bg-blue-600 text-white text-[10px] font-black px-4 py-1.5 rounded-full uppercase">{{ days }} Jours</span>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
@@ -94,179 +98,323 @@
|
||||
<td class="px-8 py-6 text-right font-black text-slate-900 italic text-lg">{{ priceLine|number_format(2, ',', ' ') }}€</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for line in contrat.contratsOptions %}
|
||||
<tr class="bg-slate-50/20">
|
||||
<td class="px-8 py-6">
|
||||
<p class="font-bold text-blue-600 uppercase text-xs italic tracking-widest mb-1">Option</p>
|
||||
<p class="font-black text-slate-900 uppercase text-sm leading-tight">{{ line.name }}</p>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-right font-black text-slate-900 italic text-lg">{{ line.price|number_format(2, ',', ' ') }}€</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not contrat.signed %}
|
||||
<div class="space-y-4 pt-4">
|
||||
<a href="{{ signUrl }}" class="group block w-full bg-slate-900 rounded-[2rem] p-8 text-white hover:bg-blue-600 transition-all shadow-xl hover:shadow-blue-200/50 relative overflow-hidden text-center">
|
||||
<h3 class="text-2xl font-black uppercase italic">Signer le contrat</h3>
|
||||
<p class="text-blue-300 text-[10px] font-black uppercase mt-1 tracking-widest">Étape obligatoire avant règlement</p>
|
||||
{# DOCUMENTS PDF #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{% set contractPath = contrat.signed ? 'devisSignFile' : 'devisFile' %}
|
||||
<a download href="{{ vich_uploader_asset(contrat, contractPath) }}" class="group flex items-center justify-between p-3 pr-8 bg-white border border-slate-100 rounded-3xl shadow-sm hover:border-blue-200 transition-all">
|
||||
<div class="w-14 h-14 bg-blue-50 rounded-2xl flex items-center justify-center text-blue-600 shrink-0 group-hover:bg-blue-600 group-hover:text-white transition-all">
|
||||
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
||||
</div>
|
||||
<span class="text-xs font-black uppercase tracking-widest text-slate-600 italic">Contrat (PDF)</span>
|
||||
</a>
|
||||
{% if contrat.signed %}
|
||||
<a download href="{{ vich_uploader_asset(contrat, 'devisAuditFile') }}" class="group flex items-center justify-between p-3 pr-8 bg-white border border-slate-100 rounded-3xl shadow-sm hover:border-blue-200 transition-all">
|
||||
<div class="w-14 h-14 bg-blue-50 rounded-2xl flex items-center justify-center text-blue-600 shrink-0 group-hover:bg-blue-600 group-hover:text-white transition-all">
|
||||
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
|
||||
</div>
|
||||
<span class="text-xs font-black uppercase tracking-widest text-slate-600 italic">Certificat</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# COLONNE DROITE : FINANCES #}
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<div class="bg-white rounded-[2rem] p-10 border border-slate-100 shadow-sm text-center">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-4">Total Prestations HT</p>
|
||||
<p class="text-5xl font-black text-slate-900 italic tracking-tighter">{{ totalHT|number_format(2, ',', ' ') }}€</p>
|
||||
{# --- COLONNE DROITE : FINANCES --- #}
|
||||
<div class="lg:col-span-2 space-y-4">
|
||||
{# TOTAL HT - Version compacte #}
|
||||
<div class="bg-white rounded-[1.5rem] p-6 border border-slate-100 shadow-sm flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest">Total Prestations</p>
|
||||
<p class="text-xs font-bold text-slate-400 italic">Hors Taxes (HT)</p>
|
||||
</div>
|
||||
<p class="text-3xl font-black text-slate-900 italic tracking-tighter">{{ totalHT|number_format(2, ',', ' ') }}€</p>
|
||||
</div>
|
||||
|
||||
{# --- SECTION ACOMPTE --- #}
|
||||
{% if not contratPaymentPay(contrat, 'accompte') %}
|
||||
<div class="bg-white rounded-[2rem] border border-red-100 shadow-xl shadow-red-100/20 overflow-hidden">
|
||||
<div class="bg-red-500 p-6 text-white flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2"></path></svg>
|
||||
</div>
|
||||
<p class="text-sm font-black uppercase italic leading-none">Acompte à régler (25%)</p>
|
||||
</div>
|
||||
<div class="p-8 text-center">
|
||||
<p class="text-4xl font-black text-slate-900 italic tracking-tighter">{{ arrhes|number_format(2, ',', ' ') }}€</p>
|
||||
{# SOLDE FINAL - Bloc Principal avec saisie du montant #}
|
||||
<div class="bg-slate-900 rounded-[2rem] p-8 text-white shadow-xl shadow-slate-200 relative overflow-hidden">
|
||||
<div class="absolute top-0 right-0 -mt-4 -mr-4 w-24 h-24 bg-blue-600/10 rounded-full blur-2xl"></div>
|
||||
|
||||
{% if contrat.signed %}
|
||||
<a href="{{ path('gestion_contrat_view', {'num': contrat.numReservation,'act':'accomptePay'}) }}"
|
||||
class="mt-6 inline-flex items-center justify-center gap-4 w-full bg-slate-900 text-white px-8 py-4 rounded-2xl font-black uppercase italic hover:bg-blue-600 transition-all shadow-lg">
|
||||
<span>Payer l'acompte</span>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M17 8l4 4m0 0l-4 4m4-4H3"></path></svg>
|
||||
</a>
|
||||
<div class="relative">
|
||||
<p class="text-xs font-black text-blue-400 uppercase tracking-widest mb-2">Solde restant à régler</p>
|
||||
<div class="flex items-baseline gap-2 mb-8">
|
||||
<p class="text-5xl font-black italic tracking-tighter">{{ solde|number_format(2, ',', ' ') }}€</p>
|
||||
</div>
|
||||
|
||||
{% set acompteOk = contratPaymentPay(contrat, 'accompte') %}
|
||||
{% set cautionOk = contratPaymentPay(contrat, 'caution') %}
|
||||
|
||||
{% if solde > 0 %}
|
||||
{% if acompteOk and cautionOk %}
|
||||
<form data-turbo="false" action="{{ path('gestion_contrat_view', {'num': contrat.numReservation, 'act': 'soldePay'}) }}" method="GET" class="space-y-4">
|
||||
{# On garde les paramètres de la route pour le formulaire en GET #}
|
||||
<input type="hidden" name="num" value="{{ contrat.numReservation }}">
|
||||
<input type="hidden" name="act" value="soldePay">
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="amountToPay" class="text-[10px] font-black uppercase text-slate-400 ml-1">Montant à régler maintenant</label>
|
||||
<div class="relative">
|
||||
<input type="number"
|
||||
name="amountToPay"
|
||||
id="amountToPay"
|
||||
step="0.01"
|
||||
min="1"
|
||||
max="{{ solde }}"
|
||||
value="{{ solde }}"
|
||||
class="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 text-2xl font-black italic text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white/10 transition-all"
|
||||
>
|
||||
<span class="absolute right-6 top-1/2 -translate-y-1/2 text-xl font-black italic text-blue-400">€</span>
|
||||
</div>
|
||||
<p class="text-[9px] text-slate-500 italic ml-1 italic">Saisissez un montant (Max. {{ solde|number_format(2, ',', ' ') }}€)</p>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="group flex items-center justify-center gap-3 w-full bg-blue-600 hover:bg-blue-500 text-white py-4 rounded-2xl font-black uppercase italic transition-all shadow-lg shadow-blue-900/20">
|
||||
<span>Procéder au paiement</span>
|
||||
<svg class="w-5 h-5 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="mt-4 text-[10px] text-amber-600 font-bold uppercase italic tracking-widest leading-tight">Veuillez signer le contrat<br>pour débloquer le paiement</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-white rounded-[2rem] border border-green-100 shadow-xl shadow-green-100/20 overflow-hidden">
|
||||
<div class="bg-green-500 p-6 text-white flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg>
|
||||
</div>
|
||||
<p class="text-sm font-black uppercase italic leading-none">Acompte encaissé</p>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
{% for payment in paymentList %}
|
||||
<div class="flex flex-col gap-3 p-5 bg-slate-50 rounded-[1.5rem] border border-slate-100">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-[9px] font-black uppercase text-green-600 bg-green-100 px-2 py-0.5 rounded-full tracking-tighter">Paiement validé</span>
|
||||
<span class="text-[9px] font-medium text-slate-400 italic">{{ payment.validateAt|date('d/m/Y à H:i') }}</span>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<p class="text-[8px] font-black text-slate-400 uppercase tracking-widest">ID Transaction</p>
|
||||
<p class="text-[10px] font-mono font-bold text-slate-600 bg-white px-2 py-1.5 rounded border border-slate-100 select-all">{{ payment.paymentId }}</p>
|
||||
</div>
|
||||
<div class="flex justify-between items-end pt-2 border-t border-slate-200/50">
|
||||
<div>
|
||||
<p class="text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">Moyen utilisé</p>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[10px] font-black uppercase text-slate-700 italic">
|
||||
{# Affiche le nom propre (ex: Carte Bancaire, Klarna...) #}
|
||||
{{ payment.card.method_label|default('Paiement Stripe') }}
|
||||
</span>
|
||||
|
||||
{% if payment.card.type == "card" %}
|
||||
<span class="text-[10px] text-slate-400 font-bold uppercase italic">
|
||||
{# Affiche la marque et les 4 chiffres #}
|
||||
({{ payment.card.card.brand|default('') }} **** {{ payment.card.card.last4|default('') }})
|
||||
</span>
|
||||
|
||||
{# Badge optionnel pour le type de débit #}
|
||||
{% if payment.card.card.funding == "debit" %}
|
||||
<span class="text-[7px] bg-slate-100 text-slate-500 px-1 rounded">DEBIT</span>
|
||||
{% endif %}
|
||||
{% if payment.card.card.funding == "credit" %}
|
||||
<span class="text-[7px] bg-slate-100 text-slate-500 px-1 rounded">CREDIT</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xl font-black text-slate-900 italic leading-none">{{ payment.amount|number_format(2, ',', ' ') }}€</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# --- SECTION CAUTION --- #}
|
||||
{% if contratPaymentPay(contrat, 'accompte') %}
|
||||
{% if not contratPaymentPay(contrat, 'caution') %}
|
||||
<div class="bg-white rounded-[2rem] border border-red-100 shadow-xl shadow-red-100/20 overflow-hidden">
|
||||
<div class="bg-red-500 p-6 text-white flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 9v2m0 4h.01"></path></svg>
|
||||
</div>
|
||||
<p class="text-sm font-black uppercase italic leading-none">Caution à déposer</p>
|
||||
</div>
|
||||
<div class="p-8 text-center">
|
||||
<p class="text-4xl font-black text-slate-900 italic tracking-tighter">{{ totalCaution|number_format(2, ',', ' ') }}€</p>
|
||||
|
||||
{% set canPayCaution = (date('now') >= contrat.dateAt.modify('-7 days')) %}
|
||||
{% if canPayCaution %}
|
||||
<a href="{{ path('gestion_contrat_view', {'num': contrat.numReservation,'act':'cautionPay'}) }}"
|
||||
class="mt-6 inline-flex items-center justify-center gap-4 w-full bg-slate-900 text-white px-8 py-4 rounded-2xl font-black uppercase italic hover:bg-red-600 transition-all shadow-lg">
|
||||
<span>Déposer la caution</span>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M17 8l4 4m0 0l-4 4m4-4H3"></path></svg>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="mt-6 p-4 bg-slate-50 rounded-2xl border border-slate-100">
|
||||
<p class="text-[10px] text-amber-600 font-black uppercase tracking-widest leading-tight">
|
||||
Lien actif le {{ contrat.dateAt.modify('-7 days')|date('d/m/Y') }}
|
||||
<div class="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-amber-500 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||||
<p class="text-[10px] text-slate-400 font-bold uppercase italic leading-tight">
|
||||
<span class="text-amber-500">Paiement du solde indisponible</span><br><br>
|
||||
Vous devez d'abord :<br>
|
||||
<span class="{{ acompteOk ? 'text-green-500' : 'text-slate-300' }}">1. Régler l'acompte ({{ acompteOk ? 'OK' : 'En attente' }})</span><br>
|
||||
<span class="{{ cautionOk ? 'text-green-500' : 'text-slate-300' }}">2. Déposer la caution ({{ cautionOk ? 'OK' : 'En attente' }})</span>
|
||||
</p>
|
||||
<p class="text-[9px] text-slate-400 italic mt-1">(7 jours avant le début)</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-white rounded-[2rem] border border-green-100 shadow-xl shadow-green-100/20 overflow-hidden">
|
||||
<div class="bg-green-500 p-6 text-white flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg>
|
||||
</div>
|
||||
<p class="text-sm font-black uppercase italic leading-none">Caution sécurisée</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="flex items-center gap-3 bg-green-500/20 text-green-400 p-6 rounded-2xl border border-green-500/30">
|
||||
<svg class="w-8 h-8 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm font-black uppercase italic">Dossier Soldé</p>
|
||||
<p class="text-[9px] opacity-70 font-bold uppercase tracking-widest">Aucun montant restant</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
{% for payment in paymentCaution %}
|
||||
<div class="flex flex-col gap-3 p-5 bg-slate-50 rounded-[1.5rem] border border-slate-100">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-[9px] font-black uppercase text-blue-600 bg-blue-100 px-2 py-0.5 rounded-full tracking-tighter">Empreinte OK</span>
|
||||
<span class="text-[9px] font-medium text-slate-400 italic">{{ payment.validateAt|date('d/m/Y à H:i') }}</span>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<p class="text-[8px] font-black text-slate-400 uppercase tracking-widest">ID Garantie</p>
|
||||
<p class="text-[10px] font-mono font-bold text-slate-600 bg-white px-2 py-1.5 rounded border border-slate-100 select-all">{{ payment.paymentId }}</p>
|
||||
</div>
|
||||
<div class="flex justify-between items-end pt-2 border-t border-slate-200/50">
|
||||
<div>
|
||||
<p class="text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">Source</p>
|
||||
<span class="text-[10px] font-black uppercase text-slate-700 italic">
|
||||
{{ payment.card.method_label|default('Empreinte CB') }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xl font-black text-slate-900 italic leading-none">{{ payment.amount|number_format(2, ',', ' ') }}€</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# SOLDE #}
|
||||
<div class="bg-white rounded-[2rem] p-10 border border-slate-100 shadow-sm text-center">
|
||||
<p class="text-xs font-black text-slate-400 uppercase tracking-widest mb-4">Solde restant</p>
|
||||
<p class="text-5xl font-black text-slate-900 italic tracking-tighter">{{ solde|number_format(2, ',', ' ') }}€</p>
|
||||
<div class="mt-4 p-4 bg-slate-50 rounded-2xl">
|
||||
<p class="text-[10px] text-slate-400 font-bold uppercase italic">À régler le jour de la prestation</p>
|
||||
<p class="text-[9px] text-slate-500 font-bold uppercase italic mt-6 text-center tracking-widest opacity-60">
|
||||
Paiement sécurisé via Stripe
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- SECTION ACTIONS : 3 COLONNES --- #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
|
||||
{# 1. SIGNATURE #}
|
||||
{% if contrat.signed %}
|
||||
<div class="bg-white rounded-[2rem] border border-green-100 shadow-xl shadow-green-100/20 overflow-hidden">
|
||||
<div class="bg-green-500 p-6 text-white flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg></div>
|
||||
<p class="text-sm font-black uppercase italic leading-none">Contrat Signé</p>
|
||||
</div>
|
||||
<div class="p-6 text-center">
|
||||
<p class="text-[10px] text-slate-400 font-black uppercase mb-1 tracking-widest">ID Yousign</p>
|
||||
<p class="text-[10px] font-mono font-bold text-slate-800 break-all bg-slate-50 p-3 rounded-xl border border-slate-100">{{ signedNumber }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-white rounded-[2rem] border border-red-100 shadow-xl shadow-red-100/20 overflow-hidden">
|
||||
<div class="bg-red-500 p-6 text-white flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg></div>
|
||||
<p class="text-sm font-black uppercase italic leading-none">Signature</p>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<a href="{{ signUrl }}" class="flex flex-col items-center justify-center p-5 bg-slate-900 text-white rounded-2xl hover:bg-blue-600 transition-all group shadow-lg">
|
||||
<span class="font-black uppercase italic text-sm">Signer le contrat</span>
|
||||
<span class="text-[9px] text-blue-300 font-black uppercase mt-1 tracking-widest">Étape 1 : Obligatoire</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# 2. ACOMPTE #}
|
||||
{% if not contratPaymentPay(contrat, 'accompte') %}
|
||||
<div class="bg-white rounded-[2rem] border border-red-100 shadow-xl shadow-red-100/20 overflow-hidden">
|
||||
<div class="bg-red-500 p-6 text-white flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2"></path></svg></div>
|
||||
<p class="text-sm font-black uppercase italic leading-none">Acompte (25%)</p>
|
||||
</div>
|
||||
<div class="p-6 text-center">
|
||||
<p class="text-3xl font-black text-slate-900 italic mb-4">{{ arrhes|number_format(2, ',', ' ') }}€</p>
|
||||
{% if contrat.signed %}
|
||||
<a href="{{ path('gestion_contrat_view', {'num': contrat.numReservation,'act':'accomptePay'}) }}" class="block w-full bg-slate-900 text-white py-4 rounded-xl font-black uppercase text-xs hover:bg-blue-600 transition-all shadow-md">Régler l'acompte</a>
|
||||
{% else %}
|
||||
<div class="p-3 bg-amber-50 rounded-xl border border-amber-100">
|
||||
<span class="text-[9px] text-amber-600 font-black uppercase tracking-tighter">Attente de signature du contrat</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-white rounded-[2rem] border border-green-100 shadow-xl shadow-green-100/20 overflow-hidden">
|
||||
<div class="bg-green-500 p-6 text-white flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg></div>
|
||||
<p class="text-sm font-black uppercase italic leading-none">Acompte Réglé</p>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
{% for payment in paymentList %}
|
||||
<div class="bg-slate-50 p-4 rounded-2xl border border-slate-100">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest">Moyen utilisé</p>
|
||||
<p class="text-lg font-black text-slate-900 italic leading-none">{{ payment.amount|number_format(2, ',', ' ') }}€</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[10px] font-black uppercase text-slate-700 italic">{{ payment.card.method_label|default('Carte Bancaire') }}</span>
|
||||
{% if payment.card.type == "card" %}
|
||||
<span class="text-[9px] text-slate-400 font-bold uppercase italic">({{ payment.card.card.brand|default('') }} **** {{ payment.card.card.last4|default('') }})</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="text-[8px] text-slate-400 font-medium italic mt-2 uppercase tracking-tighter">Validé le {{ payment.validateAt|date('d/m/Y') }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# 3. CAUTION #}
|
||||
{% if not contratPaymentPay(contrat, 'caution') %}
|
||||
<div class="bg-white rounded-[2rem] border border-slate-100 shadow-xl shadow-slate-200/20 overflow-hidden">
|
||||
<div class="bg-slate-800 p-6 text-white flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 9v2m0 4h.01"></path></svg></div>
|
||||
<p class="text-sm font-black uppercase italic leading-none">Caution</p>
|
||||
</div>
|
||||
<div class="p-6 text-center">
|
||||
<p class="text-3xl font-black text-slate-900 italic mb-4">{{ totalCaution|number_format(2, ',', ' ') }}€</p>
|
||||
{% set canPayCaution = (date('now') >= contrat.dateAt.modify('-7 days')) %}
|
||||
{% if canPayCaution and contratPaymentPay(contrat, 'accompte') %}
|
||||
<a href="{{ path('gestion_contrat_view', {'num': contrat.numReservation,'act':'cautionPay'}) }}" class="block w-full bg-slate-900 text-white py-4 rounded-xl font-black uppercase text-xs hover:bg-blue-600 transition-all shadow-md">Déposer l'empreinte</a>
|
||||
{% else %}
|
||||
<div class="p-3 bg-slate-50 rounded-xl border border-slate-100">
|
||||
<p class="text-[9px] text-slate-400 font-black uppercase tracking-tighter">Lien actif le {{ contrat.dateAt.modify('-7 days')|date('d/m/Y') }}</p>
|
||||
<p class="text-[8px] text-slate-300 italic mt-1">(7j avant prestation)</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-white rounded-[2rem] border border-green-100 shadow-xl shadow-green-100/20 overflow-hidden">
|
||||
<div class="bg-green-500 p-6 text-white flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg></div>
|
||||
<p class="text-sm font-black uppercase italic leading-none">Caution Sécurisée</p>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
{% for payment in paymentCaution %}
|
||||
<div class="bg-slate-50 p-4 rounded-2xl border border-slate-100">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest">Dépôt</p>
|
||||
<p class="text-lg font-black text-slate-900 italic leading-none">{{ payment.amount|number_format(2, ',', ' ') }}€</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[10px] font-black uppercase text-slate-700 italic">{{ payment.card.card.brand|default('Carte') }}</span>
|
||||
<span class="text-[9px] text-slate-400 font-bold uppercase italic">**** {{ payment.card.card.last4|default('') }}</span>
|
||||
{% if payment.card.card.funding == "debit" %}<span class="text-[7px] bg-slate-200 text-slate-500 px-1 rounded uppercase">Debit</span>{% endif %}
|
||||
</div>
|
||||
<div class="flex items-center gap-1 mt-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-green-500"></span>
|
||||
<p class="text-[8px] text-green-600 font-black uppercase tracking-tighter">Empreinte bancaire validée</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{# ... (Garder tout le code précédent inchangé jusqu'à la fin de la grille 3 colonnes) ... #}
|
||||
|
||||
{# --- SECTION HISTORIQUE COMPLET DES TRANSACTIONS --- #}
|
||||
<div class="mt-12">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="h-px flex-1 bg-slate-200"></div>
|
||||
<h2 class="text-[10px] font-black uppercase tracking-[0.3em] text-slate-400">Historique des transactions</h2>
|
||||
<div class="h-px flex-1 bg-slate-200"></div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-[2.5rem] border border-slate-100 shadow-sm overflow-hidden">
|
||||
<table class="w-full text-left">
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 border-b border-slate-100">
|
||||
<th class="px-8 py-4 text-[9px] font-black uppercase text-slate-400 tracking-widest">Date & Heure</th>
|
||||
<th class="px-8 py-4 text-[9px] font-black uppercase text-slate-400 tracking-widest">Type / Objet</th>
|
||||
<th class="px-8 py-4 text-[9px] font-black uppercase text-slate-400 tracking-widest">Méthode</th>
|
||||
<th class="px-8 py-4 text-[9px] font-black uppercase text-slate-400 tracking-widest text-right">Montant</th>
|
||||
<th class="px-8 py-4 text-[9px] font-black uppercase text-slate-400 tracking-widest text-center">Statut</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
{# Fusion de toutes les listes de paiement pour l'affichage global #}
|
||||
{% set allPayments = paymentList|merge(paymentCaution)|merge(paymentCtaList|default([])) %}
|
||||
|
||||
{% if allPayments is not empty %}
|
||||
{% for pay in allPayments|sort((a, b) => b.validateAt <=> a.validateAt) %}
|
||||
<tr class="hover:bg-slate-50/30 transition-colors">
|
||||
<td class="px-8 py-5">
|
||||
<p class="text-xs font-bold text-slate-900">{{ pay.validateAt|date('d/m/Y') }}</p>
|
||||
<p class="text-[10px] text-slate-400 italic">{{ pay.validateAt|date('H:i') }}</p>
|
||||
</td>
|
||||
<td class="px-8 py-5">
|
||||
{% if pay in paymentCaution %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black uppercase bg-slate-100 text-slate-600 tracking-tighter">Caution (Empreinte)</span>
|
||||
{% elseif pay in paymentList %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black uppercase bg-blue-50 text-blue-600 tracking-tighter">Acompte</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black uppercase bg-indigo-50 text-indigo-600 tracking-tighter">Paiement complémentaire</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-8 py-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-6 h-4 bg-slate-100 rounded flex items-center justify-center text-[7px] font-bold text-slate-500 uppercase">
|
||||
{{ pay.card.card.brand|default('CB') }}
|
||||
</div>
|
||||
<p class="text-[10px] font-bold text-slate-700 italic">**** {{ pay.card.card.last4|default('----') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-8 py-5 text-right font-black text-slate-900 italic text-sm">
|
||||
{{ pay.amount|number_format(2, ',', ' ') }}€
|
||||
</td>
|
||||
<td class="px-8 py-5 text-center">
|
||||
<div class="inline-flex items-center gap-1.5 text-green-600">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg>
|
||||
<span class="text-[9px] font-black uppercase">Validé</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="px-8 py-10 text-center">
|
||||
<p class="text-xs font-bold text-slate-400 italic uppercase tracking-widest">Aucune transaction enregistrée pour le moment</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -117,10 +117,10 @@
|
||||
</div>
|
||||
|
||||
{# Menu Desktop #}
|
||||
{# Menu Desktop : Remplacer l'ancien bloc par celui-ci #}
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="{{ path('reservation') }}" class="text-gray-700 hover:text-blue-600 font-medium transition-colors">Accueil</a>
|
||||
<a href="{{ path('reservation_catalogue') }}" class="text-gray-700 hover:text-blue-600 font-medium transition-colors">Catalogue</a>
|
||||
<a href="{{ path('reservation_workflow') }}" class="text-gray-700 hover:text-blue-600 font-medium transition-colors">Comment réserver ?</a>
|
||||
<a href="{{ path('reservation_contact') }}" class="text-gray-700 hover:text-blue-600 font-medium transition-colors">Contact</a>
|
||||
|
||||
<a href="{{ path('reservation_search') }}" class="p-2 text-gray-500 hover:text-blue-600 transition-colors" aria-label="Rechercher">
|
||||
@@ -129,6 +129,33 @@
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
{# GESTION DES ACCÈS COMPTE #}
|
||||
{% if app.user %}
|
||||
<div class="flex items-center gap-4">
|
||||
{# Lien Admin pour ROLE_ROOT #}
|
||||
{% if is_granted('ROLE_ROOT') %}
|
||||
<a href="https://admin.ludikevent.fr" class="text-xs font-black uppercase tracking-widest text-amber-600 bg-amber-50 px-3 py-1 rounded-full border border-amber-100 hover:bg-amber-100 transition-colors">
|
||||
Admin
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{# Lien vers l'espace client #}
|
||||
<a href="{{ path('gestion_contrat') }}" class="flex items-center gap-2 text-blue-600 font-bold hover:opacity-70 transition-opacity">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
|
||||
Mon Espace
|
||||
</a>
|
||||
|
||||
<a href="{{ path('app_logout') }}" class="text-gray-400 hover:text-red-500 transition-colors" title="Déconnexion">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="{{ path('reservation_login') }}" class="text-gray-700 hover:text-blue-600 font-bold transition-colors flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
|
||||
Connexion
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="tel:0614172447" class="inline-flex items-center px-6 py-3 border border-transparent text-sm font-bold rounded-full text-white bg-blue-600 hover:bg-blue-700 shadow-lg shadow-blue-200 transition-all hover:-translate-y-0.5">
|
||||
06 14 17 24 47
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user