feat(devis/contrat): ajoute prestataires et paiements aux devis et améliore la gestion des règlements et statuts

This commit is contained in:
Serreau Jovann
2026-02-06 11:24:45 +01:00
parent 4ced742e40
commit 919bf7038a
13 changed files with 306 additions and 18 deletions

View File

@@ -0,0 +1,31 @@
<?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 Version20260206170000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add reservation_state to contrats';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql("ALTER TABLE contrats ADD reservation_state VARCHAR(50) DEFAULT 'pending' NOT NULL");
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE contrats DROP reservation_state');
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260206180000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add relation between Prestaire and Devis';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE devis ADD prestataire_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE devis ADD CONSTRAINT FK_8B27C52BBE30DA2F7 FOREIGN KEY (prestataire_id) REFERENCES prestaire (id)');
$this->addSql('CREATE INDEX IDX_8B27C52BBE30DA2F7 ON devis (prestataire_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE devis DROP FOREIGN KEY FK_8B27C52BBE30DA2F7');
$this->addSql('DROP INDEX IDX_8B27C52BBE30DA2F7 ON devis');
$this->addSql('ALTER TABLE devis DROP prestataire_id');
}
}

View File

@@ -75,7 +75,8 @@ class ContratsController extends AbstractController
// 2. Paiement Manuel (Accompte / Solde / Caution) // 2. Paiement Manuel (Accompte / Solde / Caution)
if ($type = $request->query->get('type')) { if ($type = $request->query->get('type')) {
return $this->handleManualPayment($contrat, $type); $method = $request->query->get('method', 'Autre');
return $this->handleManualPayment($contrat, $type, $method);
} }
// 3. Calculs financiers pour l'affichage // 3. Calculs financiers pour l'affichage
@@ -231,7 +232,7 @@ class ContratsController extends AbstractController
return $this->redirectToRoute('app_crm_contrats_view', ['id' => $contrat->getId()]); return $this->redirectToRoute('app_crm_contrats_view', ['id' => $contrat->getId()]);
} }
private function handleManualPayment(Contrats $contrat, string $type): RedirectResponse private function handleManualPayment(Contrats $contrat, string $type, string $paymentMethod = 'Autre'): RedirectResponse
{ {
// Calculs basiques pour le montant (à affiner selon vos besoins réels si différent de la vue) // Calculs basiques pour le montant (à affiner selon vos besoins réels si différent de la vue)
$totalHt = 0; $totalHt = 0;
@@ -274,6 +275,7 @@ class ContratsController extends AbstractController
->setValidateAt(new \DateTimeImmutable()) ->setValidateAt(new \DateTimeImmutable())
->setUpdateAt(new \DateTimeImmutable()) ->setUpdateAt(new \DateTimeImmutable())
->setCard(['type' => 'manuel']) ->setCard(['type' => 'manuel'])
->setTypePayment($paymentMethod)
->setPaymentId("MANUAL-" . uniqid()); ->setPaymentId("MANUAL-" . uniqid());
// Génération PDF Reçu // Génération PDF Reçu

View File

@@ -14,6 +14,7 @@ use App\Repository\DevisLineRepository;
use App\Repository\DevisOptionsRepository; use App\Repository\DevisOptionsRepository;
use App\Repository\DevisRepository; use App\Repository\DevisRepository;
use App\Repository\OptionsRepository; use App\Repository\OptionsRepository;
use App\Repository\PrestaireRepository;
use App\Repository\ProductRepository; use App\Repository\ProductRepository;
use App\Service\Pdf\DevisPdfService; use App\Service\Pdf\DevisPdfService;
use App\Service\Signature\Client; use App\Service\Signature\Client;
@@ -41,7 +42,8 @@ class DevisController extends AbstractController
private readonly DevisRepository $devisRepository, private readonly DevisRepository $devisRepository,
private readonly ProductRepository $productRepository, private readonly ProductRepository $productRepository,
private readonly CustomerAddressRepository $customerAddressRepository, private readonly CustomerAddressRepository $customerAddressRepository,
private readonly CustomerRepository $customerRepository private readonly CustomerRepository $customerRepository,
private readonly PrestaireRepository $prestaireRepository
) { ) {
} }
@@ -107,7 +109,8 @@ class DevisController extends AbstractController
'optionsList' => $optionsRepository->findAll(), 'optionsList' => $optionsRepository->findAll(),
'shipAddress' => [], 'shipAddress' => [],
'billAddress' => [], 'billAddress' => [],
'devis' => $devis 'devis' => $devis,
'prestataires' => $this->prestaireRepository->findAll(),
]); ]);
} }
@@ -185,6 +188,7 @@ class DevisController extends AbstractController
'optionsList' => $optionsRepository->findAll(), 'optionsList' => $optionsRepository->findAll(),
'shipAddress' => $shipAddress, 'shipAddress' => $shipAddress,
'billAddress' => $billAddress, 'billAddress' => $billAddress,
'prestataires' => $this->prestaireRepository->findAll(),
]); ]);
} }
@@ -255,6 +259,9 @@ class DevisController extends AbstractController
if (!empty($devisData['bill_address'])) $devis->setBillAddress($this->customerAddressRepository->find($devisData['bill_address'])); if (!empty($devisData['bill_address'])) $devis->setBillAddress($this->customerAddressRepository->find($devisData['bill_address']));
if (!empty($devisData['ship_address'])) $devis->setAddressShip($this->customerAddressRepository->find($devisData['ship_address'])); if (!empty($devisData['ship_address'])) $devis->setAddressShip($this->customerAddressRepository->find($devisData['ship_address']));
if (!empty($formData['customer'])) $devis->setCustomer($this->customerRepository->find($formData['customer'])); if (!empty($formData['customer'])) $devis->setCustomer($this->customerRepository->find($formData['customer']));
if (!empty($devisData['paymentMethod'])) $devis->setPaymentMethod($devisData['paymentMethod']);
if (!empty($devisData['prestataire'])) $devis->setPrestataire($this->prestaireRepository->find($devisData['prestataire']));
// Calcul durée // Calcul durée
$day = 1; $day = 1;

View File

@@ -65,6 +65,11 @@ class SignatureController extends AbstractController
if ($submission['status'] === "completed") { if ($submission['status'] === "completed") {
$contrats->setIsSigned(true); $contrats->setIsSigned(true);
// Update Reservation State if Acompte is already paid
if ($contrats->isAccompte()) {
$contrats->setReservationState('ready');
}
$auditUrl = $submission['audit_log_url']; $auditUrl = $submission['audit_log_url'];
$signedDocUrl = $submission['documents'][0]['url']; $signedDocUrl = $submission['documents'][0]['url'];

View File

@@ -71,6 +71,13 @@ class Webhooks extends AbstractController
$pl->setPaymentSignedFile(new UploadedFile($tmpSigned, "confirmed-certificate-" . $pl->getId() . ".pdf", "application/pdf", null, true)); $pl->setPaymentSignedFile(new UploadedFile($tmpSigned, "confirmed-certificate-" . $pl->getId() . ".pdf", "application/pdf", null, true));
$pl->setUpdateAt(new \DateTimeImmutable('now')); $pl->setUpdateAt(new \DateTimeImmutable('now'));
$entityManager->persist($pl); $entityManager->persist($pl);
// Update Reservation State if Accompte and Signed
if ($pl->getType() === 'accompte' && $contrat->isSigned()) {
$contrat->setReservationState('ready');
$entityManager->persist($contrat);
}
$entityManager->flush(); $entityManager->flush();
// --- ENVOI DES EMAILS --- // --- ENVOI DES EMAILS ---

View File

@@ -143,6 +143,9 @@ class Contrats
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(length: 255, nullable: true)]
private ?string $cautionState = null; private ?string $cautionState = null;
#[ORM\Column(length: 50, options: ['default' => 'pending'])]
private string $reservationState = 'pending';
#[ORM\OneToOne(mappedBy: 'contrat', cascade: ['persist', 'remove'])] #[ORM\OneToOne(mappedBy: 'contrat', cascade: ['persist', 'remove'])]
private ?EtatLieux $etatLieux = null; private ?EtatLieux $etatLieux = null;
@@ -162,6 +165,18 @@ class Contrats
return $this->id; return $this->id;
} }
public function getReservationState(): string
{
return $this->reservationState;
}
public function setReservationState(string $reservationState): static
{
$this->reservationState = $reservationState;
return $this;
}
public function getCustomer(): ?Customer public function getCustomer(): ?Customer
{ {
return $this->customer; return $this->customer;

View File

@@ -24,6 +24,9 @@ class ContratsPayments
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
private ?string $type = null; private ?string $type = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $typePayment = null;
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
private ?string $paymentId = null; private ?string $paymentId = null;
@@ -89,6 +92,18 @@ class ContratsPayments
return $this; return $this;
} }
public function getTypePayment(): ?string
{
return $this->typePayment;
}
public function setTypePayment(?string $typePayment): static
{
$this->typePayment = $typePayment;
return $this;
}
public function getPaymentId(): ?string public function getPaymentId(): ?string
{ {
return $this->paymentId; return $this->paymentId;

View File

@@ -101,6 +101,8 @@ class Devis
#[ORM\Column(options: ['default' => false])] #[ORM\Column(options: ['default' => false])]
private ?bool $isNotAddCaution = false; private ?bool $isNotAddCaution = false;
#[ORM\ManyToOne(inversedBy: 'devis')]
private ?Prestaire $prestataire = null;
/** /**
* @var Collection<int, DevisOptions> * @var Collection<int, DevisOptions>
@@ -174,6 +176,18 @@ class Devis
return $this; return $this;
} }
public function getPrestataire(): ?Prestaire
{
return $this->prestataire;
}
public function setPrestataire(?Prestaire $prestataire): static
{
$this->prestataire = $prestataire;
return $this;
}
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;

View File

@@ -37,6 +37,12 @@ class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\OneToMany(targetEntity: OrderSession::class, mappedBy: 'prestataire')] #[ORM\OneToMany(targetEntity: OrderSession::class, mappedBy: 'prestataire')]
private Collection $orderSessions; private Collection $orderSessions;
/**
* @var Collection<int, Devis>
*/
#[ORM\OneToMany(targetEntity: Devis::class, mappedBy: 'prestataire')]
private Collection $devis;
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
private ?string $email = null; private ?string $email = null;
@@ -63,6 +69,7 @@ class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface
$this->etatLieuxes = new ArrayCollection(); $this->etatLieuxes = new ArrayCollection();
$this->contrats = new ArrayCollection(); $this->contrats = new ArrayCollection();
$this->orderSessions = new ArrayCollection(); $this->orderSessions = new ArrayCollection();
$this->devis = new ArrayCollection();
} }
public function getId(): ?int public function getId(): ?int
@@ -70,6 +77,36 @@ class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface
return $this->id; return $this->id;
} }
/**
* @return Collection<int, Devis>
*/
public function getDevis(): Collection
{
return $this->devis;
}
public function addDevi(Devis $devi): static
{
if (!$this->devis->contains($devi)) {
$this->devis->add($devi);
$devi->setPrestataire($this);
}
return $this;
}
public function removeDevi(Devis $devi): static
{
if ($this->devis->removeElement($devi)) {
// set the owning side to null (unless already changed)
if ($devi->getPrestataire() === $this) {
$devi->setPrestataire(null);
}
}
return $this;
}
/** /**
* @return Collection<int, OrderSession> * @return Collection<int, OrderSession>
*/ */

View File

@@ -141,10 +141,15 @@
<div> <div>
<span class="block text-[9px] font-black uppercase tracking-widest {{ acompteOk ? 'text-emerald-500' : 'text-rose-500' }}">Acompte</span> <span class="block text-[9px] font-black uppercase tracking-widest {{ acompteOk ? 'text-emerald-500' : 'text-rose-500' }}">Acompte</span>
{% if not acompteOk %} {% if not acompteOk %}
<a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, type: 'accompte'}) }}" <div class="mt-3 flex flex-wrap justify-center gap-2 max-w-[200px]">
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"> {% for method in ['Carte Bancaire', 'Chèque', 'Espèces', 'Virement'] %}
Marquer réglé <a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, type: 'accompte', method: method}) }}"
</a> class="px-2 py-1.5 bg-rose-500/10 hover:bg-rose-500/20 border border-rose-500/20 rounded-lg text-[9px] font-bold text-rose-500 uppercase transition-all"
title="Régler par {{ method }}">
{{ method|slice(0, 4) }}.
</a>
{% endfor %}
</div>
{% else %} {% else %}
<p class="mt-2 text-[10px] text-emerald-500/60 font-bold uppercase italic">Encaissé</p> <p class="mt-2 text-[10px] text-emerald-500/60 font-bold uppercase italic">Encaissé</p>
{% endif %} {% endif %}
@@ -200,10 +205,15 @@
{# 3. SINON, C'EST QU'ELLE N'EST PAS ENCORE ENREGISTRÉE #} {# 3. SINON, C'EST QU'ELLE N'EST PAS ENCORE ENREGISTRÉE #}
{% else %} {% else %}
<a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, type: 'caution'}) }}" <div class="flex flex-wrap justify-center gap-2 max-w-[200px]">
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"> {% for method in ['Carte Bancaire', 'Chèque', 'Espèces', 'Virement'] %}
Marquer reçue <a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, type: 'caution', method: method}) }}"
</a> class="px-2 py-1.5 bg-rose-500/10 hover:bg-rose-500/20 border border-rose-500/20 rounded-lg text-[9px] font-bold text-rose-500 uppercase transition-all"
title="Caution par {{ method }}">
{{ method|slice(0, 4) }}.
</a>
{% endfor %}
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -222,10 +232,15 @@
<div> <div>
<span class="block text-[9px] font-black uppercase tracking-widest {{ soldeOk ? 'text-emerald-500' : 'text-rose-500' }}">Solde Final</span> <span class="block text-[9px] font-black uppercase tracking-widest {{ soldeOk ? 'text-emerald-500' : 'text-rose-500' }}">Solde Final</span>
{% if not soldeOk %} {% if not soldeOk %}
<a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, type: 'solde'}) }}" <div class="mt-3 flex flex-wrap justify-center gap-2 max-w-[200px]">
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"> {% for method in ['Carte Bancaire', 'Chèque', 'Espèces', 'Virement'] %}
Régler le solde <a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: contrat.id, type: 'solde', method: method}) }}"
</a> class="px-2 py-1.5 bg-rose-500/10 hover:bg-rose-500/20 border border-rose-500/20 rounded-lg text-[9px] font-bold text-rose-500 uppercase transition-all"
title="Solde par {{ method }}">
{{ method|slice(0, 4) }}.
</a>
{% endfor %}
</div>
{% else %} {% else %}
<p class="mt-2 text-[10px] text-emerald-500/60 font-bold uppercase italic">Totalité payée</p> <p class="mt-2 text-[10px] text-emerald-500/60 font-bold uppercase italic">Totalité payée</p>
{% endif %} {% endif %}

View File

@@ -107,6 +107,36 @@
<hr class="border-white/5"> <hr class="border-white/5">
{# --- BLOC 04.5 : LIVRAISON & PAIEMENT --- #}
<div class="mt-8 backdrop-blur-xl bg-slate-900/40 border border-white/5 rounded-[2.5rem] p-8 shadow-2xl">
<h3 class="text-sm font-black text-indigo-500 uppercase tracking-widest mb-8 flex items-center justify-center">
<span class="w-6 h-6 bg-indigo-600/20 rounded-lg flex items-center justify-center mr-3 text-[10px]">05</span>
Logistique & Paiement
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
<div>
<label class="block text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] mb-3 ml-2">Mode de paiement</label>
<select name="devis[paymentMethod]" class="w-full bg-slate-900/60 border border-white/10 rounded-2xl px-5 py-4 text-sm text-white outline-none focus:border-indigo-500/50 focus:bg-slate-900/90 transition-all duration-300">
<option value="">Sélectionner...</option>
{% for type in ['Paiement Via Chorus', 'Paiement En ligne', 'Paiement Après événement', 'Autre mode de paiement'] %}
<option value="{{ type }}" {% if devis.paymentMethod == type %}selected{% endif %}>{{ type }}</option>
{% endfor %}
</select>
</div>
<div>
<label class="block text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] mb-3 ml-2">Prestataire (Livraison)</label>
<select name="devis[prestataire]" class="w-full bg-slate-900/60 border border-white/10 rounded-2xl px-5 py-4 text-sm text-white outline-none focus:border-indigo-500/50 focus:bg-slate-900/90 transition-all duration-300">
<option value="">-- Aucun prestataire assigné --</option>
{% for p in prestataires %}
<option value="{{ p.id }}" {% if devis.prestataire and devis.prestataire.id == p.id %}selected{% endif %}>
{{ p.surname }} {{ p.name }} ({{ p.email }})
</option>
{% endfor %}
</select>
</div>
</div>
</div>
{# SECTION REPEATER #} {# SECTION REPEATER #}
<div class="form-repeater" data-component="repeater" is="repeat-line"> <div class="form-repeater" data-component="repeater" is="repeat-line">
<div class="flex items-center justify-between mb-6 px-4"> <div class="flex items-center justify-between mb-6 px-4">

View File

@@ -178,6 +178,22 @@
<p class="text-3xl font-black text-slate-900 italic tracking-tighter">{{ totalTTC|number_format(2, ',', ' ') }}€</p> <p class="text-3xl font-black text-slate-900 italic tracking-tighter">{{ totalTTC|number_format(2, ',', ' ') }}€</p>
</div> </div>
{% endif %} {% endif %}
{# INFO CAUTION #}
{% if totalCaution > 0 %}
<div class="bg-amber-50 rounded-[1.5rem] p-6 border border-amber-100 shadow-sm flex items-start gap-4">
<div class="p-2 bg-amber-100 rounded-lg text-amber-600 shrink-0">
<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="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>
</div>
<div>
<p class="text-[10px] font-black text-amber-500 uppercase tracking-widest mb-1">Information Caution</p>
<p class="text-xs font-bold text-slate-600 leading-relaxed">
Un chèque de caution vous sera demandé le jour de livraison d'un montant de <span class="text-amber-600 font-black">{{ totalCaution|number_format(2, ',', ' ') }}€</span>.
</p>
</div>
</div>
{% endif %}
{# SOLDE FINAL - Bloc Principal avec saisie du montant #} {# 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="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> <div class="absolute top-0 right-0 -mt-4 -mr-4 w-24 h-24 bg-blue-600/10 rounded-full blur-2xl"></div>
@@ -240,8 +256,9 @@
</div> </div>
{% endif %} {% endif %}
{# 2. ACOMPTE #} {% if not (contrat.devis and 'Chorus' in contrat.devis.paymentMethod) %}
{% if not contratPaymentPay(contrat, 'accompte') %} {# 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-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="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> <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>
@@ -284,6 +301,64 @@
</div> </div>
{% endif %} {% endif %}
{# 3. SOLDE #}
{% if solde > 0 %}
<div class="bg-white rounded-[2rem] border border-blue-100 shadow-xl shadow-blue-100/20 overflow-hidden">
<div class="bg-blue-600 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 2M12 3v1m0 16v1m-9-9h1.79A2.76 2.76 0 005.255 12h13.49a2.76 2.76 0 002.465-1.79H23"></path></svg></div>
<p class="text-sm font-black uppercase italic leading-none">Solde restant</p>
</div>
<div class="p-6 text-center">
<p class="text-3xl font-black text-slate-900 italic mb-4">{{ solde|number_format(2, ',', ' ') }}€</p>
{% if contrat.signed and contratPaymentPay(contrat, 'accompte') %}
<form data-turbo="false" action="{{ path('gestion_contrat_view', {'num': contrat.numReservation}) }}" method="get">
<input type="hidden" name="act" value="soldePay">
<div class="mb-4">
<label for="amountToPay" class="block text-[9px] font-black text-slate-400 uppercase tracking-widest mb-2 text-left">Montant à régler</label>
<div class="relative">
<input type="number" step="0.01" min="1" max="{{ solde }}" name="amountToPay" id="amountToPay" value="{{ solde }}"
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-3 text-sm font-bold text-slate-800 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all text-center">
<div class="absolute right-4 top-3 text-slate-400 font-bold text-sm">€</div>
</div>
</div>
<button type="submit" 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 le solde
</button>
</form>
{% else %}
<div class="p-3 bg-slate-50 rounded-xl border border-slate-100">
<span class="text-[9px] text-slate-400 font-black uppercase tracking-tighter">
{% if not contrat.signed %}Attente signature{% else %}Attente acompte{% endif %}
</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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg></div>
<p class="text-sm font-black uppercase italic leading-none">Solde Réglé</p>
</div>
<div class="p-5">
{% for payment in paymentCtaList %}
<div class="bg-slate-50 p-4 rounded-2xl border border-slate-100 mb-2 last:mb-0">
<div class="flex justify-between items-center mb-1">
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest">Paiement</p>
<p class="text-sm font-black text-slate-900 italic leading-none">{{ payment.amount|number_format(2, ',', ' ') }}€</p>
</div>
<p class="text-[8px] text-slate-400 font-medium italic uppercase tracking-tighter">Le {{ payment.validateAt|date('d/m/Y') }}</p>
</div>
{% endfor %}
<div class="mt-4 p-3 bg-green-50 rounded-xl border border-green-100 text-center">
<span class="text-[10px] font-black text-green-600 uppercase tracking-tight">Dossier à jour</span>
</div>
</div>
</div>
{% endif %}
{% endif %}
</div> </div>
{# ... (Garder tout le code précédent inchangé jusqu'à la fin de la grille 3 colonnes) ... #} {# ... (Garder tout le code précédent inchangé jusqu'à la fin de la grille 3 colonnes) ... #}