feat: rename admin screen, allow deleting a resource

This commit is contained in:
fromme committed 2026-09-17 17:56:44 +02:00
1 parent dc1822b8cf
commit 5e5bf00f8f
7 files changed
+197 -9

No files matched your search

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin\Resource;
use App\Controller\Traits\ConfirmedActionTrait;
use App\Entity\Booking;
use App\Entity\BookingSeries;
use App\Entity\Resource;
use App\Htmx\HxRedirectResponse;
use App\Repository\BookingRepository;
use App\Security\Role;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/**
* Deleting a resource for good, together with every booking and series on it.
*
* Retiring (EditController) is the way to keep the history; this is for a resource that is gone
* or was created by mistake. The confirmation names how many bookings go with it.
*
* The foreign keys are ON DELETE RESTRICT, so the bookings are removed explicitly first. The
* resource row is locked like BookingManager does, so a booking made while the modal is open
* either lands before the lock -- and is deleted with the rest -- or waits and then fails.
*/
#[IsGranted(Role::ADMIN)]
final class DeleteController extends AbstractController
{
use ConfirmedActionTrait;
public function __construct(
private readonly EntityManagerInterface $em,
private readonly BookingRepository $bookings,
private readonly LoggerInterface $logger,
) {
}
#[Route('/admin/resources/{id}/delete', name: 'app_admin_resource_delete', methods: ['GET', 'POST'])]
public function index(Request $request, Resource $resource): Response
{
$tokenId = 'delete-resource-'.$resource->getId();
if (!$request->isMethod(Request::METHOD_POST)) {
$total = $this->bookings->count(['resource' => $resource]);
$upcoming = $this->bookings->countFromDate($resource, new \DateTimeImmutable('today'));
return $this->render('admin/_modal_resource_delete.html.twig', [
'resource' => $resource,
'upcoming' => $upcoming,
'past' => $total - $upcoming,
'csrf_token_id' => $tokenId,
]);
}
$this->denyUnlessCsrfValid($request, $tokenId);
$id = $resource->getId();
$label = $resource->getLabel();
$deleted = $this->em->wrapInTransaction(function () use ($resource): int {
$this->em->lock($resource, LockMode::PESSIMISTIC_WRITE);
$deleted = $this->em->createQueryBuilder()
->delete(Booking::class, 'b')
->where('b.resource = :resource')
->setParameter('resource', $resource)
->getQuery()
->execute();
$this->em->createQueryBuilder()
->delete(BookingSeries::class, 's')
->where('s.resource = :resource')
->setParameter('resource', $resource)
->getQuery()
->execute();
$this->em->remove($resource);
$this->em->flush();
return (int) $deleted;
});
$this->logger->info('Admin deleted a resource.', [
'admin' => $this->getUser()?->getUserIdentifier(),
'resource_id' => $id,
'label' => $label,
'bookings_deleted' => $deleted,
]);
$this->addFlash('success', $deleted > 0
? \sprintf('%s und %d Buchungen gelöscht.', $label, $deleted)
: \sprintf('%s gelöscht.', $label));
return new HxRedirectResponse($this->generateUrl('app_admin_resource'));
}
}