feat: pages services NDD/Esy-Web + index Meilisearch + sync + recherche

Pages services :
- /admin/services/ndd : liste tous les NDD avec client, registrar,
  Cloudflare, gestion, facturation, expiration + barre recherche
- /admin/services/esyweb : liste tous les sites avec client, UUID,
  type, statut + barre recherche
- Liens sidebar mis à jour (Esy-Web → esyweb, Nom de domaine → ndd)

MeilisearchService :
- Index customer_ndd : searchable fqdn/registrar/customerName/customerEmail,
  filterable customerId/isGestion/isBilling
- Index customer_website : searchable name/uuid/customerName/customerEmail,
  filterable customerId/type/state
- CRUD : indexDomain/removeDomain/searchDomains, indexWebsite/removeWebsite/searchWebsites
- Serializers avec infos client intégrées (customerName, customerEmail, customerId)

SyncController :
- Route POST /admin/sync/domains : sync tous les Domain dans Meilisearch
- Route POST /admin/sync/websites : sync tous les Website dans Meilisearch
- Compteurs totalDomains et totalWebsites dans index

Template admin/sync :
- Bloc "Noms de domaine" (slate) avec bouton sync
- Bloc "Sites Internet" (blue) avec bouton sync

Recherche (app.js) :
- renderHit adapté : affiche fqdn/name pour NDD/sites, customerName en sous-texte
- Lien vers la fiche client (customerId) pour les résultats NDD/Website
- setupSearch configuré pour search-ndd et search-websites

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-04 21:26:17 +02:00
parent 9316743ac6
commit f68712bd02
8 changed files with 388 additions and 6 deletions

View File

@@ -142,12 +142,16 @@ document.addEventListener('DOMContentLoaded', () => {
});
// Search (customers & revendeurs)
const renderHit = (h, linkPrefix) =>
`<a href="${linkPrefix}${h.id}" class="block px-4 py-2 hover:bg-gray-50 border-b border-gray-100 text-xs">
<span class="font-bold">${h.fullName || h.raisonSociale || (h.firstName + ' ' + h.lastName)}</span>
${h.email ? `<span class="text-gray-400 ml-2">${h.email}</span>` : ''}
const renderHit = (h, linkPrefix) => {
const id = h.customerId || h.id;
const name = h.fqdn || h.name || h.fullName || h.raisonSociale || (h.firstName + ' ' + h.lastName);
const sub = h.customerName ? `<span class="text-gray-400 ml-2">${h.customerName}</span>` : (h.email ? `<span class="text-gray-400 ml-2">${h.email}</span>` : '');
return `<a href="${linkPrefix}${id}" class="block px-4 py-2 hover:bg-gray-50 border-b border-gray-100 text-xs">
<span class="font-bold">${name}</span>
${sub}
${h.codeRevendeur ? `<span class="ml-2 px-1 py-0.5 bg-gray-900 text-[#fabf04] text-[9px] font-bold">${h.codeRevendeur}</span>` : ''}
</a>`;
};
const performSearch = async (searchUrl, linkPrefix, results, q) => {
const resp = await fetch(`${searchUrl}?q=${encodeURIComponent(q)}`);
@@ -184,6 +188,8 @@ document.addEventListener('DOMContentLoaded', () => {
setupSearch('search-customers', 'search-results', '/admin/clients/search', '/admin/clients/');
setupSearch('search-revendeurs', 'search-results-revendeurs', '/admin/revendeurs/search', '/admin/revendeurs/');
setupSearch('search-ndd', 'search-ndd-results', '/admin/services/ndd/search', '/admin/clients/');
setupSearch('search-websites', 'search-websites-results', '/admin/services/esyweb/search', '/admin/clients/');
// Tarif tabs
const tarifTabs = document.getElementById('tarif-tabs');

View File

@@ -2,7 +2,13 @@
namespace App\Controller\Admin;
use App\Entity\Domain;
use App\Entity\Website;
use App\Service\MeilisearchService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -16,4 +22,40 @@ class ServicesController extends AbstractController
{
return $this->render('admin/services/index.html.twig');
}
#[Route('/ndd', name: 'ndd')]
public function ndd(EntityManagerInterface $em): Response
{
$domains = $em->getRepository(Domain::class)->findBy([], ['fqdn' => 'ASC']);
return $this->render('admin/services/ndd.html.twig', [
'domains' => $domains,
]);
}
#[Route('/ndd/search', name: 'ndd_search', methods: ['GET'])]
public function nddSearch(Request $request, MeilisearchService $meilisearch): JsonResponse
{
$q = trim($request->query->getString('q'));
return new JsonResponse('' === $q ? [] : $meilisearch->searchDomains($q));
}
#[Route('/esyweb', name: 'esyweb')]
public function esyweb(EntityManagerInterface $em): Response
{
$websites = $em->getRepository(Website::class)->findBy([], ['createdAt' => 'DESC']);
return $this->render('admin/services/esyweb.html.twig', [
'websites' => $websites,
]);
}
#[Route('/esyweb/search', name: 'esyweb_search', methods: ['GET'])]
public function esywebSearch(Request $request, MeilisearchService $meilisearch): JsonResponse
{
$q = trim($request->query->getString('q'));
return new JsonResponse('' === $q ? [] : $meilisearch->searchWebsites($q));
}
}

View File

@@ -3,7 +3,9 @@
namespace App\Controller\Admin;
use App\Entity\CustomerContact;
use App\Entity\Domain;
use App\Entity\StripeWebhookSecret;
use App\Entity\Website;
use App\Repository\CustomerRepository;
use App\Repository\PriceAutomaticRepository;
use App\Repository\RevendeurRepository;
@@ -60,6 +62,8 @@ class SyncController extends AbstractController
'customersSynced' => $customersSynced,
'customersNotSynced' => $customersNotSynced,
'totalContacts' => $em->getRepository(CustomerContact::class)->count([]),
'totalDomains' => $em->getRepository(Domain::class)->count([]),
'totalWebsites' => $em->getRepository(Website::class)->count([]),
'webhookSecrets' => $webhookSecrets,
]);
}
@@ -98,6 +102,40 @@ class SyncController extends AbstractController
return $this->redirectToRoute('app_admin_sync_index');
}
#[Route('/domains', name: 'domains', methods: ['POST'])]
public function syncDomains(EntityManagerInterface $em, MeilisearchService $meilisearch): Response
{
try {
$meilisearch->setupIndexes();
$domains = $em->getRepository(Domain::class)->findAll();
foreach ($domains as $domain) {
$meilisearch->indexDomain($domain);
}
$this->addFlash('success', \count($domains).' domaine(s) synchronise(s) dans Meilisearch.');
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur sync domaines : '.$e->getMessage());
}
return $this->redirectToRoute('app_admin_sync_index');
}
#[Route('/websites', name: 'websites', methods: ['POST'])]
public function syncWebsites(EntityManagerInterface $em, MeilisearchService $meilisearch): Response
{
try {
$meilisearch->setupIndexes();
$websites = $em->getRepository(Website::class)->findAll();
foreach ($websites as $website) {
$meilisearch->indexWebsite($website);
}
$this->addFlash('success', \count($websites).' site(s) synchronise(s) dans Meilisearch.');
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur sync sites : '.$e->getMessage());
}
return $this->redirectToRoute('app_admin_sync_index');
}
#[Route('/revendeurs', name: 'revendeurs', methods: ['POST'])]
public function syncRevendeurs(RevendeurRepository $revendeurRepository, MeilisearchService $meilisearch): Response
{

View File

@@ -4,8 +4,10 @@ namespace App\Service;
use App\Entity\Customer;
use App\Entity\CustomerContact;
use App\Entity\Domain;
use App\Entity\PriceAutomatic;
use App\Entity\Revendeur;
use App\Entity\Website;
use Meilisearch\Client;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
@@ -166,6 +168,66 @@ class MeilisearchService
}
}
public function indexDomain(Domain $domain): void
{
try {
$this->client->index('customer_ndd')->addDocuments([$this->serializeDomain($domain)]);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to index domain '.$domain->getId().': '.$e->getMessage());
}
}
public function removeDomain(int $domainId): void
{
try {
$this->client->index('customer_ndd')->deleteDocument($domainId);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to remove domain '.$domainId.': '.$e->getMessage());
}
}
/** @return list<array<string, mixed>> */
public function searchDomains(string $query, int $limit = 20): array
{
try {
return $this->client->index('customer_ndd')->search($query, ['limit' => $limit])->getHits();
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: search domains error: '.$e->getMessage());
return [];
}
}
public function indexWebsite(Website $website): void
{
try {
$this->client->index('customer_website')->addDocuments([$this->serializeWebsite($website)]);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to index website '.$website->getId().': '.$e->getMessage());
}
}
public function removeWebsite(int $websiteId): void
{
try {
$this->client->index('customer_website')->deleteDocument($websiteId);
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: Failed to remove website '.$websiteId.': '.$e->getMessage());
}
}
/** @return list<array<string, mixed>> */
public function searchWebsites(string $query, int $limit = 20): array
{
try {
return $this->client->index('customer_website')->search($query, ['limit' => $limit])->getHits();
} catch (\Throwable $e) {
$this->logger->error('Meilisearch: search websites error: '.$e->getMessage());
return [];
}
}
public function setupIndexes(): void
{
try {
@@ -215,6 +277,30 @@ class MeilisearchService
$this->client->index('customer_contact')->updateFilterableAttributes([
'customerId', 'isBillingEmail',
]);
try {
$this->client->createIndex('customer_ndd', ['primaryKey' => 'id']);
} catch (\Throwable $e) {
$this->logger->warning('Meilisearch: setupIndexes (customer_ndd) - '.$e->getMessage());
}
$this->client->index('customer_ndd')->updateSearchableAttributes([
'fqdn', 'registrar', 'customerName', 'customerEmail',
]);
$this->client->index('customer_ndd')->updateFilterableAttributes([
'customerId', 'isGestion', 'isBilling',
]);
try {
$this->client->createIndex('customer_website', ['primaryKey' => 'id']);
} catch (\Throwable $e) {
$this->logger->warning('Meilisearch: setupIndexes (customer_website) - '.$e->getMessage());
}
$this->client->index('customer_website')->updateSearchableAttributes([
'name', 'uuid', 'customerName', 'customerEmail',
]);
$this->client->index('customer_website')->updateFilterableAttributes([
'customerId', 'type', 'state',
]);
}
/**
@@ -306,4 +392,34 @@ class MeilisearchService
'isBillingEmail' => $contact->isBillingEmail(),
];
}
/** @return array<string, mixed> */
private function serializeDomain(Domain $domain): array
{
return [
'id' => $domain->getId(),
'fqdn' => $domain->getFqdn(),
'registrar' => $domain->getRegistrar(),
'isGestion' => $domain->isGestion(),
'isBilling' => $domain->isBilling(),
'customerId' => $domain->getCustomer()->getId(),
'customerName' => $domain->getCustomer()->getFullName(),
'customerEmail' => $domain->getCustomer()->getEmail(),
];
}
/** @return array<string, mixed> */
private function serializeWebsite(Website $website): array
{
return [
'id' => $website->getId(),
'name' => $website->getName(),
'uuid' => $website->getUuid(),
'type' => $website->getType(),
'state' => $website->getState(),
'customerId' => $website->getCustomer()->getId(),
'customerName' => $website->getCustomer()->getFullName(),
'customerEmail' => $website->getCustomer()->getEmail(),
];
}
}

View File

@@ -30,7 +30,7 @@
<svg class="w-3 h-3 sidebar-dropdown-arrow transition-transform {{ current_route starts with 'app_admin_services' ? 'rotate-180' : '' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M19 9l-7 7-7-7"/></svg>
</button>
<div class="sidebar-dropdown-menu ml-7 pl-3 border-l border-white/10 space-y-0.5 py-1 {{ current_route starts with 'app_admin_services' ? '' : 'hidden' }}">
<a href="#" class="block px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest rounded-md transition-all {{ current_route == 'app_admin_services_site' ? 'text-[#fabf04]' : 'text-white/40 hover:text-white/70' }}">Esy-Web</a>
<a href="{{ path('app_admin_services_esyweb') }}" class="block px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest rounded-md transition-all {{ current_route == 'app_admin_services_esyweb' ? 'text-[#fabf04]' : 'text-white/40 hover:text-white/70' }}">Esy-Web</a>
<a href="#" class="block px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest rounded-md transition-all {{ current_route == 'app_admin_services_mail' ? 'text-[#fabf04]' : 'text-white/40 hover:text-white/70' }}">Esy-Mail</a>
<a href="#" class="block px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest rounded-md transition-all {{ current_route == 'app_admin_services_mailer' ? 'text-[#fabf04]' : 'text-white/40 hover:text-white/70' }}">Esy-Mailer</a>
<a href="#" class="block px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest rounded-md transition-all {{ current_route == 'app_admin_services_analytics' ? 'text-[#fabf04]' : 'text-white/40 hover:text-white/70' }}">Esy-Analytics</a>
@@ -42,7 +42,7 @@
<a href="#" class="block px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest rounded-md transition-all {{ current_route == 'app_admin_services_aide' ? 'text-[#fabf04]' : 'text-white/40 hover:text-white/70' }}">Esy-Aide</a>
<a href="#" class="block px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest rounded-md transition-all {{ current_route == 'app_admin_services_meet' ? 'text-[#fabf04]' : 'text-white/40 hover:text-white/70' }}">Esy-Meet</a>
<a href="#" class="block px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest rounded-md transition-all {{ current_route == 'app_admin_services_tchat' ? 'text-[#fabf04]' : 'text-white/40 hover:text-white/70' }}">Esy-Tchat</a>
<a href="#" class="block px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest rounded-md transition-all {{ current_route == 'app_admin_services_ndd' ? 'text-[#fabf04]' : 'text-white/40 hover:text-white/70' }}">Nom de domaine</a>
<a href="{{ path('app_admin_services_ndd') }}" class="block px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest rounded-md transition-all {{ current_route == 'app_admin_services_ndd' ? 'text-[#fabf04]' : 'text-white/40 hover:text-white/70' }}">Nom de domaine</a>
</div>
</div>
<a href="{{ path('app_admin_clients_index') }}" class="sidebar-nav-item {{ current_route starts with 'app_admin_clients' ? 'active' : '' }}">

View File

@@ -0,0 +1,71 @@
{% extends 'admin/_layout.html.twig' %}
{% block title %}Esy-Web - Sites Internet - CRM SITECONSEIL{% endblock %}
{% block admin_content %}
<div class="page-container">
<h1 class="text-2xl font-bold heading-page mb-8">Esy-Web - Sites Internet</h1>
<div class="mb-6">
<div class="relative">
<input type="text" id="search-websites" placeholder="Rechercher un site, client..."
class="w-full px-4 py-3 pl-10 input-glass text-sm font-medium" autocomplete="off">
<svg class="w-4 h-4 absolute left-3 top-4 text-gray-400" 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>
</div>
<div id="search-websites-results" class="hidden glass border-t-0 bg-white max-h-64 overflow-y-auto"></div>
</div>
<div class="glass overflow-x-auto overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="glass-dark text-white">
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Nom</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Client</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">UUID</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Type</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Statut</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Cree le</th>
</tr>
</thead>
<tbody>
{% for site in websites %}
<tr class="border-b border-white/20 hover:bg-white/50">
<td class="px-4 py-3 font-bold">{{ site.name }}</td>
<td class="px-4 py-3">
<a href="{{ path('app_admin_clients_show', {id: site.customer.id}) }}" class="font-bold hover:text-[#fabf04] transition-colors">{{ site.customer.fullName }}</a>
{% if site.customer.email %}
<span class="text-[10px] text-gray-400 block">{{ site.customer.email }}</span>
{% endif %}
</td>
<td class="px-4 py-3 text-xs font-mono text-gray-500">{{ site.uuid }}</td>
<td class="px-4 py-3 text-center">
{% if site.type == 'ecommerce' %}
<span class="px-2 py-0.5 bg-purple-500/20 text-purple-700 font-bold uppercase text-[10px] rounded">E-Commerce</span>
{% else %}
<span class="px-2 py-0.5 bg-blue-500/20 text-blue-700 font-bold uppercase text-[10px] rounded">Vitrine</span>
{% endif %}
</td>
<td class="px-4 py-3 text-center">
{% if site.state == 'open' %}
<span class="px-2 py-0.5 bg-green-500/20 text-green-700 font-bold uppercase text-[10px] rounded">En ligne</span>
{% elseif site.state == 'install_progress' %}
<span class="px-2 py-0.5 bg-yellow-100 text-yellow-800 font-bold uppercase text-[10px] rounded">Installation</span>
{% elseif site.state == 'suspended' %}
<span class="px-2 py-0.5 bg-orange-500/20 text-orange-700 font-bold uppercase text-[10px] rounded">Suspendu</span>
{% elseif site.state == 'closed' %}
<span class="px-2 py-0.5 bg-red-500/20 text-red-700 font-bold uppercase text-[10px] rounded">Ferme</span>
{% else %}
<span class="px-2 py-0.5 bg-gray-100 text-gray-600 font-bold uppercase text-[10px] rounded">Cree</span>
{% endif %}
</td>
<td class="px-4 py-3 text-xs text-gray-500">{{ site.createdAt|date('d/m/Y') }}</td>
</tr>
{% else %}
<tr><td colspan="6" class="px-4 py-8 text-center text-gray-400 font-bold">Aucun site internet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="mt-3 text-xs text-gray-400">{{ websites|length }} site(s)</p>
</div>
{% endblock %}

View File

@@ -0,0 +1,67 @@
{% extends 'admin/_layout.html.twig' %}
{% block title %}Noms de domaine - Services - CRM SITECONSEIL{% endblock %}
{% block admin_content %}
<div class="page-container">
<h1 class="text-2xl font-bold heading-page mb-8">Noms de domaine</h1>
<div class="mb-6">
<div class="relative">
<input type="text" id="search-ndd" placeholder="Rechercher un domaine, client..."
class="w-full px-4 py-3 pl-10 input-glass text-sm font-medium" autocomplete="off">
<svg class="w-4 h-4 absolute left-3 top-4 text-gray-400" 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>
</div>
<div id="search-ndd-results" class="hidden glass border-t-0 bg-white max-h-64 overflow-y-auto"></div>
</div>
<div class="glass overflow-x-auto overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="glass-dark text-white">
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Domaine</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Client</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Registrar</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Cloudflare</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Gestion</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Facturation</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Expiration</th>
</tr>
</thead>
<tbody>
{% for domain in domains %}
<tr class="border-b border-white/20 hover:bg-white/50">
<td class="px-4 py-3 font-bold font-mono">{{ domain.fqdn }}</td>
<td class="px-4 py-3">
<a href="{{ path('app_admin_clients_show', {id: domain.customer.id}) }}" class="font-bold hover:text-[#fabf04] transition-colors">{{ domain.customer.fullName }}</a>
{% if domain.customer.raisonSociale %}
<span class="text-[10px] text-gray-400 block">{{ domain.customer.email }}</span>
{% endif %}
</td>
<td class="px-4 py-3 text-xs">{{ domain.registrar ?? '—' }}</td>
<td class="px-4 py-3 text-center">
{% if domain.zoneIdCloudflare %}
<span class="px-2 py-0.5 bg-orange-500/20 text-orange-700 font-bold uppercase text-[10px] rounded">{{ domain.zoneCloudflare ?? 'Lie' }}</span>
{% else %}
<span class="text-gray-300">—</span>
{% endif %}
</td>
<td class="px-4 py-3 text-center">{% if domain.isGestion %}<span class="text-green-600 font-bold">&#10003;</span>{% else %}<span class="text-gray-300">&#10007;</span>{% endif %}</td>
<td class="px-4 py-3 text-center">{% if domain.isBilling %}<span class="text-green-600 font-bold">&#10003;</span>{% else %}<span class="text-gray-300">&#10007;</span>{% endif %}</td>
<td class="px-4 py-3 text-xs">
{% if domain.expiredAt %}
<span class="{{ domain.isExpired ? 'text-red-600 font-bold' : (domain.isExpiringSoon ? 'text-yellow-600 font-bold' : 'text-gray-500') }}">{{ domain.expiredAt|date('d/m/Y') }}</span>
{% else %}
<span class="text-gray-300">—</span>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="7" class="px-4 py-8 text-center text-gray-400 font-bold">Aucun nom de domaine.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="mt-3 text-xs text-gray-400">{{ domains|length }} domaine(s)</p>
</div>
{% endblock %}

View File

@@ -73,6 +73,48 @@
</div>
</div>
{# Sync NDD #}
<div class="glass p-6">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-slate-100 border-2 border-slate-600 flex items-center justify-center">
<svg class="w-6 h-6 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9"/></svg>
</div>
<div>
<h2 class="font-bold uppercase text-sm">Noms de domaine</h2>
<p class="text-xs text-gray-500">Index Meilisearch : <strong>customer_ndd</strong></p>
<p class="text-xs text-gray-400 mt-0.5">{{ totalDomains }} domaine(s) en base</p>
</div>
</div>
<form method="post" action="{{ path('app_admin_sync_domains') }}">
<button type="submit" class="px-4 py-2 btn-glass text-slate-600 font-bold uppercase text-[10px] tracking-wider">
Synchroniser
</button>
</form>
</div>
</div>
{# Sync Websites #}
<div class="glass p-6">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-blue-100 border-2 border-blue-600 flex items-center justify-center">
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
</div>
<div>
<h2 class="font-bold uppercase text-sm">Sites Internet</h2>
<p class="text-xs text-gray-500">Index Meilisearch : <strong>customer_website</strong></p>
<p class="text-xs text-gray-400 mt-0.5">{{ totalWebsites }} site(s) en base</p>
</div>
</div>
<form method="post" action="{{ path('app_admin_sync_websites') }}">
<button type="submit" class="px-4 py-2 btn-glass text-blue-600 font-bold uppercase text-[10px] tracking-wider">
Synchroniser
</button>
</form>
</div>
</div>
{# Sync revendeurs #}
<div class="glass p-6">
<div class="flex items-center justify-between">