feat(crm/customer): Ajoute la gestion des clients (list only)

This commit is contained in:
Serreau Jovann
2026-01-16 11:43:28 +01:00
parent 667da6af84
commit 52f5eece17
7 changed files with 425 additions and 1 deletions

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260116103812 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE customer (id SERIAL NOT NULL, civ VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, surname VARCHAR(255) NOT NULL, phone VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL, siret VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP TABLE customer');
}
}

View File

@@ -22,6 +22,7 @@ class BackupController extends AbstractController
#[Route(path: '/crm/sauvegarde', name: 'app_crm_backup', methods: ['GET'])] #[Route(path: '/crm/sauvegarde', name: 'app_crm_backup', methods: ['GET'])]
public function crmSauvegarde(BackupRepository $backupRepository): Response public function crmSauvegarde(BackupRepository $backupRepository): Response
{ {
$this->appLogger->record('VIEW', 'Consultation de la liste des sauvegardes système');
return $this->render('dashboard/backup.twig', [ return $this->render('dashboard/backup.twig', [
'backups' => $backupRepository->findBy([], ['createdAt' => 'DESC']), 'backups' => $backupRepository->findBy([], ['createdAt' => 'DESC']),
]); ]);

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Controller\Dashboard;
use App\Entity\Customer;
use App\Logger\AppLogger;
use App\Repository\CustomerRepository;
use Doctrine\ORM\EntityManagerInterface;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class CustomerController extends AbstractController
{
public function __construct(
private readonly AppLogger $appLogger,
private readonly EntityManagerInterface $entityManager
) {}
#[Route(path: '/crm/customer', name: 'app_crm_customer', methods: ['GET'])]
public function index(
PaginatorInterface $paginator,
CustomerRepository $customerRepository,
Request $request
): Response {
$this->appLogger->record('VIEW', 'Consultation de la liste des clients');
// Utilisation d'un QueryBuilder (recommandé pour KNP) ou findAll
$query = $customerRepository->createQueryBuilder('c')
->orderBy('c.surname', 'ASC')
->getQuery();
$pagination = $paginator->paginate(
$query,
$request->query->getInt('page', 1),
20
);
return $this->render('dashboard/customer.twig', [
'customers' => $pagination,
]);
}
#[Route(path: '/crm/customer/add', name: 'app_crm_customer_add', methods: ['GET', 'POST'])]
public function add(Request $request): Response
{
$this->appLogger->record('VIEW', 'Consultation de la page de création client');
$c = new Customer();
//$form = $this->createForm(,$c);
// Ici, tu pourras ajouter ta logique de formulaire (CustomerType)
return $this->render('dashboard/customer/add.twig');
}
#[Route(path: '/crm/customer/show/{id}', name: 'app_crm_customer_show', methods: ['GET'])]
public function show(int $id, CustomerRepository $customerRepository): Response
{
$customer = $customerRepository->find($id);
if (!$customer) {
throw $this->createNotFoundException('Client introuvable');
}
$this->appLogger->record('VIEW', sprintf('Consultation de la fiche client : %s', $customer->getName()));
return $this->render('dashboard/customer/show.twig', [
'customer' => $customer
]);
}
}

125
src/Entity/Customer.php Normal file
View File

@@ -0,0 +1,125 @@
<?php
namespace App\Entity;
use App\Repository\CustomerRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CustomerRepository::class)]
class Customer
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $civ = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column(length: 255)]
private ?string $surname = null;
#[ORM\Column(length: 255)]
private ?string $phone = null;
#[ORM\Column(length: 255)]
private ?string $email = null;
#[ORM\Column(length: 255)]
private ?string $type = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $siret = null;
public function getId(): ?int
{
return $this->id;
}
public function getCiv(): ?string
{
return $this->civ;
}
public function setCiv(string $civ): static
{
$this->civ = $civ;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getSurname(): ?string
{
return $this->surname;
}
public function setSurname(string $surname): static
{
$this->surname = $surname;
return $this;
}
public function getPhone(): ?string
{
return $this->phone;
}
public function setPhone(string $phone): static
{
$this->phone = $phone;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): static
{
$this->email = $email;
return $this;
}
public function getType(): ?string
{
return $this->type;
}
public function setType(string $type): static
{
$this->type = $type;
return $this;
}
public function getSiret(): ?string
{
return $this->siret;
}
public function setSiret(?string $siret): static
{
$this->siret = $siret;
return $this;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\Customer;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Customer>
*/
class CustomerRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Customer::class);
}
// /**
// * @return Customer[] Returns an array of Customer objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('c')
// ->andWhere('c.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('c.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Customer
// {
// return $this->createQueryBuilder('c')
// ->andWhere('c.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -41,7 +41,7 @@
{% import _self as menu %} {% import _self as menu %}
{{ menu.nav_link(path('app_crm'), 'Dashboard', '<path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>', 'app_crm') }} {{ menu.nav_link(path('app_crm'), 'Dashboard', '<path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>', 'app_crm') }}
{{ menu.nav_link('#', 'Clients', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_clients') }} {{ menu.nav_link(path('app_crm_customer'), 'Clients', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_clients') }}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,150 @@
{% extends 'dashboard/base.twig' %}
{% block title %}Gestion Clients{% endblock %}
{% block title_header %}Annuaire <span class="text-blue-500">Clients</span>{% endblock %}
{% block actions %}
<div class="flex items-center space-x-3">
<a href="{{ path('app_crm_customer_add') }}" class="flex items-center space-x-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-black uppercase tracking-[0.2em] rounded-xl transition-all shadow-lg shadow-blue-600/20 group">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 4v16m8-8H4" />
</svg>
<span>Nouveau Client</span>
</a>
</div>
{% endblock %}
{% block body %}
<div class="w-full backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] shadow-2xl overflow-hidden">
{# HEADER TABLEAU #}
<div class="px-8 py-6 border-b border-white/5 bg-white/5 flex items-center justify-between">
<div>
<h2 class="text-xl font-bold text-white tracking-tight">Liste des clients</h2>
<p class="text-[10px] text-slate-500 font-bold uppercase tracking-widest mt-1">Base de données centralisée</p>
</div>
<span class="px-4 py-1.5 bg-blue-500/10 text-blue-400 text-[10px] font-black uppercase rounded-lg border border-blue-500/20">
{{ customers|length }} CONTACTS
</span>
</div>
<div class="overflow-x-auto custom-scrollbar">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-slate-900/40 border-b border-white/5">
<th class="px-8 py-5 text-[10px] font-black text-slate-500 uppercase tracking-widest">Identité</th>
<th class="px-8 py-5 text-[10px] font-black text-slate-500 uppercase tracking-widest text-center">Type</th>
<th class="px-8 py-5 text-[10px] font-black text-slate-500 uppercase tracking-widest">Coordonnées</th>
<th class="px-8 py-5 text-[10px] font-black text-slate-500 uppercase tracking-widest">SIRET / ID</th>
<th class="px-8 py-5 text-[10px] font-black text-slate-500 uppercase tracking-widest text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-white/5">
{% for customer in customers %}
<tr class="hover:bg-white/5 transition-all group">
{# 1. IDENTITÉ #}
<td class="px-8 py-6 whitespace-nowrap">
<div class="flex items-center">
<div class="h-10 w-10 rounded-xl bg-gradient-to-br from-slate-700 to-slate-800 flex flex-shrink-0 items-center justify-center text-white font-black text-xs border border-white/10 shadow-lg">
{{ customer.surname|first|upper }}{{ customer.name|first|upper }}
</div>
<div class="ml-4">
<div class="text-sm font-bold text-white">
<span class="text-slate-500 text-[10px] uppercase mr-1">{{ customer.civ }}</span>
{{ customer.surname|upper }} {{ customer.name }}
</div>
<div class="text-[10px] text-blue-400 font-medium tracking-tight">Client ID: #{{ loop.index + 100 }}</div>
</div>
</div>
</td>
{# 2. TYPE (Badge dynamique) #}
<td class="px-8 py-6 text-center">
{% set typeStyles = {
'company': 'bg-purple-500/10 text-purple-400 border-purple-500/20',
'personal': 'bg-sky-500/10 text-sky-400 border-sky-500/20',
'association': 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',
'mairie': 'bg-amber-500/10 text-amber-400 border-amber-500/20'
} %}
<span class="px-3 py-1 rounded-lg text-[9px] font-black border uppercase tracking-widest {{ typeStyles[customer.type] ?? 'bg-slate-500/10 text-slate-400' }}">
{{ customer.type|default('Indéfini') }}
</span>
</td>
{# 3. COORDONNÉES #}
<td class="px-8 py-6">
<div class="flex flex-col space-y-1">
<div class="flex items-center text-slate-300 text-xs">
<svg class="w-3.5 h-3.5 mr-2 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
{{ customer.email }}
</div>
<div class="flex items-center text-slate-400 text-[11px] font-mono">
<svg class="w-3.5 h-3.5 mr-2 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" /></svg>
{{ customer.phone }}
</div>
</div>
</td>
{# 4. SIRET #}
<td class="px-8 py-6">
{% if customer.siret %}
<div class="text-[10px] font-mono text-slate-500 bg-black/20 px-2 py-1 rounded border border-white/5 inline-block">
{{ customer.siret }}
</div>
{% else %}
<span class="text-[10px] text-slate-600 italic">N/A</span>
{% endif %}
</td>
{# 5. ACTIONS #}
<td class="px-8 py-6 text-right">
<div class="flex items-center justify-end space-x-2">
<a href="#" class="p-2 text-slate-400 hover:text-blue-400 hover:bg-blue-400/10 rounded-lg transition-all" title="Modifier">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
</a>
<a href="#" class="p-2 text-slate-400 hover:text-white hover:bg-white/10 rounded-lg transition-all" title="Voir fiche">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>
</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="px-8 py-20 text-center italic text-slate-500 font-medium">
Aucun client enregistré dans la base.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# PAGINATION #}
<div class="px-8 py-8 bg-black/20 border-t border-white/5">
<div class="flex flex-col md:flex-row items-center justify-between gap-6">
<div class="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">
Affichage de {{ customers.getItemNumberPerPage }} clients par page
</div>
<div class="navigation custom-pagination">
{{ knp_pagination_render(customers) }}
</div>
</div>
</div>
</div> {# Fin du conteneur principal #}
{# CSS pour styliser KnpPagination aux couleurs de ton dashboard #}
<style>
.custom-pagination nav ul { @apply flex space-x-2; }
.custom-pagination nav ul li span,
.custom-pagination nav ul li a {
@apply px-4 py-2 rounded-xl bg-white/5 border border-white/5 text-slate-400 text-xs font-bold transition-all;
}
.custom-pagination nav ul li.active span {
@apply bg-blue-600 border-blue-500 text-white shadow-lg shadow-blue-600/20;
}
.custom-pagination nav ul li a:hover {
@apply bg-white/10 text-white border-white/20;
}
</style>
{% endblock %}