feat(customer.twig): Affiche le nombre total de contacts et traduit les types.

 feat(Stripe/Client.php): Ajoute la suppression et la mise à jour des clients Stripe.

 feat(base.twig): Affiche les messages flash avec des styles et des icônes.

 feat(customer/show.twig): Crée la page d'édition et de suppression du client.

🐛 fix(CustomerController.php): Corrige les actions d'édition et de suppression.
```
This commit is contained in:
Serreau Jovann
2026-01-16 13:35:13 +01:00
parent e7619e949b
commit 93f9a35130
5 changed files with 409 additions and 51 deletions

View File

@@ -80,19 +80,94 @@ class CustomerController extends AbstractController
]);
}
#[Route(path: '/crm/customer/show/{id}', name: 'app_crm_customer_show', methods: ['GET'])]
public function show(int $id, CustomerRepository $customerRepository): Response
{
#[Route(path: '/crm/customer/edit/{id}', name: 'app_crm_customer_edit', methods: ['GET', 'POST'])]
public function edit(
int $id,
CustomerRepository $customerRepository,
Request $request,
EntityManagerInterface $entityManager,
\App\Service\Stripe\Client $stripeClient,
AppLogger $appLogger
): 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()));
$form = $this->createForm(CustomerType::class, $customer);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// 1. Log de l'action
$appLogger->record('EDIT', sprintf('Modification du client : %s %s', $customer->getSurname(), $customer->getName()));
// 2. Mise à jour sur Stripe (si le client possède un CustomerId)
if ($customer->getCustomerId()) {
// Tu peux créer une méthode updateCustomer dans ton service Stripe
// ou simplement utiliser le client natif ici pour l'exemple :
try {
$stripeClient->updateCustomer($customer);
} catch (\Exception $e) {
$this->addFlash('warning', 'Modifié localement, mais erreur de synchro Stripe : ' . $e->getMessage());
}
}
// 3. Sauvegarde en base de données
$entityManager->flush();
$this->addFlash('success', 'Les informations du client ont été mises à jour.');
return $this->redirectToRoute('app_crm_customer_edit', ['id' => $customer->getId()]);
}
// Si c'est juste une consultation (GET), on log la vue
if (!$form->isSubmitted()) {
$appLogger->record('VIEW', sprintf('Consultation de la fiche client : %s', $customer->getName()));
}
return $this->render('dashboard/customer/show.twig', [
'customer' => $customer
'customer' => $customer,
'form' => $form->createView(),
]);
}
#[Route(path: '/crm/customer/delete/{id}', name: 'app_crm_customer_delete', methods: ['GET', 'POST'])]
public function delete(
int $id,
CustomerRepository $customerRepository,
Request $request,
\App\Service\Stripe\Client $client,
AppLogger $appLogger,
EntityManagerInterface $entityManager
): Response {
$customer = $customerRepository->find($id);
if (!$customer) {
$this->addFlash('error', 'Le client demandé n\'existe pas.');
return $this->redirectToRoute('app_crm_customer');
}
// Récupération du token CSRF envoyé via data-turbo-method (dans l'URL)
$token = $request->query->get('_token');
if ($this->isCsrfTokenValid('delete' . $customer->getId(), $token)) {
// Log de l'action de suppression (avant suppression réelle)
$appLogger->record('DELETE', sprintf('Suppression définitive du client : %s %s', $customer->getSurname(), $customer->getName()));
// Suppression sur Stripe si l'ID existe
if ($customer->getCustomerId()) {
$client->deleteCustomer($customer->getCustomerId());
}
$entityManager->remove($customer);
$entityManager->flush();
$this->addFlash('success', sprintf('Le client %s %s a été supprimé définitivement.', $customer->getSurname(), $customer->getName()));
} else {
$this->addFlash('error', 'Jeton de sécurité invalide. La suppression a été annulée.');
}
return $this->redirectToRoute('app_crm_customer');
}
}

View File

@@ -17,7 +17,6 @@ class Client
public function __construct(
private EntityManagerInterface $em
) {
// Récupération de la clé secrète depuis le .env
$stripeSk = $_ENV['STRIPE_SK'] ?? '';
$this->client = new StripeClient($stripeSk);
}
@@ -28,9 +27,7 @@ class Client
public function check(): array
{
try {
// Appel léger pour valider la clé API
$this->client->accounts->all(['limit' => 1]);
return [
'state' => true,
'message' => 'Connexion établie avec Stripe'
@@ -45,7 +42,7 @@ class Client
}
/**
* Crée un client sur Stripe et met à jour l'entité locale avec l'ID Stripe
* Crée un client sur Stripe et met à jour l'entité locale
*/
public function createCustomer(Customer $customer): array
{
@@ -62,7 +59,6 @@ class Client
]);
$customer->setCustomerId($stripeCustomer->id);
// Note: Le flush est à faire dans le contrôleur pour valider la transaction globale
return [
'state' => true,
@@ -74,6 +70,46 @@ class Client
}
}
/**
* Supprime un client sur Stripe
*/
public function deleteCustomer(?string $stripeCustomerId): array
{
if (!$stripeCustomerId) {
return [
'state' => false,
'message' => 'Aucun ID Stripe fourni pour la suppression.'
];
}
try {
$this->client->customers->delete($stripeCustomerId, []);
return [
'state' => true,
'message' => 'Client supprimé avec succès sur Stripe.'
];
} catch (ApiErrorException $e) {
// Si le client n'existe plus sur Stripe, on considère cela comme un succès pour le CRM
if ($e->getStripeCode() === 'resource_missing') {
return [
'state' => true,
'message' => 'Client déjà absent sur Stripe.'
];
}
return [
'state' => false,
'message' => 'Erreur API Stripe lors de la suppression : ' . $e->getMessage()
];
} catch (\Exception $e) {
return [
'state' => false,
'message' => 'Erreur système : ' . $e->getMessage()
];
}
}
/**
* Configure, met à jour et sauvegarde les secrets des Webhooks
*/
@@ -81,7 +117,6 @@ class Client
{
$baseUrl = $_ENV['STRIPE_BASEURL'] ?? 'https://votre-domaine.fr';
// Configuration des routes attendues
$configs = [
'refund' => [
'url' => $baseUrl . '/webhooks/refund',
@@ -101,13 +136,11 @@ class Client
$report = [];
try {
// Récupération des endpoints existants chez Stripe
$existingEndpoints = $this->client->webhookEndpoints->all(['limit' => 100]);
foreach ($configs as $name => $config) {
$stripeEndpoint = null;
// On cherche si l'URL est déjà enregistrée chez Stripe
foreach ($existingEndpoints->data as $endpoint) {
if ($endpoint->url === $config['url']) {
$stripeEndpoint = $endpoint;
@@ -115,17 +148,13 @@ class Client
}
}
// Recherche de la config correspondante en BDD
$dbConfig = $this->em->getRepository(StripeConfig::class)->findOneBy(['name' => $name]);
if (!$dbConfig) {
$dbConfig = new StripeConfig();
$dbConfig->setName($name);
}
if ($stripeEndpoint) {
// MISE À JOUR de l'endpoint chez Stripe
$this->client->webhookEndpoints->update($stripeEndpoint->id, [
'enabled_events' => $config['events']
]);
@@ -133,7 +162,6 @@ class Client
$dbConfig->setWebhookId($stripeEndpoint->id);
$report[$name] = ['status' => 'updated', 'url' => $config['url']];
} else {
// CRÉATION de l'endpoint chez Stripe
$newEndpoint = $this->client->webhookEndpoints->create([
'url' => $config['url'],
'enabled_events' => $config['events'],
@@ -141,7 +169,7 @@ class Client
]);
$dbConfig->setWebhookId($newEndpoint->id);
$dbConfig->setSecret($newEndpoint->secret); // On sauve le secret whsec_...
$dbConfig->setSecret($newEndpoint->secret);
$report[$name] = ['status' => 'created', 'url' => $config['url']];
}
@@ -159,11 +187,54 @@ class Client
}
}
/**
* Accès direct au client Stripe pour des besoins spécifiques
*/
public function getNativeClient(): StripeClient
{
return $this->client;
}
/**
* Met à jour un client existant sur Stripe
* @return array ['state' => bool, 'message' => string]
*/
public function updateCustomer(Customer $customer): array
{
// On vérifie d'abord si le client possède bien un ID Stripe
if (!$customer->getCustomerId()) {
return [
'state' => false,
'message' => 'Ce client n\'est pas encore lié à un compte Stripe.'
];
}
try {
// Préparation des données de mise à jour
$this->client->customers->update($customer->getCustomerId(), [
'name' => sprintf('%s %s', $customer->getSurname(), $customer->getName()),
'email' => $customer->getEmail(),
'phone' => $customer->getPhone(),
'metadata' => [
'internal_id' => $customer->getId(),
'type' => $customer->getType(),
'siret' => $customer->getSiret() ?? 'N/A',
'updated_at' => (new \DateTime())->format('Y-m-d H:i:s')
],
]);
return [
'state' => true,
'message' => 'Client mis à jour sur Stripe avec succès.'
];
} catch (ApiErrorException $e) {
return [
'state' => false,
'message' => 'Erreur API Stripe : ' . $e->getMessage()
];
} catch (\Exception $e) {
return [
'state' => false,
'message' => 'Erreur système lors de la mise à jour : ' . $e->getMessage()
];
}
}
}

View File

@@ -144,6 +144,39 @@
</div>
</div>
{% endif %}
{# SECTION DES MESSAGES FLASH #}
<div class="w-full space-y-4 mb-8">
{% for label, messages in app.flashes %}
{% for message in messages %}
{% set bgColor = label == 'success' ? 'bg-emerald-500/5' : (label == 'error' ? 'bg-rose-500/5' : 'bg-blue-500/5') %}
{% set borderColor = label == 'success' ? 'border-emerald-500/20' : (label == 'error' ? 'border-rose-500/20' : 'border-blue-500/20') %}
{% set textColor = label == 'success' ? 'text-emerald-500' : (label == 'error' ? 'text-rose-500' : 'text-blue-500') %}
{% set icon = label == 'success'
? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />'
: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />'
%}
<div class="flex items-center p-5 backdrop-blur-xl {{ bgColor }} border {{ borderColor }} rounded-[1.5rem] shadow-xl animate-in slide-in-from-right-4 duration-500" role="alert">
<div class="flex-shrink-0 w-10 h-10 rounded-xl {{ bgColor }} border {{ borderColor }} flex items-center justify-center {{ textColor }} mr-4">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{{ icon|raw }}
</svg>
</div>
<div class="flex-1">
<p class="text-[10px] font-black {{ textColor }} uppercase tracking-[0.2em] mb-0.5">
{{ label == 'success' ? 'Succès' : (label == 'error' ? 'Erreur' : 'Information') }}
</p>
<p class="text-sm text-slate-400 font-medium">
{{ message }}
</p>
</div>
<button type="button" onclick="this.parentElement.remove()" class="ml-auto text-slate-500 hover:text-slate-300 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
{% endfor %}
{% endfor %}
</div>
<div class="w-full">
{% block body %}{% endblock %}

View File

@@ -24,7 +24,7 @@
<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
{{ customers.getTotalItemCount }} CONTACTS
</span>
</div>
@@ -46,36 +46,28 @@
{# 1. IDENTITÉ #}
<td class="px-8 py-6 whitespace-nowrap">
<div class="flex items-center">
{# Avatar avec initiales #}
<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">
{# Nom et Civilité #}
<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>
{# ID Interne et État Stripe #}
<div class="flex items-center mt-1 space-x-2">
{# Badge ID Interne #}
<span class="text-[9px] font-bold text-slate-500 tracking-tighter">
ID: #{{ customer.id }}
</span>
<span class="text-[9px] font-bold text-slate-500 tracking-tighter">ID: #{{ customer.id }}</span>
{% if customer.customerId %}
{# ÉTAT : SYNCHRONISÉ (VERT) #}
<div class="flex items-center text-[8px] font-black text-emerald-400 uppercase tracking-[0.1em] bg-emerald-500/10 px-2 py-0.5 rounded-md border border-emerald-500/30 shadow-sm shadow-emerald-500/10">
<div class="flex items-center text-[8px] font-black text-emerald-400 uppercase tracking-[0.1em] bg-emerald-500/10 px-2 py-0.5 rounded-md border border-emerald-500/30">
<span class="flex h-1.5 w-1.5 rounded-full bg-emerald-500 mr-2"></span>
Stripe synchronisé
</div>
{% else %}
{# ÉTAT : NON SYNCHRONISÉ (ROUGE) #}
<div class="flex items-center text-[8px] font-black text-rose-500 uppercase tracking-[0.1em] bg-rose-500/10 px-2 py-0.5 rounded-md border border-rose-500/30 shadow-sm shadow-rose-500/10">
<div class="flex items-center text-[8px] font-black text-rose-500 uppercase tracking-[0.1em] bg-rose-500/10 px-2 py-0.5 rounded-md border border-rose-500/30">
<span class="flex h-1.5 w-1.5 rounded-full bg-rose-500 mr-2 animate-pulse"></span>
Stripe non synchronisé
Non synchronisé
</div>
{% endif %}
</div>
@@ -83,27 +75,33 @@
</div>
</td>
{# 2. TYPE (Badge dynamique) #}
{# 2. TYPE (Traduit en FR) #}
<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',
'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'
} %}
{% set typeLabels = {
'company': 'Entreprise',
'personal': 'Particulier',
'association': 'Association',
'mairie': 'Mairie'
} %}
<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>
{{ typeLabels[customer.type] ?? customer.type|upper }}
</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">
<div class="flex flex-col space-y-1 text-xs">
<div class="flex items-center text-slate-300">
<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">
<div class="flex items-center text-slate-400 font-mono text-[11px]">
<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>
@@ -113,23 +111,32 @@
{# 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">
<div class="text-[10px] font-mono text-slate-400 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>
<span class="text-[10px] text-slate-600 italic tracking-widest">---</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">
{# Consulter / Modifier #}
<a href="{{ path('app_crm_customer_edit', {id: customer.id}) }}"
class="p-2.5 text-slate-400 hover:text-blue-400 hover:bg-blue-400/10 rounded-xl transition-all border border-transparent hover:border-blue-500/20"
title="Consulter la 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>
{# Supprimer #}
<a href="{{ path('app_crm_customer_delete', {id: customer.id}) }}?_token={{ csrf_token('delete' ~ customer.id) }}"
data-turbo-method="post"
data-turbo-confirm="Confirmer la suppression définitive de {{ customer.surname }} {{ customer.name }} ?"
class="p-2.5 text-slate-400 hover:text-rose-500 hover:bg-rose-500/10 rounded-xl transition-all border border-transparent hover:border-rose-500/20"
title="Supprimer">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
</a>
</div>
</td>
@@ -144,20 +151,20 @@
</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
Page {{ customers.currentPageNumber }} sur {{ (customers.totalItemCount / customers.getItemNumberPerPage)|round(0, 'ceil') }}
</div>
<div class="navigation custom-pagination">
{{ knp_pagination_render(customers) }}
</div>
</div>
</div>
</div> {# Fin du conteneur principal #}
</div>
{# 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,

View File

@@ -0,0 +1,172 @@
{% extends 'dashboard/base.twig' %}
{% block title %}Modifier le Client{% endblock %}
{% block title_header %}Fiche Client{% endblock %}
{% block actions %}
{# Bouton Retour #}
<a href="{{ path('app_crm_customer') }}" class="flex items-center px-4 py-2 text-[10px] font-black text-slate-400 hover:text-white uppercase tracking-widest transition-all">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
Annuler
</a>
{% endblock %}
{% block body %}
<div class="w-full max-w-full mx-auto space-y-8 animate-in fade-in duration-700">
{{ form_start(form) }}
{# HEADER DE LA FICHE - GLASSMORPHISM #}
<div class="w-full backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] p-8 flex flex-col md:flex-row items-center justify-between mb-8 gap-6">
<div class="flex items-center space-x-6">
<div class="h-20 w-20 rounded-3xl bg-gradient-to-br from-blue-500 to-emerald-500 flex items-center justify-center text-white text-2xl font-black shadow-2xl">
{{ customer.surname|first|upper }}{{ customer.name|first|upper }}
</div>
<div>
<h2 class="text-3xl font-black text-white tracking-tight">
<span class="text-slate-500 text-sm uppercase tracking-[0.2em] block mb-1">Fiche Client</span>
{{ customer.surname|upper }} {{ customer.name }}
</h2>
<div class="flex items-center mt-2 space-x-4">
<span class="text-[10px] font-bold text-blue-400 uppercase tracking-widest">ID Interne: #{{ customer.id }}</span>
{% if customer.customerId %}
<span class="flex items-center text-[10px] font-bold text-emerald-400 uppercase tracking-widest bg-emerald-500/10 px-2 py-0.5 rounded-lg border border-emerald-500/20">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-2"></span>
Stripe ID: {{ customer.customerId }}
</span>
{% else %}
<span class="flex items-center text-[10px] font-bold text-rose-500 uppercase tracking-widest bg-rose-500/10 px-2 py-0.5 rounded-lg border border-rose-500/20 italic">
Non synchronisé Stripe
</span>
{% endif %}
</div>
</div>
</div>
<button type="submit" class="w-full md:w-auto px-10 py-4 bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-black uppercase tracking-widest rounded-2xl shadow-lg shadow-blue-600/20 transition-all hover:scale-105">
Enregistrer les modifications
</button>
</div>
{# GRILLE PRINCIPALE #}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
{# COLONNE GAUCHE : ÉDITION DES DONNÉES #}
<div class="lg:col-span-2 space-y-8">
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] p-10">
<div class="flex items-center space-x-4 mb-10">
<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é du client</span>
</div>
<div class="space-y-8">
{# LIGNE 1 : CIV / NOM / PRENOM #}
<div class="grid grid-cols-1 md:grid-cols-6 gap-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="flex items-center space-x-4 py-4">
<span class="w-8 h-px bg-blue-500/30"></span>
<span class="text-[10px] font-black text-blue-500 uppercase tracking-[0.3em]">Contact & Société</span>
</div>
{# LIGNE 2 : EMAIL / TEL #}
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
{{ form_label(form.email) }}
{{ form_widget(form.email) }}
</div>
<div>
{{ form_label(form.phone) }}
{{ form_widget(form.phone) }}
</div>
</div>
{# LIGNE 3 : TYPE / SIRET #}
<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>
</div>
</div>
{# COLONNE DROITE : STATUT & DANGER ZONE #}
<div class="space-y-8">
{# ACTIVITÉ #}
<div class="backdrop-blur-xl bg-emerald-500/5 border border-emerald-500/10 rounded-[2.5rem] p-8">
<h3 class="text-[10px] font-black text-emerald-500 uppercase tracking-widest mb-6 flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Statut du compte
</h3>
<div class="space-y-4">
<div class="flex justify-between items-center border-b border-white/5 pb-3">
<span class="text-xs text-slate-400 font-medium">Type Client</span>
{% set types = {
'personal': 'Particulier',
'company': 'Entreprise',
'association': 'Association',
'mairie': 'Mairie'
} %}
{# Puis plus bas dans ton code : #}
<span class="text-xs text-white font-bold">
{{ types[customer.type] ?? customer.type|upper }}
</span> </div>
</div>
</div>
{# ZONE DE DANGER #}
<div class="backdrop-blur-xl bg-rose-500/5 border border-rose-500/10 rounded-[2.5rem] p-8">
<h4 class="text-[10px] font-black text-rose-500 uppercase tracking-widest mb-4">Danger Zone</h4>
<p class="text-[10px] text-slate-500 mb-6 leading-relaxed">
La suppression d'un client entraînera la suppression immédiate de ses informations sur l'Intranet et sur son compte Stripe.
</p>
<a href="{{ path('app_crm_customer_delete', {id: customer.id}) }}?_token={{ csrf_token('delete' ~ customer.id) }}"
data-turbo-method="post"
data-turbo-confirm="Confirmer la suppression définitive de {{ customer.surname }} {{ customer.name }} ? Cette action est irréversible."
class="flex items-center justify-center w-full py-3 border border-rose-500/20 hover:bg-rose-600 text-rose-500 hover:text-white text-[9px] font-black uppercase tracking-tighter rounded-xl transition-all duration-300 shadow-lg shadow-rose-500/5">
<svg class="w-3.5 h-3.5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
Supprimer le compte
</a>
</div>
</div>
</div>
{{ form_end(form) }}
</div>
<style>
label { @apply block text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] mb-3 ml-2; }
input, select, textarea {
@apply w-full bg-slate-900/40 border border-white/10 rounded-2xl px-6 py-4 text-sm text-white outline-none focus:border-blue-500/50 focus:bg-slate-900/60 transition-all duration-300 !important;
}
/* Style spécifique pour les selects ChoiceType */
select {
@apply appearance-none cursor-pointer;
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;
}
</style>
{% endblock %}