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:
@@ -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');
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' : '' }}">
|
||||
|
||||
71
templates/admin/services/esyweb.html.twig
Normal file
71
templates/admin/services/esyweb.html.twig
Normal 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 %}
|
||||
67
templates/admin/services/ndd.html.twig
Normal file
67
templates/admin/services/ndd.html.twig
Normal 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">✓</span>{% else %}<span class="text-gray-300">✗</span>{% endif %}</td>
|
||||
<td class="px-4 py-3 text-center">{% if domain.isBilling %}<span class="text-green-600 font-bold">✓</span>{% else %}<span class="text-gray-300">✗</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 %}
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user