```
✨ feat(contrat): Améliore l'affichage des contrats et ajoute suivi des paiements.
Cette commit améliore l'affichage des contrats dans le dashboard,
ajoute le suivi des paiements et corrige des bugs mineurs.
```
This commit is contained in:
@@ -20,6 +20,7 @@ security:
|
||||
|
||||
# --- NOUVEAU FIREWALL DÉDIÉ CLIENTS ---
|
||||
customer_reservation:
|
||||
pattern: ^/reservation
|
||||
provider: reserve_account_provider # Force l'entité Customer ici
|
||||
custom_authenticator: App\Security\CustomerAuthenticator
|
||||
user_checker: App\Security\UserChecker # Si vous voulez vérifier l'activation du compte
|
||||
@@ -61,7 +62,7 @@ security:
|
||||
access_control:
|
||||
- { path: ^/2fa, roles: PUBLIC_ACCESS }
|
||||
# Protection de l'espace client (Firewall customer_reservation)
|
||||
- { path: ^/gestion-contrat, roles: [ROLE_CUSTOMER] }
|
||||
- { path: ^/espace-contrat, roles: [ROLE_CUSTOMER] }
|
||||
# Protection du CRM (Firewall main)
|
||||
- { path: ^/crm, roles: [ROLE_ADMIN] }
|
||||
- { path: ^/, roles: PUBLIC_ACCESS }
|
||||
|
||||
@@ -53,3 +53,7 @@ vich_uploader:
|
||||
uri_prefix: /pdf/payment_confirmed_signed
|
||||
upload_destination: '%kernel.project_dir%/public/pdf/payment_confirmed_signed'
|
||||
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
|
||||
facture:
|
||||
uri_prefix: /pdf/facture
|
||||
upload_destination: '%kernel.project_dir%/public/pdf/facture'
|
||||
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
|
||||
|
||||
37
migrations/Version20260123090629.php
Normal file
37
migrations/Version20260123090629.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?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 Version20260123090629 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('CREATE TABLE facture (id SERIAL NOT NULL, contrat_id INT DEFAULT NULL, num VARCHAR(255) NOT NULL, create_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, update_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_FE8664101823061F ON facture (contrat_id)');
|
||||
$this->addSql('COMMENT ON COLUMN facture.create_at IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN facture.update_at IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('ALTER TABLE facture ADD CONSTRAINT FK_FE8664101823061F FOREIGN KEY (contrat_id) REFERENCES contrats (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
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 facture DROP CONSTRAINT FK_FE8664101823061F');
|
||||
$this->addSql('DROP TABLE facture');
|
||||
}
|
||||
}
|
||||
@@ -130,14 +130,11 @@ class ContratController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/reservation/gestion-contrat/{num}', name: 'gestion_contrat_view')]
|
||||
#[Route('/gestion-contrat/{num}', name: 'gestion_contrat_view')]
|
||||
public function gestionContratView(string $num,UploaderHelper $uploaderHelper,KernelInterface $kernel,\App\Service\Stripe\Client $stripeClient,Client $client,Mailer $mailer,EntityManagerInterface $entityManager, Request $request, ContratsRepository $contratsRepository): Response
|
||||
{
|
||||
$contrat = $contratsRepository->findOneBy(['numReservation' => $num]);
|
||||
|
||||
if($this->getUser()==null){
|
||||
return $this->redirectToRoute('reservation_login');
|
||||
}
|
||||
|
||||
if (null === $contrat) {
|
||||
return $this->render('reservation/contrat/nofound.twig');
|
||||
@@ -164,6 +161,10 @@ class ContratController extends AbstractController
|
||||
$request->getSession()->set('num_reservation', $num);
|
||||
|
||||
return $this->redirectToRoute('gestion_contrat_finish');
|
||||
} else {
|
||||
if($this->getUser()==null){
|
||||
return $this->redirectToRoute('reservation_login');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->isGranted('ROLE_ROOT') && $this->getUser()->getId() !== $contrat->getCustomer()->getId()) {
|
||||
@@ -349,7 +350,7 @@ class ContratController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/reservation/gestion-contrat/configuration', name: 'gestion_contrat_finish', priority: 5)]
|
||||
#[Route('/gestion-contrat/configuration', name: 'gestion_contrat_finish', priority: 5)]
|
||||
public function gestionContratConfig(
|
||||
Request $request,
|
||||
EntityManagerInterface $em,
|
||||
@@ -392,7 +393,7 @@ class ContratController extends AbstractController
|
||||
return $this->render('reservation/contrat/finish_config.twig', ['customer' => $customer]);
|
||||
}
|
||||
|
||||
#[Route('/reservation/espace-contrat', name: 'gestion_contrat', priority: 5)]
|
||||
#[Route('/espace-contrat', name: 'gestion_contrat', priority: 5)]
|
||||
public function gestionContrat(
|
||||
Request $request,
|
||||
EntityManagerInterface $em,
|
||||
|
||||
@@ -126,6 +126,8 @@ class Contrats
|
||||
private ?string $devisAuditFileName = null;
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $devisAuditFileSize = null;
|
||||
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?\DateTimeImmutable $updateAt = null;
|
||||
|
||||
@@ -135,6 +137,9 @@ class Contrats
|
||||
#[ORM\OneToMany(targetEntity: ProductReserve::class, mappedBy: 'contrat')]
|
||||
private Collection $productReserves;
|
||||
|
||||
#[ORM\OneToOne(mappedBy: 'contrat', cascade: ['persist', 'remove'])]
|
||||
private ?Facture $facture = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->contratsPayments = new ArrayCollection();
|
||||
@@ -788,6 +793,28 @@ class Contrats
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFacture(): ?Facture
|
||||
{
|
||||
return $this->facture;
|
||||
}
|
||||
|
||||
public function setFacture(?Facture $facture): static
|
||||
{
|
||||
// unset the owning side of the relation if necessary
|
||||
if ($facture === null && $this->facture !== null) {
|
||||
$this->facture->setContrat(null);
|
||||
}
|
||||
|
||||
// set the owning side of the relation if necessary
|
||||
if ($facture !== null && $facture->getContrat() !== $this) {
|
||||
$facture->setContrat($this);
|
||||
}
|
||||
|
||||
$this->facture = $facture;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
80
src/Entity/Facture.php
Normal file
80
src/Entity/Facture.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\FactureRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: FactureRepository::class)]
|
||||
class Facture
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\OneToOne(inversedBy: 'facture', cascade: ['persist', 'remove'])]
|
||||
private ?Contrats $contrat = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $num = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $createAt = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $updateAt = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getContrat(): ?Contrats
|
||||
{
|
||||
return $this->contrat;
|
||||
}
|
||||
|
||||
public function setContrat(?Contrats $contrat): static
|
||||
{
|
||||
$this->contrat = $contrat;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNum(): ?string
|
||||
{
|
||||
return $this->num;
|
||||
}
|
||||
|
||||
public function setNum(string $num): static
|
||||
{
|
||||
$this->num = $num;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreateAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->createAt;
|
||||
}
|
||||
|
||||
public function setCreateAt(\DateTimeImmutable $createAt): static
|
||||
{
|
||||
$this->createAt = $createAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUpdateAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->updateAt;
|
||||
}
|
||||
|
||||
public function setUpdateAt(\DateTimeImmutable $updateAt): static
|
||||
{
|
||||
$this->updateAt = $updateAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
43
src/Repository/FactureRepository.php
Normal file
43
src/Repository/FactureRepository.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Facture;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Facture>
|
||||
*/
|
||||
class FactureRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Facture::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Facture[] Returns an array of Facture objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('f')
|
||||
// ->andWhere('f.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('f.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?Facture
|
||||
// {
|
||||
// return $this->createQueryBuilder('f')
|
||||
// ->andWhere('f.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
@@ -34,6 +34,7 @@ class KeycloakAuthenticator extends OAuth2Authenticator implements Authenticatio
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
|
||||
// match the route name from the controller
|
||||
return $request->attributes->get('_route') === 'connect_keycloak_check';
|
||||
}
|
||||
|
||||
@@ -271,15 +271,17 @@ class Client
|
||||
return $submiter['uuid']; // numéro de signature;
|
||||
}
|
||||
|
||||
public function autoSignConfirmedPayment(ContratsPayments $contratsPayments) {
|
||||
public function autoSignConfirmedPayment(ContratsPayments $contratsPayments)
|
||||
{
|
||||
$relativeFileUrl = $this->storage->resolveUri($contratsPayments, 'paymentFile');
|
||||
$fileUrl = $this->baseUrl . $relativeFileUrl;
|
||||
$fileUrl = $this->baseUrl . $relativeFileUrl;
|
||||
|
||||
$submission = $this->docuseal->createSubmissionFromPdf([
|
||||
'name' => 'Confirmaton de paiement N°' . $contratsPayments->getPaymentId(), // Correction : getNum()
|
||||
'name' => 'Confirmation de paiement N°' . $contratsPayments->getPaymentId(),
|
||||
'send_email' => true,
|
||||
'documents' => [
|
||||
[
|
||||
'name' => 'confirmation_paiement_' . $contratsPayments->getId() . '.pdf', // Correction : getNum()
|
||||
'name' => 'confirmation_paiement_' . $contratsPayments->getId() . '.pdf',
|
||||
'file' => $fileUrl,
|
||||
],
|
||||
],
|
||||
@@ -289,14 +291,34 @@ class Client
|
||||
'email' => 'contact@ludikevent.fr',
|
||||
'completed' => true,
|
||||
'fields' => [
|
||||
['name'=>'Sign','default_value'=>$this->logoBase64()]
|
||||
['name' => 'Sign', 'default_value' => $this->logoBase64()]
|
||||
]
|
||||
],
|
||||
],
|
||||
]);
|
||||
$sub = $this->docuseal->getSubmission($submission['id']);
|
||||
sleep(5);
|
||||
return $sub['documents'][0]['url'];
|
||||
|
||||
// --- SYSTÈME DE VÉRIFICATION DYNAMIQUE ---
|
||||
$maxAttempts = 10; // On essaie 10 fois maximum
|
||||
$attempts = 0;
|
||||
$documentUrl = null;
|
||||
|
||||
while ($attempts < $maxAttempts) {
|
||||
$sub = $this->docuseal->getSubmission($submission['id']);
|
||||
|
||||
// Vérification si le document et son URL sont prêts
|
||||
if (!empty($sub['documents'][0]['url'])) {
|
||||
$documentUrl = $sub['documents'][0]['url'];
|
||||
break; // On sort de la boucle dès que c'est prêt
|
||||
}
|
||||
|
||||
$attempts++;
|
||||
usleep(500000); // Attend 0.5 seconde avant de réessayer (plus rapide que sleep(1))
|
||||
}
|
||||
|
||||
if (!$documentUrl) {
|
||||
throw new \Exception("Docuseal n'a pas généré le document signé à temps pour le paiement " . $contratsPayments->getId());
|
||||
}
|
||||
|
||||
return $documentUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,93 +21,111 @@
|
||||
|
||||
<div id="contratsList" class="space-y-6">
|
||||
{% for contrat in contrats %}
|
||||
{# CALCUL DES ÉTATS FINANCIERS VIA TES FILTRES TWIG #}
|
||||
{% set acompteOk = contratPaymentPay(contrat, 'accompte') %}
|
||||
{% set cautionOk = contratPaymentPay(contrat, 'caution') %}
|
||||
{% 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="grid grid-cols-1 lg:grid-cols-12">
|
||||
|
||||
{# --- COLONNE 1 : NUMÉRO & STATUS --- #}
|
||||
{# --- 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 %}
|
||||
<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">
|
||||
<svg class="w-2 h-2" 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>
|
||||
<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>
|
||||
Signé
|
||||
</span>
|
||||
{% 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">
|
||||
<svg class="w-2 h-2" 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>
|
||||
<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>
|
||||
En attente
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- COLONNE 2 : CLIENT --- #}
|
||||
<div class="lg:col-span-4 p-8 border-r border-white/5">
|
||||
{# --- COLONNE 2 : CLIENT (3/12) --- #}
|
||||
<div class="lg:col-span-3 p-8 border-r border-white/5">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-10 h-10 bg-blue-600/10 rounded-xl flex items-center justify-center text-blue-500 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="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>
|
||||
<div>
|
||||
<span class="text-[9px] font-black text-slate-500 uppercase tracking-widest mb-1 block">Locataire</span>
|
||||
<p class="text-white font-bold text-lg uppercase italic text-search">{{ contrat.customer.surname }} {{ contrat.customer.name }}</p>
|
||||
<div class="mt-2 space-y-1">
|
||||
<p class="text-slate-400 text-xs flex items-center gap-2">
|
||||
<svg class="w-3 h-3 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"></path></svg>
|
||||
{{ contrat.customer.email }}
|
||||
</p>
|
||||
<p class="text-slate-400 text-xs flex items-center gap-2">
|
||||
<svg class="w-3 h-3 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"></path></svg>
|
||||
{{ contrat.customer.phone }}
|
||||
</p>
|
||||
<p class="text-white font-bold text-base uppercase italic text-search">{{ contrat.customer.surname }} {{ contrat.customer.name }}</p>
|
||||
<p class="text-slate-400 text-[11px] mt-1">{{ contrat.customer.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- COLONNE 3 : LIEU & VILLE (2/12) --- #}
|
||||
<div class="lg:col-span-2 p-8 border-r border-white/5">
|
||||
<span class="text-[9px] font-black text-slate-500 uppercase tracking-widest mb-2 block">Lieu</span>
|
||||
<p class="text-white font-bold text-sm leading-tight text-search">
|
||||
{{ contrat.townEvent }}
|
||||
</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>
|
||||
|
||||
{# --- COLONNE 3 : ÉVÉNEMENT --- #}
|
||||
<div class="lg:col-span-4 p-8 border-r border-white/5">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-10 h-10 bg-amber-500/10 rounded-xl flex items-center justify-center text-amber-500 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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-[9px] font-black text-slate-500 uppercase tracking-widest mb-1 block">Lieu de l'événement</span>
|
||||
<p class="text-white font-medium text-sm leading-relaxed">
|
||||
{{ contrat.addressEvent }}<br>
|
||||
<span class="text-amber-500 font-bold tracking-wider text-search">{{ contrat.zipCodeEvent }}</span>
|
||||
<span class="text-white font-black uppercase italic text-search">{{ contrat.townEvent }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- COLONNE 4 : ACTIONS (2/12) --- #}
|
||||
{# --- COLONNE 5 : ACTIONS (2/12) --- #}
|
||||
<div class="lg:col-span-2 p-6 flex flex-row lg:flex-col justify-center gap-2">
|
||||
{# VOIR #}
|
||||
<a href="{{ path('app_crm_contrats', {id: contrat.id}) }}"
|
||||
title="Voir les 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 group/btn border border-white/5 shadow-lg shadow-black/20">
|
||||
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>
|
||||
|
||||
{# TÉLÉCHARGER #}
|
||||
<a download="contrat N°{{ contrat.numReservation }}"
|
||||
<a download="contrat-{{ contrat.numReservation }}"
|
||||
href="{{ vich_uploader_asset(contrat,'devisFile') }}"
|
||||
title="Télécharger le PDF"
|
||||
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 shadow-lg shadow-black/20">
|
||||
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">
|
||||
<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>
|
||||
</a>
|
||||
|
||||
{# ENVOYER PAR EMAIL #}
|
||||
<a href="{{ path('app_crm_contrats', {idSend: contrat.id}) }}"
|
||||
title="Envoyer le contrat au client"
|
||||
onclick="return confirm('Souhaitez-vous envoyer ce contrat par email à {{ contrat.customer.email }} ?')"
|
||||
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 shadow-lg shadow-black/20">
|
||||
<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>
|
||||
onclick="return confirm('Envoyer à {{ contrat.customer.email }} ?')"
|
||||
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">
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,5 +136,5 @@
|
||||
|
||||
{{ knp_pagination_render(contrats) }}
|
||||
|
||||
{# --- JS SIMPLE POUR LA RECHERCHE INSTANTANÉE --- #}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -129,6 +129,9 @@
|
||||
<span class="text-xs font-black uppercase tracking-widest text-slate-600 italic">Certificat</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if solde <=0 %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user