feat(admin): Ajoute une modale de confirmation pour copier/supprimer un client.

🐛 fix(CustomerCommand): Corrige la suppression des données liées au client.
🎨 style(admin): Ajoute le style de la modale de confirmation.
 feat(CustomerCommand): Ajoute une commande pour purger les clients supprimés.
🛠️ chore(ansible): Ajoute une tâche cron pour purger les clients supprimés.
🗑️ feat(CustomerController): Ajoute une suppression forcée d'un client.
This commit is contained in:
Serreau Jovann
2025-09-27 13:02:10 +02:00
parent c958ce1665
commit 3f7ad5a90f
7 changed files with 175 additions and 4 deletions

View File

@@ -195,6 +195,14 @@
chdir: "{{ path }}"
when: ansible_os_family == "Debian" # Added a when condition here, often missed
- name: "Cron Task purge customer delete"
cron:
name: "Mainframe - Purge customer"
minute: "0"
hour: "21"
job: "php {{ site_path }}/bin/console mainframe:cron:customer"
user: root
# Ensure final state of /public/media, if you want 'bot' to own it for uploads
- name: Final check for public/media ownership and permissions
ansible.builtin.file:

View File

@@ -9,10 +9,12 @@ import {OrderCtrl} from './class/OrderCtrl'
import {LockdownWall} from './class/LockdownWall'
import {SecurityWall} from './class/SecurityWall'
import {IpWall} from './class/IpWall'
import {ConfirmModal} from './class/ConfirmModal'
import preactCustomElement from './functions/preact'
function script() {
customElements.define('confirm-modal',ConfirmModal,{extends:'a'})
customElements.define('auto-submit',AutoSubmit,{extends:'form'})
customElements.define('server-card',ServerCard,{extends:'div'})
customElements.define('auto-customer',AutoCustomer,{extends:'button'})

View File

@@ -410,3 +410,28 @@ lockdown-wall-item{
}
}
}
confirm-modal{
position: fixed;
top: 0;
left: 0;
z-index: 9000;
background: rgba(0,0,0,0.5);
backdrop-filter: blur(5px);
width: 100%;
height: 100%;
.confirm-modal-content{
width: 50%;
padding: 0.5rem;
background: #1a202c;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
}
h2 {
text-align: center;
font-size: 2rem;
margin-bottom: 2rem;
}
}

View File

@@ -0,0 +1,69 @@
export class ConfirmModal extends HTMLAnchorElement{
connectedCallback() {
let element = this;
element.addEventListener('click',btn=>{
btn.preventDefault();
let modalConfirm = document.createElement('confirm-modal');
modalConfirm.innerHTML = `<div class="confirm-modal-content"></div>`;
if(element.getAttribute('type') == "cp-customer") {
this.copyCustomer(modalConfirm,element.getAttribute('href'));
}
if(element.getAttribute('type') == "delete-customer") {
this.deleteCustomer(modalConfirm,element.getAttribute('href'));
}
document.body.appendChild(modalConfirm);
})
}
deleteCustomer(modalConfirm,link) {
let message = document.createElement('h2');
message.innerText = "Confirmer la suppression du client";
modalConfirm.querySelector('.confirm-modal-content').appendChild(message);
let grid = document.createElement('div')
grid.classList = "grid grid-cols-1 gap-4 md:grid-cols-2";
modalConfirm.querySelector('.confirm-modal-content').appendChild(grid);
let buttonOk = document.createElement('button');
buttonOk.classList = "bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded";
buttonOk.innerText = "Oui";
buttonOk.addEventListener('click',()=>{
modalConfirm.remove()
location.href = link;
})
let buttonKo = document.createElement('button');
buttonKo.classList = "bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded";
buttonKo.innerText = "Non";
buttonKo.addEventListener('click',()=>modalConfirm.remove())
grid.appendChild(buttonOk)
grid.appendChild(buttonKo)
}
copyCustomer(modalConfirm,link) {
let message = document.createElement('h2');
message.innerText = "Confirmer la copie du client";
modalConfirm.querySelector('.confirm-modal-content').appendChild(message);
let grid = document.createElement('div')
grid.classList = "grid grid-cols-1 gap-4 md:grid-cols-2";
modalConfirm.querySelector('.confirm-modal-content').appendChild(grid);
let buttonOk = document.createElement('button');
buttonOk.classList = "bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded";
buttonOk.innerText = "Oui";
buttonOk.addEventListener('click',()=>{
modalConfirm.remove()
location.href = link;
})
let buttonKo = document.createElement('button');
buttonKo.classList = "bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded";
buttonKo.innerText = "Non";
buttonKo.addEventListener('click',()=>modalConfirm.remove())
grid.appendChild(buttonOk)
grid.appendChild(buttonKo)
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Command;
use App\Entity\Account;
use App\Entity\Customer;
use App\Service\Generator\TempPasswordGenerator;
use App\Service\Mailer\Event\CreatedAdminEvent;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Uid\Uuid;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
#[AsCommand(name: 'mainframe:cron:customer')]
class CustomerCommand extends Command
{
public function __construct(private readonly EventDispatcherInterface $eventDispatcher, private readonly EntityManagerInterface $entityManager, ?string $name = null)
{
parent::__construct($name);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title("Purge all customer delete");
foreach ($this->entityManager->getRepository(Customer::class)->findBy(['isDeleted'=>true]) as $delete) {
$io->info("Delete account - ".$delete->getRaisonSocial());
foreach ($delete->getCustomerContacts()as $customerContact) {
$this->entityManager->remove($customerContact);
}
foreach ($delete->getCustomerAdvertPayments() as $customerAdvertPayment) {
$this->entityManager->remove($customerAdvertPayment);
}
foreach ($delete->getCustomerDevis() as $customerDevis) {
$this->entityManager->remove($customerDevis);
}
$this->entityManager->remove($delete);
$this->entityManager->flush();
}
return Command::SUCCESS;
}
}

View File

@@ -88,6 +88,22 @@ class CustomerController extends AbstractController
$eventDispatcher->dispatch(new DeleteCustomerEvent($customer));
return $this->redirectToRoute('artemis_intranet_customer');
}
#[Route(path: '/artemis/intranet/customer/forced/{id}',name: 'artemis_intranet_customer_forced',methods: ['GET', 'POST'])]
public function customerForced(?Customer $customer,LoggerService $loggerService,EntityManagerInterface $entityManager,Request $request,PaginatorInterface $paginator): Response
{
if(!$customer instanceof Customer){
return $this->redirectToRoute('artemis_intranet_customer');
}
$loggerService->log('DELETE',"Suppression accélérer d'un client - ".$customer->getRaisonSocial(),$this->getUser());
foreach ($customer->getCustomerContacts() as $customerContact) {
$entityManager->remove($customerContact);
}
$entityManager->remove($customer);
$entityManager->flush();
$this->addFlash("success","Suppression effectuée");
return $this->redirectToRoute('artemis_intranet_customer');
}
#[Route(path: '/artemis/intranet/customer/restore/{id}',name: 'artemis_intranet_customer_restore',methods: ['GET', 'POST'])]
public function customerRestore(?Customer $customer,LoggerService $loggerService,EntityManagerInterface $entityManager,EventDispatcherInterface $eventDispatcher): Response

View File

@@ -62,7 +62,6 @@
</thead>
<tbody class="divide-y divide-gray-700">
{% for customer in customers %}
<tr class="hover:bg-gray-700 transition relative hover:bg-gray-700 transition">
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-semibold">{{ customer.type|trans }} - {{ customer.raisonSocial }}</div>
@@ -76,9 +75,12 @@
<td class="px-6 py-4 text-center text-sm">0</td>
<td class="px-6 py-4 text-center text-sm text-green-500 font-bold">Non</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-center">
<a href="{{ path('artemis_intranet_customer',{idCopy:customer.id}) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded mr-2">Copier le client</a>
<a href="{{ path('artemis_intranet_customer_view',{id:customer.id}) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded mr-2">Voir</a>
<a href="{{ path('artemis_intranet_customer_delete',{id:customer.id}) }}" class="bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded">Supprimer</a>
<a data-turbo="false" is="confirm-modal" type="cp-customer" href="{{ path('artemis_intranet_customer',{idCopy:customer.id}) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded mr-2">Copier le client</a>
{% if customer.customerOrders.count <=0 and customer.customerDns.count <=0 %}
<a data-turbo="false" is="confirm-modal" type="delete-customer" href="{{ path('artemis_intranet_customer_delete',{id:customer.id}) }}" class="bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded">Supprimer</a>
{% endif %}
</td>
{% if customer.isDeleted %}
<td colspan="7" class="absolute inset-0 z-20 rounded">
@@ -88,7 +90,7 @@
<div class="relative flex items-center justify-center h-full text-white text-lg font-semibold space-x-4">
<span>Client supprimé</span>
<a href="{{ path('artemis_intranet_customer_restore',{id:customer.id}) }}" type="submit" class="bg-purple-600 hover:bg-purples-700 px-4 py-2 rounded text-white font-semibold">Restaurer</a>
<a href="{{ path('artemis_intranet_customer_forced',{id:customer.id}) }}" type="submit" class="bg-purple-600 hover:bg-purples-700 px-4 py-2 rounded text-white font-semibold">Accélerer la suppression</a>
</div>
</td>
{% endif %}