feat(contrats): Ajoute détails option, actions paiements et style liste

Ajoute un champ détails pour les options de contrat, permet la validation
manuelle des paiements (accompte, caution, solde) et améliore le style
de la liste des contrats.
```
This commit is contained in:
Serreau Jovann
2026-01-29 10:40:03 +01:00
parent 9a4d7b6ae1
commit e530538af8
9 changed files with 603 additions and 377 deletions

View File

@@ -0,0 +1,32 @@
<?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 Version20260129091410 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 contrats_option ADD details TEXT DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE contrats_option DROP details');
}
}

View File

@@ -7,15 +7,19 @@ use App\Entity\ContratsLine;
use App\Entity\ContratsOption; use App\Entity\ContratsOption;
use App\Entity\ContratsPayments; use App\Entity\ContratsPayments;
use App\Entity\Devis; use App\Entity\Devis;
use App\Entity\Product;
use App\Event\Signature\ContratEvent; use App\Event\Signature\ContratEvent;
use App\Form\Type\ContratsType; use App\Form\Type\ContratsType;
use App\Logger\AppLogger; use App\Logger\AppLogger;
use App\Repository\AccountRepository; use App\Repository\AccountRepository;
use App\Repository\DevisRepository; use App\Repository\DevisRepository;
use App\Repository\ContratsRepository; use App\Repository\ContratsRepository;
use App\Service\Mailer\Mailer;
use App\Service\Pdf\ContratPdfService; use App\Service\Pdf\ContratPdfService;
use App\Service\Pdf\PlPdf;
use App\Service\Signature\Client; use App\Service\Signature\Client;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Illuminate\Support\Facades\Mail;
use Knp\Component\Pager\PaginatorInterface; use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -85,6 +89,7 @@ class ContratsController extends AbstractController
AppLogger $appLogger, AppLogger $appLogger,
EventDispatcherInterface $eventDispatcher, EventDispatcherInterface $eventDispatcher,
KernelInterface $kernel, KernelInterface $kernel,
Mailer $mailer,
): Response { ): Response {
if (!$contrat) { if (!$contrat) {
throw $this->createNotFoundException('Contrat non trouvé.'); throw $this->createNotFoundException('Contrat non trouvé.');
@@ -131,37 +136,182 @@ class ContratsController extends AbstractController
$solde = $totalHt - $dejaPaye; $solde = $totalHt - $dejaPaye;
if($request->query->has('act') && $request->query->get('act') === 'cautionCapture') { if($request->query->has('type') && $request->query->get('type') === 'accompte') {
$amount = $request->query->get('amountToCapture'); $paiementAccompte = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
$paiementCaution = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
'contrat' => $contrat, 'contrat' => $contrat,
'type' => 'caution', 'type' => 'accompte',
]); ]);
$result = $stripeClient->capture($paiementCaution->getPaymentId()); if(!$paiementAccompte) {
if($result['state']) { $paiementAccompte = new ContratsPayments();
$contrat->setCautionState("recover"); $paiementAccompte->setContrat($contrat);
$entityManager->persist($contrat); $paiementAccompte->setType('accompte');
$entityManager->flush();
$this->addFlash("success","Caution restitué");
} else {
$this->addFlash("error",$result['message']);
} }
} $paiementAccompte->setValidateAt(new \DateTimeImmutable());
if($request->query->has('act') && $request->query->get('act') === 'cautionRelease') { $paiementAccompte->setUpdateAt(new \DateTimeImmutable());
$paiementAccompte->setPaymentAt(new \DateTimeImmutable());
$paiementAccompte->setAmount( $totalHt * 0.25);
$paiementAccompte->setState("complete");
$paiementAccompte->setPaymentId("");
$paiementAccompte->setCard([
'type' => 'manuel'
]);
$pdf = new PlPdf($kernel, $paiementAccompte, $contrat);
$pdf->generate();
$content = $pdf->Output('S');
$tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf';
file_put_contents($tmpSigned, $content);
$paiementCaution = $entityManager->getRepository(ContratsPayments::class)->findOneBy([ // On utilise UploadedFile pour simuler un upload propre pour VichUploader
$paiementAccompte->setPaymentFile(new UploadedFile($tmpSigned, "confirmed-" . $paiementAccompte->getId() . ".pdf", "application/pdf", null, true));
$paiementAccompte->setUpdateAt(new \DateTimeImmutable('now'));
$entityManager->persist($paiementAccompte);
$entityManager->flush();
$data = $client->autoSignConfirmedPayment($paiementAccompte);
// 1. Gestion du PDF SIGNÉ
$tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf';
$signedContent = file_get_contents($data);
file_put_contents($tmpSigned, $signedContent);
// On utilise UploadedFile pour simuler un upload propre pour VichUploader
$paiementAccompte->setPaymentSignedFile(new UploadedFile($tmpSigned, "confirmed-certificate-" . $paiementAccompte->getId() . ".pdf", "application/pdf", null, true));
$paiementAccompte->setUpdateAt(new \DateTimeImmutable('now'));
$entityManager->persist($paiementAccompte);
$entityManager->flush();
$customer = $contrat->getCustomer();
$subjectCustomer = "[Ludikevent] Confirmation de votre acompte - #" . $contrat->getNumReservation();
$mailer->send(
$customer->getEmail(),
$customer->getSurname() . ' ' . $customer->getName(),
$subjectCustomer,
"mails/customer/accompte_confirmation.twig",
[
'contrat' => $contrat,
'payment' => $paiementAccompte,
'customer' => $customer,
'reservationLink' => "https://reservation.ludikevent.fr" . $this->generateUrl('gestion_contrat_view', ['num' => $contrat->getNumReservation()])
]
);
$appLogger->record('PAYMENT','Validation accompte manuel pour contrat #' . $contrat->getNumReservation());
$this->addFlash("success","Validation accompte effectuée");
return $this->redirectToRoute('app_crm_contrats_view', ['id' => $contrat->getId()]);
}
if($request->query->has('type') && $request->query->get('type') === 'caution') {
$paiementAccompte = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
'contrat' => $contrat, 'contrat' => $contrat,
'type' => 'caution', 'type' => 'caution',
]); ]);
$result = $stripeClient->cancelPayment($paiementCaution->getPaymentId()); if(!$paiementAccompte) {
if($result['state']) { $paiementAccompte = new ContratsPayments();
$contrat->setCautionState("restitue"); $paiementAccompte->setContrat($contrat);
$entityManager->persist($contrat); $paiementAccompte->setType('caution');
$entityManager->flush();
$this->addFlash("success","Caution restitué");
} else {
$this->addFlash("error",$result['message']);
} }
$paiementAccompte->setValidateAt(new \DateTimeImmutable());
$paiementAccompte->setUpdateAt(new \DateTimeImmutable());
$paiementAccompte->setPaymentAt(new \DateTimeImmutable());
$paiementAccompte->setAmount( $totalCaution);
$paiementAccompte->setState("complete");
$paiementAccompte->setPaymentId("");
$paiementAccompte->setCard([
'type' => 'manuel'
]);
$pdf = new PlPdf($kernel, $paiementAccompte, $contrat);
$pdf->generate();
$content = $pdf->Output('S');
$tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf';
file_put_contents($tmpSigned, $content);
// On utilise UploadedFile pour simuler un upload propre pour VichUploader
$paiementAccompte->setPaymentFile(new UploadedFile($tmpSigned, "confirmed-" . $paiementAccompte->getId() . ".pdf", "application/pdf", null, true));
$paiementAccompte->setUpdateAt(new \DateTimeImmutable('now'));
$entityManager->persist($paiementAccompte);
$entityManager->flush();
$data = $client->autoSignConfirmedPayment($paiementAccompte);
// 1. Gestion du PDF SIGNÉ
$tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf';
$signedContent = file_get_contents($data);
file_put_contents($tmpSigned, $signedContent);
// On utilise UploadedFile pour simuler un upload propre pour VichUploader
$paiementAccompte->setPaymentSignedFile(new UploadedFile($tmpSigned, "confirmed-certificate-" . $paiementAccompte->getId() . ".pdf", "application/pdf", null, true));
$paiementAccompte->setUpdateAt(new \DateTimeImmutable('now'));
$entityManager->persist($paiementAccompte);
$entityManager->flush();
$customer = $contrat->getCustomer();
$subjectCustomer = "[Ludikevent] Confirmation de votre caution - #" . $contrat->getNumReservation();
$mailer->send(
$customer->getEmail(),
$customer->getSurname() . ' ' . $customer->getName(),
$subjectCustomer,
"mails/customer/accompte_confirmation.twig",
[
'contrat' => $contrat,
'payment' => $paiementAccompte,
'customer' => $customer,
'reservationLink' => "https://reservation.ludikevent.fr" . $this->generateUrl('gestion_contrat_view', ['num' => $contrat->getNumReservation()])
]
);
$appLogger->record('PAYMENT','Validation caution manuel pour contrat #' . $contrat->getNumReservation());
$this->addFlash("success","Validation caution effectuée");
return $this->redirectToRoute('app_crm_contrats_view', ['id' => $contrat->getId()]);
}
if($request->query->has('type') && $request->query->get('type') === 'solde') {
$paiementAccompte = $entityManager->getRepository(ContratsPayments::class)->findOneBy([
'contrat' => $contrat,
'type' => 'solde',
]);
if(!$paiementAccompte) {
$paiementAccompte = new ContratsPayments();
$paiementAccompte->setContrat($contrat);
$paiementAccompte->setType('solde');
}
$paiementAccompte->setValidateAt(new \DateTimeImmutable());
$paiementAccompte->setUpdateAt(new \DateTimeImmutable());
$paiementAccompte->setPaymentAt(new \DateTimeImmutable());
$paiementAccompte->setAmount( $totalHt);
$paiementAccompte->setState("complete");
$paiementAccompte->setPaymentId("");
$paiementAccompte->setCard([
'type' => 'manuel'
]);
$pdf = new PlPdf($kernel, $paiementAccompte, $contrat);
$pdf->generate();
$content = $pdf->Output('S');
$tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf';
file_put_contents($tmpSigned, $content);
// On utilise UploadedFile pour simuler un upload propre pour VichUploader
$paiementAccompte->setPaymentFile(new UploadedFile($tmpSigned, "confirmed-" . $paiementAccompte->getId() . ".pdf", "application/pdf", null, true));
$paiementAccompte->setUpdateAt(new \DateTimeImmutable('now'));
$entityManager->persist($paiementAccompte);
$entityManager->flush();
$data = $client->autoSignConfirmedPayment($paiementAccompte);
// 1. Gestion du PDF SIGNÉ
$tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf';
$signedContent = file_get_contents($data);
file_put_contents($tmpSigned, $signedContent);
// On utilise UploadedFile pour simuler un upload propre pour VichUploader
$paiementAccompte->setPaymentSignedFile(new UploadedFile($tmpSigned, "confirmed-certificate-" . $paiementAccompte->getId() . ".pdf", "application/pdf", null, true));
$paiementAccompte->setUpdateAt(new \DateTimeImmutable('now'));
$entityManager->persist($paiementAccompte);
$entityManager->flush();
$customer = $contrat->getCustomer();
$subjectCustomer = "[Ludikevent] Votre réservation est désormais soldée - #" . $contrat->getNumReservation();
$mailer->send(
$customer->getEmail(),
$customer->getSurname() . ' ' . $customer->getName(),
$subjectCustomer,
"mails/customer/accompte_confirmation.twig",
[
'contrat' => $contrat,
'payment' => $paiementAccompte,
'customer' => $customer,
'reservationLink' => "https://reservation.ludikevent.fr" . $this->generateUrl('gestion_contrat_view', ['num' => $contrat->getNumReservation()])
]
);
$appLogger->record('PAYMENT','Validation solde manuel pour contrat #' . $contrat->getNumReservation());
$this->addFlash("success","Validation solde effectuée");
return $this->redirectToRoute('app_crm_contrats_view', ['id' => $contrat->getId()]);
} }
return $this->render('dashboard/contrats/view.twig', [ return $this->render('dashboard/contrats/view.twig', [
@@ -192,7 +342,7 @@ class ContratsController extends AbstractController
$c = new Contrats(); $c = new Contrats();
$lines = [['id' => 0, 'name' => '', 'priceHt1Day' => 0, 'priceHtSupDay' => 0, 'caution' => 0]]; $lines = [['id' => 0, 'name' => '', 'priceHt1Day' => 0, 'priceHtSupDay' => 0, 'caution' => 0]];
$options = [['id' => 0, 'name' => '', 'priceHt' => 0]]; $options = [['id' => 0, 'name' => '', 'priceHt' => 0,'details'=>'']];
if ($devis instanceof Devis) { if ($devis instanceof Devis) {
$c->setDateAt($devis->getStartAt()); $c->setDateAt($devis->getStartAt());
@@ -200,6 +350,7 @@ class ContratsController extends AbstractController
$c->setCustomer($devis->getCustomer()); $c->setCustomer($devis->getCustomer());
$c->setDevis($devis); $c->setDevis($devis);
// Mapping adresse de l'événement // Mapping adresse de l'événement
if ($devis->getAddressShip()) { if ($devis->getAddressShip()) {
$c->setAddressEvent($devis->getAddressShip()->getAddress()); $c->setAddressEvent($devis->getAddressShip()->getAddress());
@@ -213,19 +364,22 @@ class ContratsController extends AbstractController
$options = []; $options = [];
foreach ($devis->getDevisLines() as $line) { foreach ($devis->getDevisLines() as $line) {
$p = $entityManager->getRepository(Product::class)->findOneBy(['name'=>$line->getProduct()]);
$lines[] = [ $lines[] = [
'id' => $line->getId(), 'id' => $line->getId(),
'name' => $line->getProduct()->getName() . " - " . $line->getProduct()->getRef(), 'name' =>$p->getName() . " - " . $p->getRef(),
'priceHt1Day' => $line->getPriceHt(), 'priceHt1Day' => $line->getPriceHt(),
'priceHtSupDay' => $line->getPriceHtSup(), 'priceHtSupDay' => $line->getPriceHtSup(),
'caution' => $line->getProduct()->getCaution(), 'caution' => $p->getCaution(),
]; ];
} }
foreach ($devis->getDevisOptions() as $line) { foreach ($devis->getDevisOptions() as $line) {
$options[] = [ $options[] = [
'id' => $line->getId(), 'id' => $line->getId(),
'name' => $line->getOption()->getName(), 'name' => $line->getOption(),
'details' => $line->getDetails(),
'priceHt' => $line->getPriceHt(), 'priceHt' => $line->getPriceHt(),
]; ];
} }
@@ -257,6 +411,7 @@ class ContratsController extends AbstractController
$vc = new ContratsOption(); $vc = new ContratsOption();
$vc->setContrat($c); $vc->setContrat($c);
$vc->setName($line['name']); $vc->setName($line['name']);
$vc->setDetails($line['details']);
$vc->setPrice($line['priceHt']); $vc->setPrice($line['priceHt']);
$entityManager->persist($vc); $entityManager->persist($vc);
} }

View File

@@ -158,18 +158,6 @@ class SignatureController extends AbstractController
// 4. Sauvegarde en base de données // 4. Sauvegarde en base de données
$devis->setUpdateAt(new \DateTimeImmutable()); $devis->setUpdateAt(new \DateTimeImmutable());
foreach ($devis->getDevisLines() as $line) {
$product = $line->getProduct();
$productReserve = new ProductReserve();
$productReserve->setProduct($product);
$productReserve->setCustomer($devis->getCustomer());
$productReserve->setStartAt($devis->getStartAt());
$productReserve->setEndAt($devis->getEndAt());
$productReserve->setDevis($devis);
$entityManager->persist($productReserve);
}
$entityManager->persist($devis); $entityManager->persist($devis);
$entityManager->flush(); $entityManager->flush();

View File

@@ -3,6 +3,7 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\ContratsOptionRepository; use App\Repository\ContratsOptionRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ContratsOptionRepository::class)] #[ORM\Entity(repositoryClass: ContratsOptionRepository::class)]
@@ -22,6 +23,9 @@ class ContratsOption
#[ORM\Column] #[ORM\Column]
private ?float $price = null; private ?float $price = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $details = null;
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
@@ -62,4 +66,16 @@ class ContratsOption
return $this; return $this;
} }
public function getDetails(): ?string
{
return $this->details;
}
public function setDetails(?string $details): static
{
$this->details = $details;
return $this;
}
} }

View File

@@ -134,7 +134,12 @@ class PlPdf extends Fpdf
$this->SetFont('Arial', 'B', 10); $this->SetFont('Arial', 'B', 10);
$this->Cell(50, 8, $this->clean("Mode de règlement :"), 0, 0); $this->Cell(50, 8, $this->clean("Mode de règlement :"), 0, 0);
$this->SetFont('Arial', '', 10); $this->SetFont('Arial', '', 10);
$this->Cell(0, 8, $this->clean($this->contratsPayments->getCard()['type'] ?? 'Carte Bancaire'), 0, 1); if($this->contratsPayments->getCard()['type'] == "manuel") {
$this->Cell(0, 8, $this->clean("Paiement valider par Ludikevent"), 0, 1);
} else {
$this->Cell(0, 8, $this->clean($this->contratsPayments->getCard()['type'] ?? 'Carte Bancaire'), 0, 1);
}
$this->SetFont('Arial', 'B', 10); $this->SetFont('Arial', 'B', 10);
$this->Cell(50, 8, $this->clean("Référence transaction :"), 0, 0); $this->Cell(50, 8, $this->clean("Référence transaction :"), 0, 0);

View File

@@ -224,7 +224,7 @@
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-end"> <div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-end">
{# 1. PRODUIT AVEC BOUTON RECHERCHE #} {# 1. PRODUIT AVEC BOUTON RECHERCHE #}
<div class="lg:col-span-8"> <div class="lg:col-span-6">
<label class="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1 mb-2 block">Produit / Prestation</label> <label class="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1 mb-2 block">Produit / Prestation</label>
<div class="relative flex items-center"> <div class="relative flex items-center">
<input type="text" name="options[{{ key }}][name]" value="{{ line.name }}" required class="w-full bg-slate-950/50 border-white/5 rounded-2xl text-white focus:ring-purple-500/20 focus:border-purple-500 transition-all py-3 pl-5 pr-12 text-sm"> <input type="text" name="options[{{ key }}][name]" value="{{ line.name }}" required class="w-full bg-slate-950/50 border-white/5 rounded-2xl text-white focus:ring-purple-500/20 focus:border-purple-500 transition-all py-3 pl-5 pr-12 text-sm">
@@ -235,7 +235,10 @@
</button> </button>
</div> </div>
</div> </div>
<div class="lg:col-span-2">
<label class="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1 mb-2 block">Détails</label>
<input type="text" name="options[{{ key }}][details]" value="{{ line.details }}" required class="w-full bg-slate-950/50 border-white/5 rounded-2xl text-white focus:ring-purple-500/20 focus:border-purple-500 transition-all py-3 px-5 text-sm font-mono">
</div>
{# 2. PRIX 1J #} {# 2. PRIX 1J #}
<div class="lg:col-span-3"> <div class="lg:col-span-3">
<label class="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1 mb-2 block">Prix 1J HT</label> <label class="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1 mb-2 block">Prix 1J HT</label>

View File

@@ -3,138 +3,186 @@
{% block title %}Contrats de locations{% endblock %} {% block title %}Contrats de locations{% endblock %}
{% block title_header %}Contrats de locations{% endblock %} {% block title_header %}Contrats de locations{% endblock %}
{% block body %} {% block actions %}
<div class="space-y-6 pb-20"> <div class="flex items-center gap-3">
<a href="{{ path('app_crm_contrats_create') }}"
class="group relative flex items-center gap-2 px-6 py-3 bg-gradient-to-br from-blue-600 to-indigo-700 hover:from-blue-500 hover:to-indigo-600 rounded-2xl text-white text-xs font-bold uppercase tracking-wider transition-all shadow-[0_8px_20px_-6px_rgba(37,99,235,0.5)]">
<svg class="w-4 h-4 transition-transform group-hover:rotate-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Créer un contrat
</a>
</div>
{% endblock %}
{# --- BARRE DE RECHERCHE --- #} {% block body %}
<div class="relative group mb-12"> <div class="space-y-8 pb-20">
<div class="absolute inset-y-0 left-0 pl-6 flex items-center pointer-events-none">
<svg class="w-5 h-5 text-slate-500 group-focus-within:text-blue-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"> {# --- RECHERCHE STYLE NÉO-GLASS --- #}
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> <div class="relative group max-w-2xl mx-auto">
</svg> <div class="absolute -inset-1 bg-gradient-to-r from-blue-500 to-indigo-500 rounded-[2rem] blur opacity-20 group-focus-within:opacity-40 transition duration-1000"></div>
<div class="relative flex items-center">
<div class="absolute inset-y-0 left-0 pl-6 flex items-center pointer-events-none">
<svg class="w-5 h-5 text-slate-400 group-focus-within:text-blue-400 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<input type="text" id="searchContrat"
placeholder="Rechercher un contrat, un client ou une ville..."
class="w-full bg-slate-900/40 border border-white/10 backdrop-blur-2xl text-white text-sm rounded-[1.8rem] pl-14 pr-6 py-5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all placeholder:text-slate-500">
</div> </div>
<input type="text"
id="searchContrat"
placeholder="Rechercher un contrat (Nom, N° ou Ville)..."
class="w-full bg-white/5 border border-white/10 backdrop-blur-xl text-white text-sm rounded-[1.5rem] pl-14 pr-6 py-5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition-all placeholder:text-slate-500 font-medium">
</div> </div>
<div id="contratsList" class="space-y-6"> <div id="contratsList" class="grid gap-6">
{% for contrat in contrats %} {% for contrat in contrats %}
{# CALCUL DES ÉTATS FINANCIERS VIA TES FILTRES TWIG #}
{% set acompteOk = contratPaymentPay(contrat, 'accompte') %} {% set acompteOk = contratPaymentPay(contrat, 'accompte') %}
{% set cautionOk = contratPaymentPay(contrat, 'caution') %} {% set cautionOk = contratPaymentPay(contrat, 'caution') %}
{% set soldeOk = contratPaymentPay(contrat, 'solde') %} {% set soldeOk = contratPaymentPay(contrat, 'solde') %}
<div class="contrat-card bg-white/5 border border-white/10 backdrop-blur-md rounded-[2rem] overflow-hidden hover:border-blue-500/40 transition-all group"> <div class="contrat-card relative overflow-hidden group">
<div class="grid grid-cols-1 lg:grid-cols-12"> {# Background Glow Effect #}
<div class="absolute -inset-px bg-gradient-to-r from-transparent via-white/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-[2rem]"></div>
<div class="relative bg-white/[0.03] border border-white/10 backdrop-blur-md rounded-[2rem] transition-all duration-300 group-hover:bg-white/[0.06] group-hover:translate-y-[-2px] group-hover:shadow-2xl group-hover:shadow-blue-500/10">
<div class="grid grid-cols-1 lg:grid-cols-12 items-center">
{# 1. REF & STATUS #}
<div class="lg:col-span-2 p-8 border-b lg:border-b-0 lg:border-r border-white/5 text-center lg:text-left">
<span class="text-[10px] font-bold text-blue-400 uppercase tracking-[0.2em] mb-1 block">Contrat</span>
<h3 class="text-white font-black text-xl tracking-tighter mb-3">{{ contrat.numReservation }}</h3>
{# --- COLONNE 1 : RÉFÉRENCE & SIGNATURE (2/12) --- #}
<div class="lg:col-span-2 p-8 bg-white/[0.02] flex flex-col justify-center border-r border-white/5">
<span class="text-[9px] font-black text-blue-500 uppercase tracking-widest mb-2 block text-search">Référence</span>
<h3 class="text-white font-black italic text-lg tracking-tighter text-search">{{ contrat.numReservation }}</h3>
<div class="mt-4">
{% if contrat.isSigned %} {% if contrat.isSigned %}
<span class="px-3 py-1 bg-emerald-500/10 text-emerald-500 text-[8px] font-black uppercase rounded-lg border border-emerald-500/20 inline-flex items-center gap-1"> <div class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-[9px] font-bold uppercase tracking-wider">
<svg class="w-2.5 h-2.5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg> <span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span> Signé
Signé </div>
</span>
{% else %} {% else %}
<span class="px-3 py-1 bg-amber-500/10 text-amber-500 text-[8px] font-black uppercase rounded-lg border border-amber-500/20 inline-flex items-center gap-1"> <div class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-amber-500/10 border border-amber-500/20 text-amber-400 text-[9px] font-bold uppercase tracking-wider">
<svg class="w-2.5 h-2.5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"></path></svg> <span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span> Attente
En attente </div>
</span>
{% endif %} {% endif %}
</div> </div>
</div>
{# --- COLONNE 2 : CLIENT (3/12) --- #} {# 2. CLIENT #}
<div class="lg:col-span-3 p-8 border-r border-white/5"> <div class="lg:col-span-3 p-8 border-b lg:border-b-0 lg:border-r border-white/5">
<div class="flex items-start gap-4"> <div class="flex items-center gap-4">
<div class="w-10 h-10 bg-blue-600/10 rounded-xl flex items-center justify-center text-blue-500 shrink-0"> <div class="relative">
<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"></path></svg> <div class="w-12 h-12 bg-gradient-to-tr from-blue-600/20 to-indigo-600/20 rounded-2xl flex items-center justify-center border border-white/10 group-hover:scale-110 transition-transform">
</div> <svg class="w-6 h-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" stroke-width="1.5"></path></svg>
<div> </div>
<span class="text-[9px] font-black text-slate-500 uppercase tracking-widest mb-1 block">Locataire</span> </div>
<p class="text-white font-bold text-base uppercase italic text-search">{{ contrat.customer.surname }} {{ contrat.customer.name }}</p> <div>
<p class="text-slate-400 text-[11px] mt-1">{{ contrat.customer.email }}</p> <p class="text-white font-bold text-base tracking-tight uppercase">{{ contrat.customer.surname }} {{ contrat.customer.name }}</p>
<p class="text-slate-500 text-xs font-medium">{{ contrat.customer.email }}</p>
</div>
</div> </div>
</div> </div>
</div>
{# --- COLONNE 3 : LIEU & VILLE (2/12) --- #} {# 3. LIEU #}
<div class="lg:col-span-2 p-8 border-r border-white/5"> <div class="lg:col-span-2 p-8 border-b lg:border-b-0 lg:border-r border-white/5">
<span class="text-[9px] font-black text-slate-500 uppercase tracking-widest mb-2 block">Lieu</span> <div class="flex flex-col">
<p class="text-white font-bold text-sm leading-tight text-search"> <span class="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1 text-search">Destination</span>
{{ contrat.townEvent }} <p class="text-slate-200 font-bold text-sm">{{ contrat.townEvent }}</p>
</p> <p class="text-blue-500 font-black text-[11px]">{{ contrat.zipCodeEvent }}</p>
<p class="text-amber-500 font-black text-[10px] text-search">{{ contrat.zipCodeEvent }}</p>
</div>
{# --- COLONNE 4 : STATUT FINANCIER (3/12) --- #}
<div class="lg:col-span-3 p-8 border-r border-white/5 bg-white/[0.01]">
<span class="text-[9px] font-black text-slate-500 uppercase tracking-widest mb-4 block">Suivi Paiements</span>
<div class="space-y-3">
{# ACOMPTE #}
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="w-1.5 h-1.5 rounded-full {{ acompteOk ? 'bg-emerald-500 shadow-[0_0_8px_#10b981]' : 'bg-slate-600' }}"></div>
<span class="text-[10px] {{ acompteOk ? 'text-slate-200' : 'text-slate-500' }}">Acompte</span>
</div>
{% if acompteOk %}
<svg class="w-3 h-3 text-emerald-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"></path></svg>
{% endif %}
</div>
{# CAUTION #}
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="w-1.5 h-1.5 rounded-full {{ cautionOk ? 'bg-blue-500 shadow-[0_0_8px_#3b82f6]' : 'bg-rose-500 animate-pulse' }}"></div>
<span class="text-[10px] {{ cautionOk ? 'text-slate-200' : 'text-slate-500' }}">Caution</span>
</div>
<span class="text-[8px] font-black {{ cautionOk ? 'text-blue-400' : 'text-rose-500' }} uppercase">{{ cautionOk ? 'OK' : 'REQUISE' }}</span>
</div>
{# SOLDE #}
<div class="pt-2 border-t border-white/10 flex items-center justify-between">
<span class="text-[9px] font-black {{ soldeOk ? 'text-emerald-400' : 'text-white' }} uppercase italic">{{ soldeOk ? 'SOLDÉ' : 'À PERCEVOIR' }}</span>
{% if soldeOk %}
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
</span>
{% endif %}
</div> </div>
</div> </div>
</div>
{# --- COLONNE 5 : ACTIONS (2/12) --- #} {# 4. PAIEMENTS #}
<div class="lg:col-span-2 p-6 flex flex-row lg:flex-col justify-center gap-2"> <div class="lg:col-span-3 p-8 bg-black/10 lg:bg-transparent">
<a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id}) }}" <div class="grid grid-cols-3 gap-2">
title="Détails"
class="flex-1 lg:flex-none py-3 bg-white/5 hover:bg-blue-600 text-white rounded-xl flex items-center justify-center transition-all border border-white/5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
</a>
<a data-turbo="false" download="contrat-{{ contrat.numReservation }}" {# --- ACOMPTE --- #}
href="{{ vich_uploader_asset(contrat,'devisFile') }}" <div class="flex flex-col items-center gap-2">
class="flex-1 lg:flex-none py-3 bg-white/5 hover:bg-emerald-600 text-white rounded-xl flex items-center justify-center transition-all border border-white/5"> <div class="w-8 h-8 rounded-xl flex items-center justify-center transition-all duration-300
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg> {{ acompteOk
</a> ? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'
: 'bg-rose-500/10 text-rose-500 border border-rose-500/20 shadow-[0_0_15px_rgba(244,63,94,0.1)]'
}}">
{% if acompteOk %}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"></path></svg>
{% else %}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"></path></svg>
{% endif %}
</div>
<span class="text-[8px] font-black uppercase tracking-tighter {{ acompteOk ? 'text-emerald-500' : 'text-rose-500' }}">
Acompte
</span>
</div>
<a data-turbo="false" href="{{ path('app_crm_contrats', {idSend: contrat.id}) }}" {# --- CAUTION --- #}
onclick="return confirm('Envoyer à {{ contrat.customer.email }} ?')" <div class="flex flex-col items-center gap-2">
class="flex-1 lg:flex-none py-3 bg-white/5 hover:bg-indigo-500 text-white rounded-xl flex items-center justify-center transition-all border border-white/5"> <div class="w-8 h-8 rounded-xl flex items-center justify-center transition-all duration-300
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path></svg> {{ cautionOk
</a> ? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'
: 'bg-rose-500/20 text-rose-500 border border-rose-500/30 animate-pulse shadow-[0_0_15px_rgba(244,63,94,0.2)]'
}}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{% if cautionOk %}
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" 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>
{% else %}
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
{% endif %}
</svg>
</div>
<span class="text-[8px] font-black uppercase tracking-tighter {{ cautionOk ? 'text-emerald-500' : 'text-rose-500' }}">
Caution
</span>
</div>
{# --- SOLDE --- #}
<div class="flex flex-col items-center gap-2">
<div class="w-8 h-8 rounded-xl flex items-center justify-center transition-all duration-300
{{ soldeOk
? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'
: 'bg-rose-500/10 text-rose-500 border border-rose-500/20'
}}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{% if soldeOk %}
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"></path>
{% else %}
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
{% endif %}
</svg>
</div>
<span class="text-[8px] font-black uppercase tracking-tighter {{ soldeOk ? 'text-emerald-500' : 'text-rose-500' }}">
Solde
</span>
</div>
</div>
</div>
{# 5. ACTIONS #}
<div class="lg:col-span-2 p-6 flex lg:flex-col items-center justify-center gap-3">
<a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id}) }}"
class="w-full lg:w-12 h-12 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center text-white hover:bg-blue-600 hover:border-blue-400 transition-all group/btn" title="Voir">
<svg class="w-5 h-5 group-hover/btn:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
</a>
<a data-turbo="false" href="{{ path('app_crm_contrats', {idSend: contrat.id}) }}"
onclick="return confirm('Envoyer à {{ contrat.customer.email }} ?')"
class="w-full lg:w-12 h-12 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center text-white hover:bg-indigo-600 hover:border-indigo-400 transition-all group/btn" title="Envoyer">
<svg class="w-5 h-5 group-hover/btn:translate-x-1 group-hover/btn:-translate-y-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path></svg>
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<div class="mt-12 glass-pagination">
{{ knp_pagination_render(contrats) }}
</div>
</div> </div>
{{ knp_pagination_render(contrats) }} <style>
/* Optionnel : Custom style pour la pagination KNP pour matcher le glassmorphism */
.glass-pagination nav ul { @apply flex justify-center gap-2; }
.glass-pagination nav ul li span,
.glass-pagination nav ul li a {
@apply px-4 py-2 bg-white/5 border border-white/10 rounded-xl text-white text-sm transition-all backdrop-blur-md;
}
.glass-pagination nav ul li.active span { @apply bg-blue-600 border-blue-500 font-bold; }
.glass-pagination nav ul li a:hover { @apply bg-white/10 border-white/20; }
</style>
{% endblock %} {% endblock %}

View File

@@ -4,311 +4,286 @@
{% block actions %} {% block actions %}
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
{% if contrat.devis %}
<a href="{{ vich_uploader_asset(contrat.devis,'devisFile') }}"
download
class="flex items-center gap-2 px-6 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 backdrop-blur-md rounded-xl text-white text-xs font-black uppercase italic transition-all group">
<svg class="w-4 h-4 text-red-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
Télécharger Devis PDF
</a>
{% endif %}
{% if contrat.signed %} {% if contrat.signed %}
<a href="{{ vich_uploader_asset(contrat,'devisSignFile') }}" <a href="{{ vich_uploader_asset(contrat,'devisSignFile') }}" download
download class="flex items-center gap-2 px-6 py-2.5 bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/20 backdrop-blur-md rounded-xl text-emerald-400 text-[10px] font-black uppercase italic transition-all group">
class="flex items-center gap-2 px-6 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 backdrop-blur-md rounded-xl text-white text-xs font-black uppercase italic transition-all group"> <svg class="w-4 h-4 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-4 h-4 text-red-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg> </svg>
Télécharger Contrat Signée PDF Contrat Signé PDF
</a> </a>
<a href="{{ vich_uploader_asset(contrat,'devisAuditFile') }}" <a href="{{ vich_uploader_asset(contrat,'devisAuditFile') }}" download
download class="flex items-center gap-2 px-6 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 backdrop-blur-md rounded-xl text-white text-[10px] font-black uppercase italic transition-all group">
class="flex items-center gap-2 px-6 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 backdrop-blur-md rounded-xl text-white text-xs font-black uppercase italic transition-all group"> <svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-4 h-4 text-red-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2a4 4 0 00-4-4H5m11 9a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h11a2 2 0 012 2v11z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg> </svg>
Télécharger Audit Signée PDF Audit Signature
</a> </a>
{% else %} {% else %}
<a href="{{ vich_uploader_asset(contrat,'devisFile') }}" <a href="{{ vich_uploader_asset(contrat,'devisFile') }}" download
download class="flex items-center gap-2 px-6 py-2.5 bg-rose-500/10 hover:bg-rose-500/20 border border-rose-500/20 backdrop-blur-md rounded-xl text-rose-500 text-[10px] font-black uppercase italic transition-all group">
class="flex items-center gap-2 px-6 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 backdrop-blur-md rounded-xl text-white text-xs font-black uppercase italic transition-all group"> <svg class="w-4 h-4 group-hover:rotate-12 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-4 h-4 text-red-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /> </svg>
</svg> Télécharger Contrat PDF
Télécharger Contrat PDF </a>
</a>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{# Définition des états de paiement basés sur les variables du contrôleur #} {# Définition des états de paiement #}
{% set acompteOk = contratPaymentPay(contrat, 'accompte') %} {% set acompteOk = contratPaymentPay(contrat, 'accompte') %}
{% set cautionOk = contratPaymentPay(contrat, 'caution') %} {% set cautionOk = contratPaymentPay(contrat, 'caution') %}
{% set soldeOk = (solde <= 0.05) %} {% set soldeOk = (solde <= 0.05) %}
<div class="space-y-8 pb-20"> <div class="space-y-8 pb-20">
{# --- GRILLE DES 4 CARTES DE STATUT --- #} {# --- 1. BANDEAU DE STATUT RAPIDE --- #}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="p-4 rounded-[1.5rem] bg-white/5 border border-white/10 backdrop-blur-md flex items-center justify-between">
<span class="text-[10px] font-black uppercase text-slate-500 tracking-widest">État Juridique</span>
{% if contrat.signed %}
<span class="px-3 py-1 bg-emerald-500/10 text-emerald-400 text-[9px] font-black rounded-lg border border-emerald-500/20">SIGNÉ</span>
{% else %}
<span class="px-3 py-1 bg-amber-500/10 text-amber-400 text-[9px] font-black rounded-lg border border-amber-500/20 animate-pulse">EN ATTENTE</span>
{% endif %}
</div>
<div class="p-4 rounded-[1.5rem] bg-white/5 border border-white/10 backdrop-blur-md flex items-center justify-between">
<span class="text-[10px] font-black uppercase text-slate-500 tracking-widest">Reste à percevoir</span>
<span class="text-xs font-black {{ soldeOk ? 'text-emerald-400' : 'text-rose-500' }} italic">
{{ soldeOk ? 'CONTRAT SOLDÉ' : (solde|number_format(2, ',', ' ') ~ ' €') }}
</span>
</div>
<div class="p-4 rounded-[1.5rem] bg-white/5 border border-white/10 backdrop-blur-md flex items-center justify-between">
<span class="text-[10px] font-black uppercase text-slate-500 tracking-widest">Référence</span>
<span class="text-xs font-black text-blue-400 italic">#{{ contrat.numReservation }}</span>
</div>
</div>
{# 1. SIGNATURE #} {# --- 2. INFOS CLIENT & ÉVÉNEMENT --- #}
<div class="relative overflow-hidden bg-white/5 border border-white/10 backdrop-blur-xl p-6 rounded-[2rem] transition-all hover:border-white/20"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="flex flex-col gap-4"> {# CARTE CLIENT #}
<div class="w-10 h-10 rounded-xl flex items-center justify-center {{ contrat.isSigned ? 'bg-emerald-500/20 text-emerald-400' : 'bg-amber-500/20 text-amber-500 animate-pulse' }}"> <div class="relative group">
<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="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/></svg> <div class="absolute -inset-0.5 bg-gradient-to-br from-blue-500/20 to-purple-500/20 rounded-[2.5rem] blur opacity-50"></div>
<div class="relative h-full p-8 bg-slate-900/40 border border-white/10 backdrop-blur-xl rounded-[2.5rem]">
<div class="flex items-center gap-4 mb-8">
<div class="w-14 h-14 bg-blue-600/20 rounded-2xl flex items-center justify-center border border-blue-500/30">
<svg class="w-7 h-7 text-blue-400" 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>
</div>
<div>
<h2 class="text-xl font-black text-white italic tracking-tighter uppercase">{{ contrat.customer.surname }} {{ contrat.customer.name }}</h2>
<p class="text-slate-500 font-bold text-[10px] uppercase tracking-widest">Informations Locataire</p>
</div>
</div> </div>
<div> <div class="space-y-3">
<p class="text-slate-500 text-[9px] font-black uppercase tracking-widest">Contrat</p> <div class="flex items-center gap-3 p-4 rounded-2xl bg-white/[0.02] border border-white/5 text-slate-300 text-sm">
{% if contrat.isSigned %} <svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path 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" stroke-width="2"/></svg>
<p class="text-white font-bold italic uppercase">Signé</p> {{ contrat.customer.email }}
{% else %} </div>
<a href="{{ signUrl }}" target="_blank" class="text-amber-500 font-black italic hover:underline">À SIGNER ICI</a> <div class="flex items-center gap-3 p-4 rounded-2xl bg-white/[0.02] border border-white/5 text-slate-300 text-sm">
{% endif %} <svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path 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" stroke-width="2"/></svg>
{{ contrat.customer.phone }}
</div>
</div> </div>
</div> </div>
</div> </div>
{# 2. ARRHES (ACOMPTE) #} {# CARTE ÉVÉNEMENT #}
<div class="relative overflow-hidden bg-white/5 border border-white/10 backdrop-blur-xl p-6 rounded-[2rem]"> <div class="relative group">
<div class="flex flex-col gap-4"> <div class="absolute -inset-0.5 bg-gradient-to-br from-emerald-500/20 to-teal-500/20 rounded-[2.5rem] blur opacity-50"></div>
<div class="w-10 h-10 rounded-xl flex items-center justify-center {{ acompteOk ? 'bg-blue-500/20 text-blue-400' : 'bg-slate-500/20 text-slate-400' }}"> <div class="relative h-full p-8 bg-slate-900/40 border border-white/10 backdrop-blur-xl rounded-[2.5rem]">
<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 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z"/></svg> <div class="flex items-center gap-4 mb-8">
<div class="w-14 h-14 bg-emerald-600/20 rounded-2xl flex items-center justify-center border border-emerald-500/30">
<svg class="w-7 h-7 text-emerald-400" 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>
<h2 class="text-xl font-black text-white italic tracking-tighter uppercase">Lieu de l'événement</h2>
<p class="text-slate-500 font-bold text-[10px] uppercase tracking-widest">{{ contrat.dateAt|date('d/m/Y') }} - {{ contrat.endAt|date('d/m/Y') }}</p>
</div>
</div> </div>
<div> <div class="p-5 rounded-2xl bg-white/[0.02] border border-white/5">
<p class="text-slate-500 text-[9px] font-black uppercase tracking-widest">Acompte (Arrhes)</p> <span class="text-[9px] font-black text-slate-500 uppercase tracking-widest block mb-1">Adresse de livraison</span>
{% if acompteOk %} <p class="text-white text-base font-bold">{{ contrat.addressEvent }}</p>
<p class="text-white font-bold italic uppercase">Encaissé</p> <p class="text-emerald-400 font-black text-sm uppercase italic">{{ contrat.zipCodeEvent }} {{ contrat.townEvent }}</p>
{% else %}
<a href="{{ path('gestion_contrat_view', {num: contrat.numReservation, act: 'accomptePay'}) }}" class="text-blue-400 font-black italic hover:underline uppercase">Régler {{ arrhes|number_format(2, ',', ' ') }}€</a>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
{# 3. CAUTION (MODE ADMINISTRATION) #} </div>
<div class="relative overflow-hidden bg-white/5 border border-white/10 backdrop-blur-xl p-6 rounded-[2rem]">
<div class="flex flex-col gap-4"> {# --- 3. ÉTAT DES PAIEMENTS AVEC ACTIONS DE GESTION --- #}
{# Icône dynamique : Pulse si non déposée, fixe si OK #} <div class="p-8 bg-white/[0.02] border border-white/10 rounded-[2.5rem] backdrop-blur-md shadow-2xl">
<div class="w-10 h-10 rounded-xl flex items-center justify-center {{ cautionOk ? 'bg-purple-500/20 text-purple-400' : 'bg-rose-500/20 text-rose-500 animate-pulse' }}"> <div class="flex flex-col md:flex-row gap-8 justify-around items-start">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> {# --- ACOMPTE --- #}
<div class="flex flex-col items-center gap-4 text-center w-full">
<div class="w-16 h-16 rounded-2xl flex items-center justify-center border transition-all duration-500
{{ acompteOk ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30 shadow-[0_0_20px_rgba(16,185,129,0.1)]' : 'bg-rose-500/10 text-rose-500 border-rose-500/20' }}">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{% if acompteOk %}<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/>{% else %}<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>{% endif %}
</svg> </svg>
</div> </div>
<div> <div>
<p class="text-slate-500 text-[9px] font-black uppercase tracking-widest">Garantie (Caution)</p> <span class="block text-[9px] font-black uppercase tracking-widest {{ acompteOk ? 'text-emerald-500' : 'text-rose-500' }}">Acompte</span>
{% if not acompteOk %}
{% if not cautionOk %} <a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, type: 'accompte'}) }}"
{# ÉTAT 1 : ATTENTE DE DÉPÔT #} class="mt-3 inline-flex px-4 py-1.5 bg-rose-500/20 hover:bg-rose-500/30 border border-rose-500/30 rounded-lg text-[10px] font-black text-rose-400 uppercase transition-all">
<div class="mt-1"> Marquer réglé
<p class="text-rose-500 font-black italic uppercase text-sm leading-tight">Non Déposée</p> </a>
<p class="text-[9px] text-slate-500 uppercase font-bold italic">Attendu : {{ totalCaution|number_format(2, ',', ' ') }}€</p>
</div>
{% else %} {% else %}
{# ÉTAT 2 : CAUTION DÉPOSÉE #} <p class="mt-2 text-[10px] text-emerald-500/60 font-bold uppercase italic">Encaissé</p>
{% if contrat.cautionState == null %} {% endif %}
{# ACTIONS DISPONIBLES #} </div>
<div class="space-y-3 mt-2"> </div>
<p class="text-white font-bold italic uppercase text-[10px] mb-2 opacity-70">Empreinte active</p>
<div class="flex flex-col gap-2">
{# LIBÉRER #}
<a href="{{ path('app_crm_contrats_view', {id: contrat.id, act: 'cautionRelease'}) }}"
data-turbo="false"
onclick="return confirm('Confirmer la libération de la caution ?')"
class="w-full py-1.5 bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/30 rounded-lg text-emerald-400 text-[9px] font-black uppercase italic text-center transition-all">
Libérer la caution
</a>
{# ENCAISSER #} <div class="hidden md:block w-px h-20 bg-white/5 self-center"></div>
<form action="{{ path('app_crm_contrats_view', {id: contrat.id}) }}" method="GET" data-turbo="false" class="space-y-1">
<input type="hidden" name="act" value="cautionCapture"> {# --- CAUTION --- #}
<div class="flex items-center bg-rose-500/5 rounded-lg border border-rose-500/20 px-2 py-1"> <div class="flex flex-col items-center gap-4 text-center w-full">
<input type="number" name="amountToCapture" step="0.01" max="{{ totalCaution }}" value="{{ totalCaution }}" <div class="w-16 h-16 rounded-2xl flex items-center justify-center border transition-all duration-500
class="bg-transparent border-none text-rose-500 text-[10px] font-black w-14 p-0 focus:ring-0"> {{ cautionOk ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30 shadow-[0_0_20px_rgba(16,185,129,0.1)]' : 'bg-rose-500/20 text-rose-500 border-rose-500/30 animate-pulse' }}">
<button type="submit" onclick="return confirm('Confirmer l\'encaissement ?')" <svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
class="ml-auto text-rose-500 text-[9px] font-black uppercase italic hover:text-white transition-colors"> {% if cautionOk %}<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"/>{% else %}<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>{% endif %}
Encaisser </svg>
</button> </div>
</div> <div>
</form> <span class="block text-[9px] font-black uppercase tracking-widest {{ cautionOk ? 'text-emerald-500' : 'text-rose-500' }}">Caution</span>
</div> <div class="flex flex-col gap-2 mt-3">
</div> {% if not cautionOk %}
<a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, type: 'caution'}) }}"
class="px-4 py-1.5 bg-rose-500/20 hover:bg-rose-500/30 border border-rose-500/30 rounded-lg text-[10px] font-black text-rose-400 uppercase transition-all">
Marquer reçue
</a>
{% else %} {% else %}
{# ÉTAT 3 : ARCHIVÉ (RESTITUÉ OU RÉCUPÉRÉ) #} <div class="flex gap-2">
<div class="mt-2"> <a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, action: 'encaisser'}) }}"
{% if contrat.cautionState == 'restitue' %} class="px-3 py-1.5 bg-amber-500/20 hover:bg-amber-500/30 border border-amber-500/30 rounded-lg text-[9px] font-black text-amber-500 uppercase transition-all">
<span class="px-3 py-1 bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-[10px] font-black uppercase italic rounded-full"> Encaisser
Restituée </a>
</span> <a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, action: 'liberer'}) }}"
{% elseif contrat.cautionState == 'recover' %} class="px-3 py-1.5 bg-emerald-500/20 hover:bg-emerald-500/30 border border-emerald-500/30 rounded-lg text-[9px] font-black text-emerald-400 uppercase transition-all">
<span class="px-3 py-1 bg-amber-500/10 border border-amber-500/20 text-amber-400 text-[10px] font-black uppercase italic rounded-full"> Libérer
Encaissée </a>
</span>
{% else %}
<span class="text-white font-bold text-xs uppercase opacity-50">{{ contrat.cautionState }}</span>
{% endif %}
</div> </div>
{% endif %} {% endif %}
{% endif %} </div>
</div> </div>
</div> </div>
</div>
{# 4. SOLDE #} <div class="hidden md:block w-px h-20 bg-white/5 self-center"></div>
<div class="relative overflow-hidden bg-white/5 border border-white/10 backdrop-blur-xl p-6 rounded-[2rem]">
<div class="flex flex-col gap-4"> {# --- SOLDE --- #}
<div class="w-10 h-10 rounded-xl flex items-center justify-center {{ soldeOk ? 'bg-emerald-500 text-white' : 'bg-amber-500/20 text-amber-500' }}"> <div class="flex flex-col items-center gap-4 text-center w-full">
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg> <div class="w-16 h-16 rounded-2xl flex items-center justify-center border transition-all duration-500
{{ soldeOk ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30 shadow-[0_0_20px_rgba(16,185,129,0.1)]' : 'bg-rose-500/10 text-rose-500 border-rose-500/20' }}">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{% if soldeOk %}<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>{% else %}<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z"/>{% endif %}
</svg>
</div> </div>
<div> <div>
<p class="text-slate-500 text-[9px] font-black uppercase tracking-widest">État du solde</p> <span class="block text-[9px] font-black uppercase tracking-widest {{ soldeOk ? 'text-emerald-500' : 'text-rose-500' }}">Solde Final</span>
{% if soldeOk %} {% if not soldeOk %}
<p class="text-emerald-400 font-black italic uppercase">Dossier Soldé</p> <a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, type: 'solde'}) }}"
class="px-4 py-1.5 bg-rose-500/20 hover:bg-rose-500/30 border border-rose-500/30 rounded-lg text-[10px] font-black text-rose-400 uppercase transition-all">
Régler le solde
</a>
{% else %} {% else %}
<form action="{{ path('gestion_contrat_view', {num: contrat.numReservation}) }}" method="GET" class="flex flex-col gap-2"> <p class="mt-2 text-[10px] text-emerald-500/60 font-bold uppercase italic">Totalité payée</p>
<input type="hidden" name="act" value="soldePay">
<div class="flex items-center bg-white/5 rounded-lg border border-white/10 px-2 py-1">
<input type="number" name="amountToPay" step="0.01" max="{{ solde }}" value="{{ solde }}"
class="bg-transparent border-none text-white text-xs font-black w-20 p-0 focus:ring-0">
<button type="submit" class="text-amber-500 text-[10px] font-black uppercase italic hover:text-white">Payer</button>
</div>
<span class="text-[8px] text-slate-500 uppercase">Reste : {{ solde|number_format(2, ',', ' ') }}€</span>
</form>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{# --- 4. PRODUITS ET OPTIONS --- #}
{# --- SECTION DÉTAILS : CLIENT / LOGISTIQUE / FINANCES --- #} <div class="grid grid-cols-1 xl:grid-cols-2 gap-8">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> {# PRODUITS #}
<div class="space-y-4">
{# CARD 1 : CLIENT #} <h3 class="px-2 text-sm font-black text-slate-400 uppercase tracking-[0.3em]">Équipements loués</h3>
<div class="bg-white/5 border border-white/10 backdrop-blur-md rounded-[2.5rem] p-8 relative overflow-hidden group"> <div class="bg-white/[0.02] border border-white/10 rounded-[2rem] divide-y divide-white/5 overflow-hidden">
<div class="absolute -top-10 -right-10 w-32 h-32 bg-blue-500/5 rounded-full blur-3xl group-hover:bg-blue-500/10 transition-all"></div> {% for product in contrat.contratsLines %}
<h2 class="text-blue-500 font-black text-[10px] uppercase tracking-[0.3em] mb-6 flex items-center gap-2"> <div class="p-6 hover:bg-white/[0.03] transition-colors">
<svg class="w-3 h-3" 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> <div class="flex justify-between items-start">
Locataire <div>
</h2> <h4 class="text-white font-bold text-base uppercase italic">{{ product.name }}</h4>
<div class="space-y-1"> <p class="text-[10px] text-slate-500 font-bold uppercase mt-1">Caution : {{ product.caution }}€</p>
<p class="text-white font-black text-2xl uppercase italic tracking-tighter">{{ contrat.customer.surname }}</p> </div>
<p class="text-white font-medium text-xl uppercase italic opacity-80">{{ contrat.customer.name }}</p> <div class="text-right">
</div> <span class="text-blue-400 font-black italic">{{ product.price1DayHt }}€</span>
<div class="mt-6 pt-6 border-t border-white/5 space-y-3"> <span class="block text-[8px] text-slate-500 uppercase font-black">HT / Jour</span>
<div class="flex items-center gap-3 text-slate-400 text-xs font-medium"> </div>
<svg class="w-4 h-4 text-blue-500/50" 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> </div>
{{ contrat.customer.email }} </div>
</div> {% endfor %}
<div class="flex items-center gap-3 text-slate-400 text-xs font-medium">
<svg class="w-4 h-4 text-blue-500/50" 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>
{{ contrat.customer.phone }}
</div>
</div> </div>
</div> </div>
{# CARD 2 : LOGISTIQUE #} {# OPTIONS #}
<div class="bg-white/5 border border-white/10 backdrop-blur-md rounded-[2.5rem] p-8 relative overflow-hidden group"> <div class="space-y-4">
<h2 class="text-amber-500 font-black text-[10px] uppercase tracking-[0.3em] mb-6 flex items-center gap-2"> <h3 class="px-2 text-sm font-black text-slate-400 uppercase tracking-[0.3em]">Options & Services</h3>
<svg class="w-3 h-3" 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 class="bg-white/[0.02] border border-white/10 rounded-[2rem] divide-y divide-white/5 overflow-hidden">
Lieu de l'événement {% for product in contrat.contratsOptions %}
</h2> <div class="p-6 hover:bg-white/[0.03] transition-colors">
<div class="space-y-4"> <div class="flex justify-between items-center">
<div class="p-4 bg-white/[0.03] border border-white/5 rounded-2xl"> <div class="max-w-[70%]">
<p class="text-slate-500 text-[10px] font-bold uppercase mb-2 text-white/50">Adresse</p> <h4 class="text-white font-bold text-sm uppercase">{{ product.name }}</h4>
<p class="text-white text-sm leading-relaxed font-bold italic"> <p class="text-slate-500 text-[10px] italic mt-1">{{ product.details }}</p>
{{ contrat.addressEvent }}<br> </div>
{{ contrat.zipCodeEvent }} {{ contrat.townEvent|upper }} <div class="bg-purple-500/10 border border-purple-500/20 px-3 py-1 rounded-xl">
</p> <span class="text-purple-400 font-black text-xs italic">{{ product.price }}€</span>
</div> </div>
<div class="flex items-center gap-4"> </div>
<div class="flex-1 p-3 bg-white/[0.03] border border-white/5 rounded-2xl">
<p class="text-[9px] font-bold uppercase text-white/50">Du</p>
<p class="text-white font-bold text-xs italic">{{ contrat.dateAt|date('d/m/Y') }}</p>
</div> </div>
<div class="flex-1 p-3 bg-white/[0.03] border border-white/5 rounded-2xl"> {% else %}
<p class="text-[9px] font-bold uppercase text-white/50">Au</p> <div class="p-10 text-center text-slate-600 text-[10px] font-black uppercase tracking-widest opacity-30">Aucun supplément</div>
<p class="text-white font-bold text-xs italic">{{ contrat.endAt|date('d/m/Y') }}</p> {% endfor %}
</div>
</div>
</div>
</div>
{# CARD 3 : RÉCAPITULATIF FINANCIER #}
<div class="bg-slate-900/40 border border-white/10 backdrop-blur-2xl rounded-[2.5rem] p-8 relative overflow-hidden group">
<div class="absolute top-0 right-0 w-32 h-32 bg-blue-600/10 blur-[80px]"></div>
<h2 class="text-white font-black text-[10px] uppercase tracking-[0.3em] mb-6 flex items-center gap-2">
<svg class="w-3 h-3 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01"/></svg>
Détail Règlement
</h2>
<div class="space-y-5">
<div class="flex items-end justify-between">
<span class="text-slate-400 text-xs font-bold uppercase italic">Total Prestation</span>
<span class="text-2xl font-black text-white italic tracking-tighter">{{ totalHT|number_format(2, ',', ' ') }}€</span>
</div>
<div class="p-4 bg-white/5 border border-white/10 rounded-2xl space-y-3">
<div class="flex items-center justify-between">
<p class="text-blue-400 text-[9px] font-black uppercase tracking-wider">Acompte Requis</p>
<p class="text-white font-black text-sm italic tracking-tighter">{{ arrhes|number_format(2, ',', ' ') }}€</p>
</div>
<div class="h-px bg-white/5"></div>
<div class="flex items-center justify-between">
<p class="text-purple-400 text-[9px] font-black uppercase tracking-wider">Montant Caution</p>
<p class="text-white font-black text-sm italic tracking-tighter">{{ totalCaution|number_format(2, ',', ' ') }}€</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
{# --- HISTORIQUE DES PAIEMENTS --- #} {# --- 5. HISTORIQUE DES PAIEMENTS --- #}
<div class="mt-12"> <div class="space-y-4">
<h2 class="text-white font-black text-xl italic uppercase tracking-tighter mb-6 flex items-center gap-3"> <h3 class="px-2 text-sm font-black text-slate-400 uppercase tracking-[0.3em]">Historique Financier</h3>
<span class="w-8 h-px bg-white/20"></span> Historique des transactions <div class="bg-white/[0.02] border border-white/10 rounded-[2rem] overflow-hidden">
</h2> <table class="w-full text-left">
<div class="bg-white/5 border border-white/10 backdrop-blur-xl rounded-[2.5rem] overflow-hidden">
<table class="w-full text-left border-collapse">
<thead> <thead>
<tr class="border-b border-white/5 bg-white/[0.02]"> <tr class="border-b border-white/5 bg-white/[0.03]">
<th class="px-8 py-5 text-[10px] font-black text-slate-500 uppercase tracking-widest">Date</th> <th class="px-8 py-4 text-[9px] font-black text-slate-500 uppercase tracking-widest">Transaction</th>
<th class="px-8 py-5 text-[10px] font-black text-slate-500 uppercase tracking-widest">Type</th> <th class="px-8 py-4 text-[9px] font-black text-slate-500 uppercase tracking-widest">Type</th>
<th class="px-8 py-5 text-[10px] font-black text-slate-500 uppercase tracking-widest">Montant</th> <th class="px-8 py-4 text-[9px] font-black text-slate-500 uppercase tracking-widest">Montant</th>
<th class="px-8 py-5 text-[10px] font-black text-slate-500 uppercase tracking-widest text-right">Action</th> <th class="px-8 py-4 text-[9px] font-black text-slate-500 uppercase tracking-widest text-right">Justificatif</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-white/5"> <tbody class="divide-y divide-white/5">
{% for payment in contrat.contratsPayments %} {% for payment in contrat.contratsPayments %}
{% if payment.state == 'complete' %} {% if payment.state == 'complete' %}
<tr class="group hover:bg-white/[0.02] transition-colors"> <tr class="group hover:bg-white/[0.02] transition-colors">
<td class="px-8 py-5 text-xs text-white font-medium">{{ payment.paymentAt|date('d/m/Y H:i') }}</td> <td class="px-8 py-5 text-xs text-white font-bold tracking-tight">{{ payment.paymentAt|date('d/m/Y H:i') }}</td>
<td class="px-8 py-5"> <td class="px-8 py-5">
<span class="px-3 py-1 rounded-full text-[9px] font-black uppercase italic <span class="px-2 py-1 rounded-lg text-[8px] font-black uppercase italic
{% if payment.type == 'caution' %}bg-purple-500/10 text-purple-400 border border-purple-500/20 {% if payment.type == 'caution' %}bg-purple-500/10 text-purple-400 border border-purple-500/20
{% elseif payment.type == 'accompte' %}bg-blue-500/10 text-blue-400 border border-blue-500/20 {% elseif payment.type == 'accompte' %}bg-blue-500/10 text-blue-400 border border-blue-500/20
{% else %}bg-emerald-500/10 text-emerald-400 border border-emerald-500/20{% endif %}"> {% else %}bg-emerald-500/10 text-emerald-400 border border-emerald-500/20{% endif %}">
{{ payment.type|replace({'_': ' '}) }} {{ payment.type|replace({'_': ' '}) }}
</span> </span>
</td> </td>
<td class="px-8 py-5 text-sm text-white font-black italic">{{ payment.amount|number_format(2, ',', ' ') }}€</td> <td class="px-8 py-5 text-sm text-white font-black italic">{{ payment.amount|number_format(2, ',', ' ') }}€</td>
<td class="px-8 py-5 text-right"> <td class="px-8 py-5 text-right">
<a href="{{ path('app_crm_contrats_view', {id: contrat.id, idPaymentPdf: payment.id}) }}" <a href="{{ path('app_crm_contrats_view', {id: contrat.id, idPaymentPdf: payment.id}) }}" target="_blank"
target="_blank" class="inline-flex items-center gap-2 text-[9px] font-black uppercase text-slate-400 hover:text-white transition-all">
class="inline-flex items-center gap-2 text-[10px] font-black uppercase italic text-slate-400 hover:text-white group transition-all"> <svg class="w-4 h-4 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" stroke-width="2"/></svg>
<svg class="w-4 h-4 text-red-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> REÇU PDF
Reçu
</a> </a>
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
{% else %} {% else %}
<tr> <tr><td colspan="4" class="py-12 text-center text-slate-600 text-[10px] font-black uppercase opacity-40">Aucun paiement effectué</td></tr>
<td colspan="4" class="px-8 py-10 text-center text-slate-500 italic text-xs uppercase opacity-50">Aucune transaction enregistrée</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -49,7 +49,11 @@
<mj-column width="50%"> <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="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"> <mj-text font-size="12px" font-weight="700" color="#475569" align="right" padding-top="10px">
{% if datas.payment.card.type is defined and datas.payment.card.type == "manuel" %}
Paiement effectuer par Ludikevent
{% else %}
{{ datas.payment.card.method_label|default('Carte Bancaire') }} {{ datas.payment.card.method_label|default('Carte Bancaire') }}
{% endif %}
</mj-text> </mj-text>
</mj-column> </mj-column>
</mj-section> </mj-section>