diff --git a/src/Controller/Admin/Resource/DeleteController.php b/src/Controller/Admin/Resource/DeleteController.php new file mode 100644 index 0000000..6588e0d --- /dev/null +++ b/src/Controller/Admin/Resource/DeleteController.php @@ -0,0 +1,103 @@ +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')); + } +} diff --git a/src/Form/Admin/ResourceFormType.php b/src/Form/Admin/ResourceFormType.php index b2ec15f..ca1e287 100644 --- a/src/Form/Admin/ResourceFormType.php +++ b/src/Form/Admin/ResourceFormType.php @@ -16,7 +16,7 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** - * A desk or parking spot. The type is not a field: it is fixed by the route that creates the + * A resource. The type is not a field: it is fixed by the route that creates the * resource and never changes afterwards, which is what lets the group choices be limited to it. */ final class ResourceFormType extends AbstractType @@ -50,7 +50,7 @@ final class ResourceFormType extends AbstractType if ($options['with_active']) { $builder->add('active', CheckboxType::class, [ 'label' => 'Buchbar', - 'help' => 'Nicht buchbare Plätze erscheinen nicht mehr im Plan. Bestehende Buchungen bleiben erhalten.', + 'help' => 'Nicht buchbare Ressourcen erscheinen nicht mehr im Plan. Bestehende Buchungen bleiben erhalten.', 'required' => false, ]); } diff --git a/templates/admin/_modal_resource.html.twig b/templates/admin/_modal_resource.html.twig index 1c8a46f..97faadf 100644 --- a/templates/admin/_modal_resource.html.twig +++ b/templates/admin/_modal_resource.html.twig @@ -1,5 +1,5 @@ {# - Creating and editing a desk or parking spot. Same double duty as the booking modal: the form + Creating and editing a resource. Same double duty as the booking modal: the form targets the modal itself, so an invalid submit (422) replaces it with the errors showing, and a valid one answers with HX-Redirect back to the list. #} diff --git a/templates/admin/_modal_resource_delete.html.twig b/templates/admin/_modal_resource_delete.html.twig new file mode 100644 index 0000000..349c1d9 --- /dev/null +++ b/templates/admin/_modal_resource_delete.html.twig @@ -0,0 +1,17 @@ +{% extends 'htmx_confirmation_modal.html.twig' %} + +{% block title %}{{ resource.type.label }} löschen?{% endblock %} + +{% block content %} +
+ {{ resource.type.label }} {{ resource.label }} wird endgültig gelöscht. +
+ {% if upcoming > 0 or past > 0 %} ++ Mit gelöscht werden {{ upcoming }} Buchungen ab heute und {{ past }} vergangene. + Um die Buchungen zu behalten, stattdessen „nicht buchbar“ setzen. +
+ {% endif %} +{% endblock %} + +{% block button_confirm %}Löschen{% endblock %} diff --git a/templates/admin/resources.html.twig b/templates/admin/resources.html.twig index 14733c1..4c46cac 100644 --- a/templates/admin/resources.html.twig +++ b/templates/admin/resources.html.twig @@ -3,11 +3,12 @@ here once saved -- the same shape as the sibling apps' admin lists. Retired resources stay listed, muted, because this is the only place they can be reactivated. - Only an empty room offers "Löschen"; DeleteController refuses the rest regardless. + Only an empty room offers "Löschen"; DeleteController refuses the rest regardless. A resource + can always be deleted, taking its bookings with it -- the modal says how many. #} {% extends 'layout.html.twig' %} -{% block title %}Plätze & Räume · ephub{% endblock %} +{% block title %}Ressourcen · ephub{% endblock %} {% macro opener(url, label, variant = 'neutral') %}