```
✨ feat(search): Indexe les clients pour la recherche globale
```
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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'])]
|
||||||
|
|||||||
@@ -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
71
src/Form/CustomerType.php
Normal 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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ class Client
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
"intranet_ludikevent_admin" => [],
|
"intranet_ludikevent_admin" => [],
|
||||||
|
"intranet_ludikevent_customer" => [],
|
||||||
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
119
templates/dashboard/customer/add.twig
Normal file
119
templates/dashboard/customer/add.twig
Normal 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 %}
|
||||||
Reference in New Issue
Block a user