```
✨ 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:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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,
|
||||
|
||||
172
templates/dashboard/customer/show.twig
Normal file
172
templates/dashboard/customer/show.twig
Normal 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 %}
|
||||
Reference in New Issue
Block a user