feat(facture): Ajoute la gestion des factures et paiements (CRUD, export).

Cette commit ajoute la fonctionnalité de gestion des factures et des paiements,
incluant l'affichage, la recherche, l'export Excel et la pagination.
```
This commit is contained in:
Serreau Jovann
2026-01-29 13:05:08 +01:00
parent d0d2e73e78
commit 61af0fd0dc
9 changed files with 674 additions and 120 deletions

View File

@@ -2,25 +2,179 @@
namespace App\Controller\Dashboard;
use App\Entity\ContratsPayments;
use App\Logger\AppLogger;
use App\Repository\AccountRepository;
use App\Repository\ContratsPaymentsRepository;
use Knp\Component\Pager\PaginatorInterface;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Color;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Attribute\Route;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
class FactureController extends AbstractController
{
/**
* Liste des administrateurs
*/
#[Route(path: '/crm/facture', name: 'app_crm_facture', options: ['sitemap' => false], methods: ['GET'])]
public function contrats(AccountRepository $accountRepository, AppLogger $appLogger): Response
{
$appLogger->record('VIEW', 'Consultation de la liste des facture');
return $this->render('dashboard/contrats/facture.twig',[
'factures' => [],
#[Route(path: '/crm/facture', name: 'app_crm_facture', methods: ['GET'])]
public function index(
Request $request,
ContratsPaymentsRepository $contratsPaymentsRepo,
AppLogger $appLogger,
UploaderHelper $uploaderHelper,
PaginatorInterface $paginator
): Response {
// 1. Gestion des dates par défaut (Début et Fin du mois en cours)
$startDateStr = $request->query->get('startDate');
$endDateStr = $request->query->get('endDate');
$searchTerm = $request->query->get('q', '');
if (!$startDateStr) {
$startDate = new \DateTime('first day of this month 00:00:00');
$startDateStr = $startDate->format('Y-m-d');
} else {
$startDate = new \DateTime($startDateStr . ' 00:00:00');
}
if (!$endDateStr) {
$endDate = new \DateTime('last day of this month 23:59:59');
$endDateStr = $endDate->format('Y-m-d');
} else {
$endDate = new \DateTime($endDateStr . ' 23:59:59');
}
// 2. Construction de la requête de recherche (QueryBuilder)
// On récupère une instance de QueryBuilder pour la pagination ou l'export
$queryBuilder = $contratsPaymentsRepo->createQueryBuilder('p')
->leftJoin('p.contrat', 'c')
->leftJoin('c.customer', 'u')
->where('p.paymentAt BETWEEN :start AND :end')
->setParameter('start', $startDate)
->setParameter('end', $endDate)
->orderBy('p.paymentAt', 'DESC');
if (!empty($searchTerm)) {
$queryBuilder->andWhere('u.name LIKE :q OR u.surname LIKE :q OR c.numReservation LIKE :q OR p.type LIKE :q')
->setParameter('q', '%' . $searchTerm . '%');
}
// 3. Gestion de l'extraction Excel
if ($request->query->has('extract')) {
// Pour l'export, on récupère tous les résultats filtrés sans pagination
$allFilteredPayments = $queryBuilder->getQuery()->getResult();
// On ne garde que les paiements complétés pour l'export comptable
$dataToExport = array_filter($allFilteredPayments, function($p) {
return $p->getState() === 'complete';
});
if (empty($dataToExport)) {
$this->addFlash('warning', 'Aucune donnée validée à exporter pour cette période.');
return $this->redirectToRoute('app_crm_facture', [
'startDate' => $startDateStr,
'endDate' => $endDateStr,
'q' => $searchTerm
]);
}
$appLogger->record('EXPORT_EXCEL', "Export des factures du {$startDateStr} au {$endDateStr}");
return $this->generateExcelExport(
$dataToExport,
$uploaderHelper,
$request->getSchemeAndHttpHost(),
$startDateStr,
$endDateStr
);
}
// 4. Pagination des résultats pour l'affichage Web
$pagination = $paginator->paginate(
$queryBuilder, // Query object, pas le résultat final
$request->query->getInt('page', 1), // Numéro de page
15 // Nombre d'éléments par page
);
return $this->render('dashboard/contrats/facture.twig', [
'pagination' => $pagination,
'startDate' => $startDateStr,
'endDate' => $endDateStr,
'searchTerm' => $searchTerm,
'active' => $request->query->get('active', 'facture'),
]);
}
/**
* Génère un fichier Excel XLSX formaté
*/
private function generateExcelExport(array $payments, UploaderHelper $uploaderHelper, string $host, string $start, string $end): StreamedResponse
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('Export Compta');
// Styles des en-têtes
$headers = [
'A1' => 'DATE PAIEMENT',
'B1' => 'NOM DU CLIENT',
'C1' => 'RÉF. RÉSERVATION',
'D1' => 'MODE DE PAIEMENT',
'E1' => 'MONTANT TTC',
'F1' => 'STATUT',
'G1' => 'LIEN JUSTIFICATIF'
];
foreach ($headers as $cell => $label) {
$sheet->setCellValue($cell, $label);
$sheet->getStyle($cell)->getFont()->setBold(true)->setColor(new Color('FFFFFF'));
$sheet->getStyle($cell)->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setARGB('4F46E5');
}
$row = 2;
foreach ($payments as $payment) {
$contrat = $payment->getContrat();
$customer = $contrat ? $contrat->getCustomer() : null;
$sheet->setCellValue('A' . $row, $payment->getPaymentAt() ? $payment->getPaymentAt()->format('d/m/Y H:i') : '');
$sheet->setCellValue('B' . $row, $customer ? strtoupper((string)$customer->getName()) . ' ' . $customer->getSurname() : 'Inconnu');
$sheet->setCellValue('C' . $row, $contrat ? $contrat->getNumReservation() : 'N/A');
$sheet->setCellValue('D' . $row, strtoupper((string)$payment->getType()));
$sheet->setCellValue('E' . $row, $payment->getAmount());
$sheet->getStyle('E' . $row)->getNumberFormat()->setFormatCode('#,##0.00" €"');
$sheet->setCellValue('F' . $row, $payment->getState());
// Gestion du lien vers le fichier justificatif (VichUploader)
$assetPath = $uploaderHelper->asset($payment, 'paymentFile');
if ($assetPath) {
$sheet->setCellValue('G' . $row, 'Voir le document');
$sheet->getCell('G' . $row)->getHyperlink()->setUrl($host . $assetPath);
$sheet->getStyle('G' . $row)->getFont()->setUnderline(true)->setColor(new Color(Color::COLOR_BLUE));
} else {
$sheet->setCellValue('G' . $row, 'Aucun fichier');
}
$row++;
}
// Auto-size des colonnes
foreach (range('A', 'G') as $col) {
$sheet->getColumnDimension($col)->setAutoSize(true);
}
$writer = new Xlsx($spreadsheet);
$response = new StreamedResponse(function() use ($writer) {
$writer->save('php://output');
});
$filename = "compta_export_{$start}_au_{$end}.xlsx";
$response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
$response->headers->set('Content-Disposition', 'attachment;filename="' . $filename . '"');
$response->headers->set('Cache-Control', 'max-age=0');
return $response;
}
}

View File

@@ -40,4 +40,13 @@ class ContratsPaymentsRepository extends ServiceEntityRepository
// ->getOneOrNullResult()
// ;
// }
public function findByDateRange(\DateTime $startDate, \DateTime $endDate)
{
$this->createQueryBuilder('a')
->andWhere('a.paymentAt BETWEEN :startDate AND :endDate')
->setParameter('startDate', $startDate)
->setParameter('endDate', $endDate)
->getQuery()
->getResult();
}
}

View File

@@ -27,6 +27,8 @@ class StripeExtension extends AbstractExtension
new TwigFilter('totalQuoto',[$this,'totalQuoto']),
new TwigFilter('totalQuotoAccompte', [$this, 'totalQuotoAccompte']),
new TwigFilter('totalContrat',[$this,'totalContrat']),
new TwigFilter('totalPayContrat',[$this,'totalPayContrat']),
new TwigFilter('totalRestpayContrat',[$this,'totalRestpayContrat']),
new TwigFilter('totalContratAccompte',[$this,'totalContratAccompte']),
new TwigFilter('devisSignUrl',[$this,'devisSignUrl']),
];
@@ -89,7 +91,8 @@ class StripeExtension extends AbstractExtension
// 1. Calcul des lignes de produits (Location)
foreach ($devis->getDevisLines() as $line) {
$totalHT = $totalHT + $line->getProduct()->getCaution();
$p = $this->em->getRepository(Product::class)->findOneBy(['name'=>$line->getProduct()]);
$totalHT = $totalHT + $p->getCaution();
}
@@ -97,6 +100,28 @@ class StripeExtension extends AbstractExtension
return (float) $totalHT;
}
public function totalPayContrat(Contrats $contrats)
{
$total = 0;
foreach ($contrats->getContratsPayments() as $contratsPayment) {
if($contratsPayment->getState() == "complete")
$total += $contratsPayment->getAmount();
}
return $total;
}
public function totalRestpayContrat(Contrats $contrats)
{
$total = 0;
foreach ($contrats->getContratsPayments() as $contratsPayment) {
if($contratsPayment->getState() == "complete" && $contratsPayment->getType() != "caution" && $contratsPayment->getType() != "accompte")
$total += $contratsPayment->getAmount();
}
$totalA = $this->totalContrat($contrats);
return $totalA - $total ;
}
public function totalContrat(Contrats $devis): float
{
$totalHT = 0;

View File

@@ -44,10 +44,10 @@
{{ menu.nav_link(path('app_crm_reservation'), 'Planing de réservation', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_crm_reservation') }}
{{ menu.nav_link(path('app_crm_product'), 'Produits', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_crm_product') }}
{{ menu.nav_link(path('app_crm_formules'), 'Formules', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_crm_formules') }}
{# {{ menu.nav_link(path('app_crm_contrats'), 'Contrat de location', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_clients') }}#}
{# {{ menu.nav_link(path('app_crm_facture'), 'Facture', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_clients') }}#}
{# {{ menu.nav_link(path('app_crm_devis'), 'Devis', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_clients') }}#}
{# {{ menu.nav_link(path('app_crm_customer'), 'Clients', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_clients') }}#}
{# {{ menu.nav_link(path('app_crm_contrats'), 'Contrat de location', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_crm_contrats') }}#}
{{ menu.nav_link(path('app_crm_facture'), 'Facture', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_crm_facture') }}
{# {{ menu.nav_link(path('app_crm_devis'), 'Devis', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_crm_devis') }}#}
{{ menu.nav_link(path('app_crm_customer'), 'Clients', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_clients') }}
</div>
</div>

View File

@@ -1 +1,257 @@
{% extends 'dashboard/base.twig' %}
{% block title %}Facturation & Paiements{% endblock %}
{% block title_header %}Gestion <span class="text-blue-500">Financière</span>{% endblock %}
{% block body %}
<div class="w-full max-w-full mx-auto space-y-8 animate-in fade-in duration-700">
{# NAVIGATION DES ONGLETS #}
<div class="flex flex-col lg:flex-row items-center justify-between gap-6 mb-8">
<div class="inline-flex p-1.5 bg-[#1e293b]/60 backdrop-blur-xl border border-white/5 rounded-[2rem] shadow-2xl">
<a data-turbo="false" href="{{ path('app_crm_facture', {active: 'facture'}) }}"
class="flex items-center px-8 py-3.5 rounded-[1.5rem] transition-all duration-500 group {{ active == 'facture' ? 'bg-blue-600 shadow-lg shadow-blue-600/20' : 'hover:bg-white/5' }}">
<svg class="w-4 h-4 mr-3 {{ active == 'facture' ? 'text-white' : 'text-slate-500 group-hover:text-blue-400' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<span class="text-[10px] font-black uppercase tracking-[0.2em] {{ active == 'facture' ? 'text-white' : 'text-slate-400 group-hover:text-white' }}">
Factures Émises
</span>
</a>
<a data-turbo="false" href="{{ path('app_crm_facture', {active: 'confirmed_paiement'}) }}"
class="flex items-center px-8 py-3.5 rounded-[1.5rem] transition-all duration-500 group {{ active == 'confirmed_paiement' ? 'bg-emerald-600 shadow-lg shadow-emerald-600/20' : 'hover:bg-white/5' }}">
<svg class="w-4 h-4 mr-3 {{ active == 'confirmed_paiement' ? 'text-white' : 'text-slate-500 group-hover:text-emerald-400' }}" 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>
<span class="text-[10px] font-black uppercase tracking-[0.2em] {{ active == 'confirmed_paiement' ? 'text-white' : 'text-slate-400 group-hover:text-white' }}">
Confirmations de paiement
</span>
</a>
</div>
{# ACTIONS RAPIDES #}
<div class="flex items-center gap-3">
{% if active == 'confirmed_paiement' %}
<a data-turbo="false" href="{{ path('app_crm_facture',{active:active,'extract':true, 'startDate': startDate, 'endDate': endDate, 'q': searchTerm}) }}"
class="px-6 py-3.5 bg-emerald-500/10 hover:bg-emerald-500 text-emerald-500 hover:text-white rounded-2xl border border-emerald-500/20 flex items-center transition-all duration-500 group shadow-xl shadow-emerald-500/5">
<svg class="w-4 h-4 mr-2.5 opacity-80 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="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<span class="text-[10px] font-black uppercase tracking-widest">Exporter .XLSX</span>
</a>
{% endif %}
<button onclick="window.location.reload()" class="px-6 py-3.5 bg-slate-800/80 hover:bg-slate-700 text-slate-300 rounded-2xl border border-white/5 flex items-center transition-all group">
<svg class="w-4 h-4 mr-2 opacity-50 group-hover:rotate-180 transition-transform duration-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<span class="text-[10px] font-black uppercase tracking-widest">Actualiser</span>
</button>
</div>
</div>
{# MOTEUR DE RECHERCHE ET FILTRES #}
<div class="bg-[#1e293b]/40 backdrop-blur-xl border border-white/5 rounded-[2rem] p-6 shadow-xl mb-6">
<form method="GET" action="{{ path('app_crm_facture') }}" class="flex flex-col md:flex-row items-end gap-4">
<input type="hidden" name="active" value="{{ active }}">
<div class="flex-1 w-full space-y-2">
<label class="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-2">Recherche globale</label>
<div class="relative">
<svg class="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" 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"/>
</svg>
<input type="text" name="q" value="{{ searchTerm }}"
placeholder="Nom, Réservation, Email..."
class="w-full bg-slate-900/50 border border-white/5 rounded-xl py-3 pl-11 pr-4 text-xs text-white placeholder:text-slate-600 focus:border-blue-500/50 focus:ring-0 transition-all">
</div>
</div>
<div class="w-full md:w-44 space-y-2">
<label class="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-2">Date début</label>
<input type="date" name="startDate" value="{{ startDate }}"
class="w-full bg-slate-900/50 border border-white/5 rounded-xl py-3 px-4 text-xs text-white focus:border-blue-500/50 focus:ring-0 transition-all">
</div>
<div class="w-full md:w-44 space-y-2">
<label class="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-2">Date fin</label>
<input type="date" name="endDate" value="{{ endDate }}"
class="w-full bg-slate-900/50 border border-white/5 rounded-xl py-3 px-4 text-xs text-white focus:border-blue-500/50 focus:ring-0 transition-all">
</div>
<button type="submit" class="bg-blue-600 hover:bg-blue-500 text-white p-3.5 rounded-xl transition-all shadow-lg shadow-blue-600/20 group">
<svg class="w-4 h-4 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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</button>
{% if searchTerm or startDate or endDate %}
<a href="{{ path('app_crm_facture', {active: active}) }}" class="bg-slate-800 hover:bg-slate-700 text-slate-400 p-3.5 rounded-xl 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="M6 18L18 6M6 6l12 12"/>
</svg>
</a>
{% endif %}
</form>
</div>
{# CONTENU DYNAMIQUE #}
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] p-8 shadow-2xl">
{% if active == 'facture' %}
{# VUE FACTURES #}
<div class="flex items-center space-x-4 mb-10">
<span class="w-8 h-px bg-blue-500/30"></span>
<span class="text-[10px] font-black text-blue-500 uppercase tracking-[0.3em]">Flux de facturation en cours</span>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left">
<thead>
<tr class="border-b border-white/5">
<th class="pb-6 text-[10px] font-black text-slate-500 uppercase tracking-widest">Date</th>
<th class="pb-6 text-[10px] font-black text-slate-500 uppercase tracking-widest">Client</th>
<th class="pb-6 text-[10px] font-black text-slate-500 uppercase tracking-widest text-right">Montant TTC</th>
<th class="pb-6 text-[10px] font-black text-slate-500 uppercase tracking-widest text-center">Statut</th>
</tr>
</thead>
<tbody class="divide-y divide-white/5 text-slate-400 italic text-xs">
<tr><td colspan="4" class="py-10 text-center uppercase tracking-widest opacity-30">Section factures en développement</td></tr>
</tbody>
</table>
</div>
{% else %}
{# VUE PAIEMENTS CONFIRMÉS #}
<div class="flex items-center space-x-4 mb-10">
<span class="w-8 h-px bg-emerald-500/30"></span>
<span class="text-[10px] font-black text-emerald-500 uppercase tracking-[0.3em]">Historique des encaissements</span>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-3">
<thead>
<tr class="text-slate-500 uppercase">
<th class="pb-4 pl-6 text-[10px] font-black tracking-[0.2em]">Date</th>
<th class="pb-4 text-[10px] font-black tracking-[0.2em]">Client</th>
<th class="pb-4 text-[10px] font-black tracking-[0.2em]">N° Réservation</th>
<th class="pb-4 text-[10px] font-black tracking-[0.2em]">Méthode</th>
<th class="pb-4 text-[10px] font-black tracking-[0.2em]">État</th>
<th class="pb-4 pr-6 text-[10px] font-black tracking-[0.2em] text-right">Preuve</th>
</tr>
</thead>
<tbody class="space-y-4">
{% for confirmedPaiement in pagination %}
<tr class="group bg-white/[0.02] hover:bg-white/[0.05] transition-all duration-300">
<td class="py-5 pl-6 rounded-l-2xl border-y border-l border-white/5">
<div class="flex flex-col">
<span class="text-xs font-bold text-white">{{ confirmedPaiement.paymentAt|date('d/m/Y') }}</span>
<span class="text-[9px] text-slate-500 font-medium">Encaissé</span>
</div>
</td>
<td class="py-5 border-y border-white/5">
<div class="flex flex-col">
<span class="text-xs font-bold text-slate-200 uppercase tracking-tight">
{{ confirmedPaiement.contrat.customer.name }} {{ confirmedPaiement.contrat.customer.surname }}
</span>
<span class="text-[10px] text-slate-500 lowercase">{{ confirmedPaiement.contrat.customer.email }}</span>
</div>
</td>
<td class="py-5 border-y border-white/5">
<span class="px-3 py-1.5 bg-slate-900/50 rounded-lg border border-white/5 text-[10px] font-mono font-bold text-blue-400">
{{ confirmedPaiement.contrat.numReservation }}
</span>
</td>
<td class="py-5 border-y border-white/5">
<div class="inline-flex items-center px-2.5 py-1 rounded-md bg-white/5 border border-white/10">
<span class="text-[9px] font-black text-slate-400 uppercase tracking-widest">{{ confirmedPaiement.type }}</span>
</div>
</td>
<td class="py-5 border-y border-white/5">
<div class="flex items-center space-x-2">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.6)]"></span>
<span class="text-[10px] font-black text-emerald-500 uppercase tracking-widest">{{ confirmedPaiement.state }}</span>
</div>
</td>
<td class="py-5 pr-6 rounded-r-2xl border-y border-r border-white/5 text-right">
{% if confirmedPaiement.paymentFileName !="" %}
<a href="{{ vich_uploader_asset(confirmedPaiement, 'paymentFile') }}"
download
class="inline-flex p-2.5 bg-emerald-500/10 hover:bg-emerald-500 text-emerald-500 hover:text-white rounded-xl transition-all duration-300 border border-emerald-500/20 group/btn"
title="Télécharger le justificatif">
<svg class="w-4 h-4 transform 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="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
</a>
{% else %}
<span class="text-[9px] font-black text-slate-600 uppercase tracking-tighter italic">Aucun fichier</span>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="py-20 text-center border-2 border-dashed border-white/5 rounded-[2rem]">
<div class="flex flex-col items-center">
<svg class="w-10 h-10 text-slate-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/>
</svg>
<p class="text-slate-500 text-[10px] font-black uppercase tracking-[0.3em]">Aucune donnée trouvée pour cette recherche</p>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# PAGINATION KNP BUNDLE #}
<div class="mt-8 flex justify-center">
<div class="knp-pagination-wrapper">
{{ knp_pagination_render(pagination) }}
</div>
</div>
{% endif %}
</div>
</div>
{# Style personnalisé pour la pagination KNP afin de correspondre au thème sombre #}
<style>
.knp-pagination-wrapper .pagination {
display: flex;
gap: 0.5rem;
align-items: center;
}
.knp-pagination-wrapper .page-item .page-link {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #94a3b8;
padding: 0.5rem 1rem;
border-radius: 0.75rem;
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
transition: all 0.3s ease;
}
.knp-pagination-wrapper .page-item.active .page-link {
background: #10b981;
color: white;
border-color: #10b981;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
}
.knp-pagination-wrapper .page-item:hover:not(.active) .page-link {
background: rgba(255, 255, 255, 0.1);
color: white;
}
/* Style spécifique pour les inputs de date (chrome/safari) */
input[type="date"]::-webkit-calendar-picker-indicator {
filter: invert(1);
opacity: 0.5;
cursor: pointer;
}
</style>
{% endblock %}

View File

@@ -18,7 +18,7 @@
{% block body %}
<div class="space-y-8 pb-20">
{# --- RECHERCHE STYLE NÉO-GLASS --- #}
{# --- RECHERCHE --- #}
<div class="relative group max-w-2xl mx-auto">
<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">
@@ -38,9 +38,10 @@
{% set acompteOk = contratPaymentPay(contrat, 'accompte') %}
{% set cautionOk = contratPaymentPay(contrat, 'caution') %}
{% set soldeOk = contratPaymentPay(contrat, 'solde') %}
{% set cautionRelase = contratPaymentPay(contrat, 'caution_free') %}
{% set cautionEncaisser = contratPaymentPay(contrat, 'caution_recup') %}
<div class="contrat-card relative overflow-hidden group">
{# 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">
@@ -65,14 +66,12 @@
{# 2. CLIENT #}
<div class="lg:col-span-3 p-8 border-b lg:border-b-0 lg:border-r border-white/5">
<div class="flex items-center gap-4">
<div class="relative">
<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">
<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 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">
<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>
<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>
<p class="text-slate-500 text-xs font-medium line-clamp-1">{{ contrat.customer.email }}</p>
</div>
</div>
</div>
@@ -80,7 +79,7 @@
{# 3. LIEU #}
<div class="lg:col-span-2 p-8 border-b lg:border-b-0 lg:border-r border-white/5">
<div class="flex flex-col">
<span class="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1 text-search">Destination</span>
<span class="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1">Destination</span>
<p class="text-slate-200 font-bold text-sm">{{ contrat.townEvent }}</p>
<p class="text-blue-500 font-black text-[11px]">{{ contrat.zipCodeEvent }}</p>
</div>
@@ -90,62 +89,54 @@
<div class="lg:col-span-3 p-8 bg-black/10 lg:bg-transparent">
<div class="grid grid-cols-3 gap-2">
{# --- ACOMPTE --- #}
{# ACOMPTE #}
<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
{{ acompteOk
? '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 %}
{{ acompteOk ? '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"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="{{ acompteOk ? 'M5 13l4 4L19 7' : 'M6 18L18 6M6 6l12 12' }}"></path></svg>
</div>
<span class="text-[8px] font-black uppercase tracking-tighter {{ acompteOk ? 'text-emerald-500' : 'text-rose-500' }}">
Acompte
</span>
<span class="text-[8px] font-black uppercase tracking-tighter {{ acompteOk ? 'text-emerald-500' : 'text-rose-500' }}">Acompte</span>
</div>
{# --- CAUTION --- #}
{# CAUTION (MULTI-ETATS) #}
<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
{{ cautionOk
? '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>
{% if cautionEncaisser %}
{# ÉTAT : RÉCUPÉRÉE / ENCAISSÉE (Litige) #}
<div class="w-8 h-8 rounded-xl flex items-center justify-center bg-rose-600/30 text-rose-400 border border-rose-500/50 shadow-[0_0_15px_rgba(225,29,72,0.3)]" title="Caution encaissée">
<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="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg>
</div>
<span class="text-[8px] font-black uppercase tracking-tighter text-rose-400">Encaissée</span>
{% elseif cautionRelase %}
{# ÉTAT : LIBÉRÉE / RENDUE #}
<div class="w-8 h-8 rounded-xl flex items-center justify-center bg-blue-500/20 text-blue-400 border border-blue-500/30" title="Caution libérée">
<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="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"></path></svg>
</div>
<span class="text-[8px] font-black uppercase tracking-tighter text-blue-400">Libérée</span>
{% elseif cautionOk %}
{# ÉTAT : DÉTENUE (Payée mais pas encore libérée) #}
<div class="w-8 h-8 rounded-xl flex items-center justify-center bg-emerald-500/20 text-emerald-400 border border-emerald-500/30" title="Caution détenue">
<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="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
</div>
<span class="text-[8px] font-black uppercase tracking-tighter text-emerald-500">Détenue</span>
{% else %}
{# ÉTAT : MANQUANTE #}
<div class="w-8 h-8 rounded-xl flex items-center justify-center bg-rose-500/10 text-rose-500 border border-rose-500/20 animate-pulse" title="Caution manquante">
<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="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"></path></svg>
</div>
<span class="text-[8px] font-black uppercase tracking-tighter text-rose-500">Requise</span>
{% endif %}
</div>
{# --- SOLDE --- #}
{# 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>
{{ 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"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="{{ soldeOk ? 'M5 13l4 4L19 7' : '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></svg>
</div>
<span class="text-[8px] font-black uppercase tracking-tighter {{ soldeOk ? 'text-emerald-500' : 'text-rose-500' }}">
Solde
</span>
<span class="text-[8px] font-black uppercase tracking-tighter {{ soldeOk ? 'text-emerald-500' : 'text-rose-500' }}">Solde</span>
</div>
</div>
@@ -153,7 +144,7 @@
{# 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}) }}"
<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>
@@ -174,15 +165,5 @@
</div>
</div>
<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 %}

View File

@@ -281,27 +281,25 @@
</thead>
<tbody class="divide-y divide-white/5">
{% for payment in contrat.contratsPayments %}
{% if payment.state == 'complete' %}
<tr class="group hover:bg-white/[0.02] transition-colors">
<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">
<tr class="group hover:bg-white/[0.02] transition-colors">
<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">
<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
{% 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 %}">
{{ payment.type|replace({'_': ' '}) }}
</span>
</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">
<a href="{{ path('app_crm_contrats_view', {id: contrat.id, idPaymentPdf: payment.id}) }}" target="_blank"
class="inline-flex items-center gap-2 text-[9px] font-black uppercase text-slate-400 hover:text-white 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>
REÇU PDF
</a>
</td>
</tr>
{% endif %}
</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">
<a href="{{ path('app_crm_contrats_view', {id: contrat.id, idPaymentPdf: payment.id}) }}" target="_blank"
class="inline-flex items-center gap-2 text-[9px] font-black uppercase text-slate-400 hover:text-white 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>
REÇU PDF
</a>
</td>
</tr>
{% else %}
<tr><td colspan="4" class="py-12 text-center text-slate-600 text-[10px] font-black uppercase opacity-40">Aucun paiement effectué</td></tr>
{% endfor %}

View File

@@ -4,7 +4,7 @@
{% block title_header %}Fiche <span class="text-blue-500">Client</span>{% endblock %}
{% block actions %}
<a data-turbo="false" href="{{ path('app_crm_customer') }}" class="flex items-center px-4 py-2 text-[10px] font-black text-slate-400 hover:text-white uppercase tracking-widest transition-all group">
<a data-turbo="false" href="{{ path('app_crm_customer') }}" class="flex items-center px-4 py-2 text-[10px] font-black text-slate-400 hover:text-white uppercase tracking-widest transition-all group">
<svg class="w-4 h-4 mr-2 transform group-hover:-translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
Retour au listing
</a>
@@ -29,9 +29,9 @@
<span class="text-[10px] font-bold text-blue-400 uppercase tracking-widest">ID #{{ customer.id }}</span>
{% if customer.customerId %}
<span class="flex items-center text-[10px] font-bold text-emerald-400 uppercase tracking-widest bg-emerald-500/10 px-2 py-1 rounded-lg border border-emerald-500/20">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-2 shadow-[0_0_8px_rgba(16,185,129,0.5)]"></span>
Stripe Sync
</span>
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-2 shadow-[0_0_8px_rgba(16,185,129,0.5)]"></span>
Stripe Sync
</span>
{% endif %}
</div>
</div>
@@ -44,6 +44,7 @@
{# GRILLE PRINCIPALE INFOS #}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2 space-y-8">
{# FORMULAIRE COORDONNÉES #}
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] p-10">
<div class="flex items-center space-x-4 mb-10">
<span class="w-8 h-px bg-blue-500/30"></span>
@@ -66,30 +67,147 @@
</div>
</div>
</div>
{# SECTION DOCUMENTS (DEVIS & CONTRATS) #}
<div class="grid grid-cols-1 gap-8">
{# DEVIS #}
<div class="backdrop-blur-xl bg-[#1e293b]/20 border border-white/5 rounded-[2.5rem] p-8">
<h3 class="text-sm font-black text-white uppercase tracking-widest mb-6 flex items-center">
<span class="w-2 h-2 rounded-full bg-blue-500 mr-3"></span>
Devis récents
</h3>
<div class="overflow-x-auto">
<table class="w-full text-left">
<thead>
<tr class="border-b border-white/5">
<th class="pb-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Numéro</th>
<th class="pb-4 text-[10px] font-black text-slate-500 uppercase tracking-widest text-center">Statut</th>
<th class="pb-4 text-[10px] font-black text-slate-500 uppercase tracking-widest text-right">Montant HT</th>
</tr>
</thead>
<tbody class="divide-y divide-white/5">
{% for devis in customer.devis %}
<tr>
<td class="py-4 text-xs font-bold text-white">
<a target="_blank" href="{{ path('app_crm_devis_edit',{id:devis.id}) }}"> {{ devis.num }}</a>
</td>
<td class="py-4 text-center">
<span class="px-2 py-1 bg-blue-500/10 text-blue-400 rounded-md text-[9px] font-black uppercase tracking-tighter">
{{ devis.state|default('Brouillon') }}
</span>
</td>
<td class="py-4 text-right text-xs font-mono text-slate-300">
{{ devis|totalQuoto|number_format(2, ',', ' ') }}
</td>
</tr>
{% else %}
<tr>
<td colspan="3" class="py-6 text-center text-[10px] font-bold text-slate-600 uppercase">Aucun devis</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{# CONTRATS #}
<div class="backdrop-blur-xl bg-[#1e293b]/20 border border-white/5 rounded-[2.5rem] p-8">
<h3 class="text-sm font-black text-white uppercase tracking-widest mb-6 flex items-center">
<span class="w-2 h-2 rounded-full bg-emerald-500 mr-3"></span>
Contrats & Réservations
</h3>
<div class="overflow-x-auto">
<table class="w-full text-left">
<thead>
<tr class="border-b border-white/5">
<th class="pb-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Réservation</th>
<th class="pb-4 text-[10px] font-black text-slate-500 uppercase tracking-widest text-right">Total</th>
<th class="pb-4 text-[10px] font-black text-slate-500 uppercase tracking-widest text-right">Réglé</th>
<th class="pb-4 text-[10px] font-black text-slate-500 uppercase tracking-widest text-right">Solde</th>
</tr>
</thead>
<tbody class="divide-y divide-white/5">
{% for contrat in customer.contrats %}
{% set total = contrat|totalContrat %}
{% set paid = contrat|totalPayContrat %}
{% set rest = contrat|totalRestpayContrat %}
<tr class="group hover:bg-white/[0.02] transition-colors">
<td class="py-4 text-xs font-bold text-white">
<a target="_blank" href="{{ path('app_crm_contrats_view',{id:contrat.id}) }}"> {{ contrat.numReservation }}</a>
</td>
<td class="py-4 text-right text-xs font-mono text-slate-400">
{{ total|number_format(2, ',', ' ') }}
</td>
<td class="py-4 text-right text-xs font-mono text-emerald-400">
{{ paid|number_format(2, ',', ' ') }}
</td>
<td class="py-4 text-right text-xs font-mono {{ rest > 0 ? 'text-rose-400' : 'text-emerald-500' }}">
{{ rest|number_format(2, ',', ' ') }}
{% if rest > 0 %}
<div class="text-[8px] font-black uppercase opacity-50 tracking-tighter">En attente</div>
{% else %}
<div class="text-[8px] font-black uppercase opacity-50 tracking-tighter italic">Soldé</div>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="4" class="py-6 text-center text-[10px] font-bold text-slate-600 uppercase tracking-widest">Aucun contrat actif</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="space-y-8">
<div class="backdrop-blur-xl bg-emerald-500/5 border border-emerald-500/10 rounded-[2.5rem] p-8">
{# CARTE RÉSUMÉ & CHIFFRE D'AFFAIRES #}
<div class="backdrop-blur-xl bg-emerald-500/5 border border-emerald-500/10 rounded-[2.5rem] p-8 shadow-xl">
<h3 class="text-[10px] font-black text-emerald-500 uppercase tracking-widest mb-6 flex items-center">
<svg class="w-4 h-4 mr-2" 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>
Résumé
Résumé financier
</h3>
<div class="space-y-4">
<div class="flex justify-between items-center border-b border-white/5 pb-3">
<span class="text-xs text-slate-400 font-medium">Contrat</span>
<span class="text-xs text-white font-bold uppercase tracking-widest">
{{ customer.type|default('Standard') }}
</span>
<div class="space-y-6">
{# CALCUL DU CA GLOBAL (Devis + Contrats) #}
{% set totalCA = 0 %}
{% for c in customer.contrats %}{% set totalCA = totalCA + (c|totalContrat) %}{% endfor %}
<div class="p-6 bg-emerald-500/10 rounded-3xl border border-emerald-500/20">
<span class="text-[9px] font-black text-emerald-500/60 uppercase tracking-widest block mb-1">Total HT généré</span>
<div class="flex items-baseline gap-1">
<span class="text-3xl font-black text-white tracking-tighter">{{ totalCA|number_format(2, ',', ' ') }}</span>
<span class="text-xs font-bold text-emerald-500">€</span>
</div>
<div class="mt-3 grid grid-cols-2 gap-2">
<div class="text-[8px] text-slate-500 font-bold uppercase">
{{ customer.devis|length }} Devis
</div>
<div class="text-[8px] text-slate-500 font-bold uppercase text-right">
{{ customer.contrats|length }} Contrats
</div>
</div>
</div>
<div class="flex justify-between items-center border-b border-white/5 pb-4">
<span class="text-xs text-slate-400 font-medium">Type Client</span>
<span class="text-[10px] text-white font-black uppercase tracking-widest bg-white/5 px-3 py-1 rounded-full border border-white/5">
{{ customer.type|default('Standard') }}
</span>
</div>
</div>
</div>
{# ZONE SENSIBLE #}
<div class="backdrop-blur-xl bg-rose-500/5 border border-rose-500/10 rounded-[2.5rem] p-8">
<h4 class="text-[10px] font-black text-rose-500 uppercase tracking-widest mb-4">Zone sensible</h4>
<p class="text-[9px] text-slate-500 mb-6 leading-relaxed uppercase font-bold tracking-tight">
La suppression est définitive sur l'intranet et Stripe.
</p>
<a data-turbo="false" href="{{ path('app_crm_customer_delete', {id: customer.id}) }}?_token={{ csrf_token('delete' ~ customer.id) }}"
<a data-turbo="false" href="{{ path('app_crm_customer_delete', {id: customer.id}) }}?_token={{ csrf_token('delete' ~ customer.id) }}"
data-turbo-method="post"
data-turbo-confirm="Confirmer la suppression définitive ?"
class="flex items-center justify-center w-full py-3 border border-rose-500/20 hover:bg-rose-600 text-rose-500 hover:text-white text-[9px] font-black uppercase tracking-widest rounded-xl transition-all shadow-lg shadow-rose-500/5">
@@ -102,7 +220,6 @@
{# SECTION ADRESSES #}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mt-12">
{# LISTE DES ADRESSES #}
<div class="lg:col-span-2 space-y-6">
<h3 class="text-lg font-bold text-white mb-6 flex items-center ml-4">
@@ -122,7 +239,7 @@
</p>
</div>
<div class="flex space-x-2">
<a data-turbo="false" href="{{ path('app_crm_customer_edit', {id: customer.id, idAddr: address.id}) }}" class="p-2 bg-blue-500/10 text-blue-400 rounded-lg hover:bg-blue-500 hover:text-white transition-all">
<a data-turbo="false" href="{{ path('app_crm_customer_edit', {id: customer.id, idAddr: address.id}) }}" class="p-2 bg-blue-500/10 text-blue-400 rounded-lg hover:bg-blue-500 hover:text-white transition-all">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="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>
</a>
</div>
@@ -136,7 +253,7 @@
</div>
</div>
{# FORMULAIRE D'ADRESSE (AJOUT/EDIT) #}
{# FORMULAIRE D'ADRESSE #}
<div class="lg:col-span-1">
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/10 rounded-[2.5rem] p-8 shadow-2xl sticky top-8">
<div class="mb-8">
@@ -161,9 +278,8 @@
<button type="submit" class="w-full py-4 bg-emerald-600 hover:bg-emerald-500 text-white text-[10px] font-black uppercase tracking-widest rounded-2xl shadow-lg shadow-emerald-600/10 transition-all">
{{ editingAddress ? 'Enregistrer les modifications' : 'Ajouter au carnet' }}
</button>
{% if editingAddress %}
<a data-turbo="false" href="{{ path('app_crm_customer_edit', {id: customer.id}) }}" class="text-center py-2 text-[9px] font-black text-slate-500 uppercase tracking-widest hover:text-white transition-colors">
<a data-turbo="false" href="{{ path('app_crm_customer_edit', {id: customer.id}) }}" class="text-center py-2 text-[9px] font-black text-slate-500 uppercase tracking-widest hover:text-white transition-colors">
Annuler la modification
</a>
{% endif %}
@@ -188,4 +304,6 @@
background-size: 0.8rem;
}
</style>
{% endblock %}

View File

@@ -129,14 +129,27 @@
<a download="AUDIT_{{ quote.num }}.pdf" href="{{ vich_uploader_asset(quote, 'devisAuditFile') }}" title="Télécharger le certificat d'audit" target="_blank" class="p-2 bg-purple-600/10 hover:bg-purple-600 text-purple-500 hover:text-white rounded-xl transition-all border border-purple-500/20 shadow-lg shadow-purple-600/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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
</a>
<a data-turbo="false" href="{{ path('app_crm_contrats_create', {idDevis: quote.id}) }}"
title="Générer le contrat de location"
class="flex items-center gap-2 px-4 py-2 bg-blue-600/10 hover:bg-blue-600 text-blue-500 hover:text-white rounded-xl transition-all border border-blue-500/20 shadow-lg shadow-blue-600/5 font-bold text-xs uppercase tracking-widest">
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
<span>Créer le contrat</span>
</a>
{% if quote.contrats is null %}
{# ÉTAT : AUCUN CONTRAT - BOUTON CRÉATION (BLEU) #}
<a data-turbo="false" href="{{ path('app_crm_contrats_create', {idDevis: quote.id}) }}"
title="Générer le contrat de location"
class="group flex items-center gap-2 px-5 py-2.5 bg-blue-600/10 hover:bg-blue-600 text-blue-500 hover:text-white rounded-xl transition-all duration-300 border border-blue-500/20 shadow-lg shadow-blue-600/5 font-bold text-[10px] uppercase tracking-widest">
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
<span>Créer le contrat</span>
</a>
{% else %}
{# ÉTAT : CONTRAT EXISTANT - BOUTON ACCÈS (VERT) #}
<a data-turbo="false" href="{{ path('app_crm_contrats_view', {id: quote.contrats.id}) }}"
title="Accéder au contrat de location"
class="group flex items-center gap-2 px-5 py-2.5 bg-emerald-500/10 hover:bg-emerald-500 text-emerald-500 hover:text-white rounded-xl transition-all duration-300 border border-emerald-500/20 shadow-lg shadow-emerald-600/5 font-bold text-[10px] uppercase tracking-widest">
<svg class="w-4 h-4 transition-transform group-hover:scale-110" 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 0" />
</svg>
<span>Voir le contrat</span>
</a>
{% endif %}
{% else %}
{# PDF Brouillon #}
<a download="{{ quote.num }}.pdf" href="{{ vich_uploader_asset(quote,'devisDocuSealFile') }}" target="_blank" class="p-2 bg-slate-600/10 hover:bg-slate-600 text-slate-500 hover:text-white rounded-xl transition-all border border-slate-500/20">