feat: ligne info services sous chaque client dans /admin/clients

Sous chaque ligne client, une ligne compacte affiche :
- Raison sociale, SIRET, type entreprise (si disponibles)
- Sites : nombre (placeholder, 0 pour l'instant)
- NDD : nombre de domaines liés au client
- Emails : nombre de DomainEmail liés aux domaines du client
- Sign : check vert/rouge (Esy-Signature activé)
- News : check vert/rouge (Esy-Mailer/Newsletter activé)
- Mail : check vert/rouge (au moins 1 email Esy-Mail)
- Statut paiement : OK (vert) ou IMPAYEE (rouge avec nombre)

ClientsController :
- index() reçoit EntityManagerInterface pour requêter Domain/DomainEmail
- buildCustomersInfo() : construit les compteurs par client
  (domains, emails, esyMail depuis DomainEmail count > 0)
- Les flags esySign/esyNewsletter/unpaid/sites seront branchés
  quand les entités correspondantes existeront

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-04 12:09:52 +02:00
parent c2c05505c8
commit 91b4100560
3 changed files with 87 additions and 11 deletions

View File

@@ -23,12 +23,14 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
class ClientsController extends AbstractController
{
#[Route('', name: 'index')]
public function index(CustomerRepository $customerRepository): Response
public function index(CustomerRepository $customerRepository, EntityManagerInterface $em): Response
{
$customers = $customerRepository->findBy([], ['createdAt' => 'DESC']);
$customersInfo = $this->buildCustomersInfo($customers, $em);
return $this->render('admin/clients/index.html.twig', [
'customers' => $customers,
'customersInfo' => $customersInfo,
]);
}
@@ -83,6 +85,38 @@ class ClientsController extends AbstractController
return $this->render('admin/clients/create.html.twig');
}
/**
* @param list<Customer> $customers
*
* @return array<int, array{sites: int, domains: int, emails: int, esySign: bool, esyNewsletter: bool, esyMail: bool, unpaid: int}>
*/
private function buildCustomersInfo(array $customers, EntityManagerInterface $em): array
{
$domainRepo = $em->getRepository(\App\Entity\Domain::class);
$emailRepo = $em->getRepository(\App\Entity\DomainEmail::class);
$info = [];
foreach ($customers as $customer) {
$domains = $domainRepo->findBy(['customer' => $customer]);
$emailCount = 0;
foreach ($domains as $domain) {
$emailCount += $emailRepo->count(['domain' => $domain]);
}
$info[$customer->getId()] = [
'sites' => 0,
'domains' => \count($domains),
'emails' => $emailCount,
'esySign' => false,
'esyNewsletter' => false,
'esyMail' => $emailCount > 0,
'unpaid' => 0,
];
}
return $info;
}
private function populateCustomerData(Request $request, Customer $customer): void
{
$customer->setFirstName(trim($request->request->getString('firstName')));

View File

@@ -44,30 +44,31 @@
</thead>
<tbody>
{% for customer in customers %}
<tr class="border-b border-white/20 hover:bg-white/50">
<td class="px-4 py-3">
{% set info = customersInfo[customer.id] ?? {} %}
<tr class="hover:bg-white/50">
<td class="px-4 pt-3 pb-1">
<span class="font-bold">{{ customer.fullName }}</span>
{% if customer.raisonSociale and customer.firstName %}
<span class="text-[10px] text-gray-400 ml-1">{{ customer.firstName }} {{ customer.lastName }}</span>
{% endif %}
</td>
<td class="px-4 py-3 text-xs font-mono">{{ customer.email ?? customer.user.email }}</td>
<td class="px-4 py-3">
<td class="px-4 pt-3 pb-1 text-xs font-mono">{{ customer.email ?? customer.user.email }}</td>
<td class="px-4 pt-3 pb-1">
{% if customer.typeCompany %}
<span class="px-2 py-0.5 bg-gray-100 text-gray-700 font-bold uppercase text-[10px]">{{ customer.typeCompany }}</span>
{% else %}
<span class="text-gray-400">—</span>
{% endif %}
</td>
<td class="px-4 py-3 text-xs font-mono">{{ customer.siret ?? '—' }}</td>
<td class="px-4 py-3 text-center">
<td class="px-4 pt-3 pb-1 text-xs font-mono">{{ customer.siret ?? '—' }}</td>
<td class="px-4 pt-3 pb-1 text-center">
{% if customer.stripeCustomerId %}
<span class="px-2 py-0.5 bg-green-500/20 text-green-700 font-bold uppercase text-[10px] rounded">Lie</span>
{% else %}
<span class="px-2 py-0.5 bg-gray-100 text-gray-500 font-bold uppercase text-[10px]">Non</span>
{% endif %}
</td>
<td class="px-4 py-3 text-center">
<td class="px-4 pt-3 pb-1 text-center">
{% if customer.state == 'active' %}
<span class="px-2 py-0.5 bg-green-500/20 text-green-700 font-bold uppercase text-[10px] rounded">Actif</span>
{% elseif customer.state == 'suspended' %}
@@ -78,8 +79,8 @@
<span class="px-2 py-0.5 bg-red-500/20 text-red-700 font-bold uppercase text-[10px] rounded">Desactive</span>
{% endif %}
</td>
<td class="px-4 py-3 text-xs text-gray-500">{{ customer.createdAt|date('d/m/Y') }}</td>
<td class="px-4 py-3 text-center">
<td class="px-4 pt-3 pb-1 text-xs text-gray-500">{{ customer.createdAt|date('d/m/Y') }}</td>
<td class="px-4 pt-3 pb-1 text-center">
<div class="flex items-center justify-center gap-2">
{% if not customer.isPendingDelete %}
<form method="post" action="{{ path('app_admin_clients_toggle', {id: customer.id}) }}">
@@ -98,6 +99,45 @@
</div>
</td>
</tr>
{# Ligne info services #}
<tr class="border-b border-white/20">
<td colspan="8" class="px-4 pb-3 pt-0">
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-[10px]">
{% if customer.raisonSociale %}
<span class="text-gray-400">{{ customer.raisonSociale }}</span>
<span class="text-gray-300">|</span>
{% endif %}
{% if customer.siret %}
<span class="text-gray-400 font-mono">{{ customer.siret }}</span>
<span class="text-gray-300">|</span>
{% endif %}
{% if customer.typeCompany %}
<span class="text-gray-400 uppercase font-bold">{{ customer.typeCompany }}</span>
<span class="text-gray-300">|</span>
{% endif %}
<span class="font-bold text-gray-500">Sites : {{ info.sites ?? 0 }}</span>
<span class="text-gray-300">|</span>
<span class="font-bold text-gray-500">NDD : {{ info.domains ?? 0 }}</span>
<span class="text-gray-300">|</span>
<span class="font-bold text-gray-500">Emails : {{ info.emails ?? 0 }}</span>
<span class="text-gray-300">|</span>
{# Services checks #}
<span class="font-bold" title="Esy-Signature">Sign {% if info.esySign ?? false %}<span class="text-green-600">&#10003;</span>{% else %}<span class="text-red-500">&#10007;</span>{% endif %}</span>
<span class="font-bold" title="Esy-Mailer">News {% if info.esyNewsletter ?? false %}<span class="text-green-600">&#10003;</span>{% else %}<span class="text-red-500">&#10007;</span>{% endif %}</span>
<span class="font-bold" title="Esy-Mail">Mail {% if info.esyMail ?? false %}<span class="text-green-600">&#10003;</span>{% else %}<span class="text-red-500">&#10007;</span>{% endif %}</span>
<span class="text-gray-300">|</span>
{# Statut paiement #}
{% if (info.unpaid ?? 0) > 0 %}
<span class="px-1.5 py-0.5 bg-red-500/20 text-red-700 font-bold uppercase rounded">Impayee ({{ info.unpaid }})</span>
{% else %}
<span class="px-1.5 py-0.5 bg-green-500/20 text-green-700 font-bold uppercase rounded">OK</span>
{% endif %}
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="8" class="px-4 py-8 text-center text-gray-400 font-bold">Aucun client.</td>

View File

@@ -57,8 +57,10 @@ class ClientsControllerTest extends TestCase
$repo = $this->createStub(CustomerRepository::class);
$repo->method('findBy')->willReturn([]);
$em = $this->createStub(EntityManagerInterface::class);
$controller = $this->createController();
$response = $controller->index($repo);
$response = $controller->index($repo, $em);
$this->assertInstanceOf(Response::class, $response);
}