feat(search): Indexe les clients pour la recherche globale
```
This commit is contained in:
Serreau Jovann
2026-01-16 12:02:17 +01:00
parent 52f5eece17
commit 4f43dc9066
7 changed files with 257 additions and 7 deletions

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Command; namespace App\Command;
use App\Entity\Account; use App\Entity\Account;
use App\Entity\Customer;
use App\Service\Search\Client; use App\Service\Search\Client;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
@@ -42,6 +43,23 @@ class SearchCommand extends Command
$this->client->indexDocuments($datas, 'admin'); $this->client->indexDocuments($datas, 'admin');
} }
$customers = $this->entityManager->getRepository(Customer::class)->findAll();
foreach ($customers as $customer) {
$datas = [
'id' => $customer->getId(),
'name' => $customer->getName(),
'surname' => $customer->getSurname(),
'siret' => $customer->getSiret(),
'civ' => $customer->getCiv(),
'type' => $customer->getType(),
'phone' => $customer->getPhone(),
'email' => $customer->getEmail(),
];
$this->client->indexDocuments($datas, 'customer');
}
$output->writeln('Indexation terminée (hors ROOT).'); $output->writeln('Indexation terminée (hors ROOT).');
return Command::SUCCESS; return Command::SUCCESS;

View File

@@ -3,6 +3,7 @@
namespace App\Controller\Dashboard; namespace App\Controller\Dashboard;
use App\Entity\Customer; use App\Entity\Customer;
use App\Form\CustomerType;
use App\Logger\AppLogger; use App\Logger\AppLogger;
use App\Repository\CustomerRepository; use App\Repository\CustomerRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@@ -42,17 +43,39 @@ class CustomerController extends AbstractController
'customers' => $pagination, 'customers' => $pagination,
]); ]);
} }
#[Route(path: '/crm/customer/add', name: 'app_crm_customer_add', methods: ['GET', 'POST'])] #[Route(path: '/crm/customer/add', name: 'app_crm_customer_add', methods: ['GET', 'POST'])]
public function add(Request $request): Response public function add(Request $request, EntityManagerInterface $entityManager): Response
{ {
$this->appLogger->record('VIEW', 'Consultation de la page de création client'); $this->appLogger->record('VIEW', 'Consultation de la page de création client');
$c = new Customer(); $customer = new Customer();
//$form = $this->createForm(,$c); $form = $this->createForm(CustomerType::class, $customer);
// Ici, tu pourras ajouter ta logique de formulaire (CustomerType)
return $this->render('dashboard/customer/add.twig'); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// 1. Sauvegarde en base de données
$entityManager->persist($customer);
$entityManager->flush();
// 2. Log de l'action de création
$this->appLogger->record('CREATE', sprintf(
'Nouveau client créé : %s %s (Type: %s)',
$customer->getSurname(),
$customer->getName(),
$customer->getType()
));
// 3. Notification flash pour l'utilisateur
$this->addFlash('success', 'Le client a été enregistré avec succès.');
// 4. Redirection vers la liste
return $this->redirectToRoute('app_crm_customer');
}
return $this->render('dashboard/customer/add.twig', [
'form' => $form->createView(),
]);
} }
#[Route(path: '/crm/customer/show/{id}', name: 'app_crm_customer_show', methods: ['GET'])] #[Route(path: '/crm/customer/show/{id}', name: 'app_crm_customer_show', methods: ['GET'])]

View File

@@ -8,6 +8,7 @@ use App\Entity\AccountResetPasswordRequest;
use App\Form\RequestPasswordConfirmType; use App\Form\RequestPasswordConfirmType;
use App\Form\RequestPasswordRequestType; use App\Form\RequestPasswordRequestType;
use App\Repository\AccountRepository; use App\Repository\AccountRepository;
use App\Repository\CustomerRepository;
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent; use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
use App\Service\ResetPassword\Event\ResetPasswordEvent; use App\Service\ResetPassword\Event\ResetPasswordEvent;
use App\Service\Search\Client; use App\Service\Search\Client;
@@ -27,6 +28,7 @@ class SearchController extends AbstractController
#[Route(path: '/crm/recherche', name: 'app_crm_search', options: ['sitemap' => false], methods: ['GET'])] #[Route(path: '/crm/recherche', name: 'app_crm_search', options: ['sitemap' => false], methods: ['GET'])]
public function crmSearch( public function crmSearch(
AccountRepository $accountRepository, AccountRepository $accountRepository,
CustomerRepository $customerRepository,
Client $client, Client $client,
Request $request Request $request
): Response { ): Response {
@@ -37,6 +39,22 @@ class SearchController extends AbstractController
$response = $client->searchGlobal($query, 20); $response = $client->searchGlobal($query, 20);
foreach ($response['results'] as $resultGroup) { foreach ($response['results'] as $resultGroup) {
if (str_contains($resultGroup['indexUid'], 'intranet_ludikevent_customer')) {
// Extraction des IDs pour éviter les requêtes en boucle
$ids = array_map(fn($h) => $h['id'], $resultGroup['hits']);
$accounts = $customerRepository->findBy(['id' => $ids]);
foreach ($accounts as $account) {
$unifiedResults[] = [
'title' => $account->getName() . " " . $account->getSurname(),
'subtitle' => $account->getEmail(),
'link' => $this->generateUrl('app_crm_customer_show', ['id' => $account->getId()]),
'type' => 'Client',
'id' => $account->getId(),
'initials' => strtoupper(substr($account->getName(), 0, 1) . substr($account->getSurname(), 0, 1))
];
}
}
// On vérifie si l'index correspond aux administrateurs // On vérifie si l'index correspond aux administrateurs
if (str_contains($resultGroup['indexUid'], 'intranet_ludikevent_admin')) { if (str_contains($resultGroup['indexUid'], 'intranet_ludikevent_admin')) {

71
src/Form/CustomerType.php Normal file
View File

@@ -0,0 +1,71 @@
<?php
namespace App\Form;
use App\Entity\Customer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TelType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CustomerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('civ', ChoiceType::class, [
'label' => 'Civilité',
'choices' => [
'Monsieur' => 'M.',
'Madame' => 'Mme',
],
'expanded' => false,
'multiple' => false,
])
->add('surname', TextType::class, [
'label' => 'Prénom',
'attr' => ['placeholder' => 'ex: Jean'],
'required' => true,
])
->add('name', TextType::class, [
'label' => 'Nom',
'attr' => ['placeholder' => 'ex: DUPONT'],
'required' => true,
])
->add('type', ChoiceType::class, [
'label' => 'Type de client',
'choices' => [
'Particulier' => 'personal',
'Entreprise' => 'company',
'Association' => 'association',
'Mairie / Collectivité' => 'mairie',
],
])
->add('email', EmailType::class, [
'label' => 'Adresse Email',
'attr' => ['placeholder' => 'contact@exemple.fr'],
'required' => true,
])
->add('phone', TelType::class, [ // TelType est plus adapté pour mobile
'label' => 'Téléphone',
'attr' => ['placeholder' => '06 .. .. .. ..'],
'required' => true,
])
->add('siret', TextType::class, [
'label' => 'Numéro SIRET',
'required' => false,
'attr' => ['placeholder' => '14 chiffres'],
'help' => 'Obligatoire pour les entreprises et mairies',
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Customer::class,
]);
}
}

View File

@@ -29,6 +29,7 @@ class Client
{ {
return [ return [
"intranet_ludikevent_admin" => [], "intranet_ludikevent_admin" => [],
"intranet_ludikevent_customer" => [],
]; ];
} }

View File

@@ -54,7 +54,7 @@
<span class="text-slate-500 text-[10px] uppercase mr-1">{{ customer.civ }}</span> <span class="text-slate-500 text-[10px] uppercase mr-1">{{ customer.civ }}</span>
{{ customer.surname|upper }} {{ customer.name }} {{ customer.surname|upper }} {{ customer.name }}
</div> </div>
<div class="text-[10px] text-blue-400 font-medium tracking-tight">Client ID: #{{ loop.index + 100 }}</div> <div class="text-[10px] text-blue-400 font-medium tracking-tight">Client ID: #{{ customer.id }}</div>
</div> </div>
</div> </div>
</td> </td>

View File

@@ -0,0 +1,119 @@
{% extends 'dashboard/base.twig' %}
{% block title %}Nouveau Client{% endblock %}
{% block title_header %}Ajouter un <span class="text-blue-500">Client</span>{% endblock %}
{% block body %}
<div class="w-full">
{# Navigation haute #}
<div class="mb-6 px-2">
<a href="{{ path('app_crm_customer') }}" class="inline-flex items-center text-[10px] font-black text-slate-500 hover:text-white uppercase tracking-[0.2em] transition-colors group">
<svg class="w-4 h-4 mr-2 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="3" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Annuaire Clients
</a>
</div>
{# Carte Full Width #}
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] shadow-2xl overflow-hidden">
<div class="px-10 py-8 border-b border-white/5 bg-white/5 flex items-center justify-between">
<div>
<h2 class="text-xl font-bold text-white tracking-tight">Fiche d'identification</h2>
<p class="text-[10px] text-slate-500 font-bold uppercase tracking-widest mt-1">Saisie des informations de compte</p>
</div>
</div>
<div class="p-10">
{{ form_start(form, {'attr': {'class': 'space-y-12'}}) }}
{# SECTION 1 : IDENTITÉ & TYPE (Pleine largeur) #}
<div class="w-full">
<div class="flex items-center space-x-4 mb-8">
<span class="w-8 h-px bg-blue-500/30"></span>
<span class="text-[10px] font-black text-blue-500 uppercase tracking-[0.3em]">Identité & Type</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-6 gap-8 mb-8">
<div class="md:col-span-1">
{{ form_label(form.civ) }}
{{ form_widget(form.civ) }}
</div>
<div class="md:col-span-2">
{{ form_label(form.surname) }}
{{ form_widget(form.surname) }}
</div>
<div class="md:col-span-3">
{{ form_label(form.name) }}
{{ form_widget(form.name) }}
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
{{ form_label(form.type) }}
{{ form_widget(form.type) }}
</div>
<div>
{{ form_label(form.siret) }}
{{ form_widget(form.siret) }}
</div>
</div>
</div>
{# SECTION 2 : CONTACT (Ratio 50/50 exact) #}
<div class="w-full pt-4">
<div class="flex items-center space-x-4 mb-8">
<span class="w-8 h-px bg-emerald-500/30"></span>
<span class="text-[10px] font-black text-emerald-500 uppercase tracking-[0.3em]">Contact</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="w-full">
{{ form_label(form.email) }}
{{ form_widget(form.email) }}
</div>
<div class="w-full">
{{ form_label(form.phone) }}
{{ form_widget(form.phone) }}
</div>
</div>
</div>
{# ACTIONS : Boutons espacés #}
<div class="pt-10 border-t border-white/5 flex items-center justify-end">
<div class="flex items-center space-x-16"> {# Espace large entre les deux boutons #}
<a href="{{ path('app_crm_customer') }}" class="mr-2 text-[10px] font-black text-slate-500 hover:text-rose-500 uppercase tracking-widest transition-colors">
Annuler l'opération
</a>
<button type="submit" class="p-2 px-16 py-4 bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-black uppercase tracking-[0.2em] rounded-2xl shadow-lg shadow-blue-600/20 transition-all hover:scale-105 active:scale-95">
Valider et enregistrer
</button>
</div>
</div>
{{ form_end(form) }}
</div>
</div>
</div>
<style>
/* Styles communs aux champs */
label { @apply block text-[10px] font-black text-slate-500 uppercase tracking-widest mb-3 ml-2; }
input, select {
@apply w-full bg-slate-900/40 border border-white/10 rounded-2xl px-6 py-4 text-sm text-white focus:border-blue-500 focus:ring-4 focus:ring-blue-500/5 outline-none transition-all duration-300 !important;
}
/* Personnalisation du select (flèche custom) */
select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23475569'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 1.5rem center;
background-size: 1rem;
@apply appearance-none cursor-pointer pr-12;
}
</style>
{% endblock %}