diff --git a/src/Controller/Admin/AccommodationBooking/ChangeAccommodationController.php b/src/Controller/Admin/AccommodationBooking/ChangeAccommodationController.php
new file mode 100644
index 0000000..73d3d02
--- /dev/null
+++ b/src/Controller/Admin/AccommodationBooking/ChangeAccommodationController.php
@@ -0,0 +1,88 @@
+isDraft()) {
+ $this->addFlash('error', 'Das Gruppenhaus lässt sich nur bei einem Entwurf ändern.');
+
+ return $this->htmxRedirect($request, $this->editUrl($booking, $request));
+ }
+
+ $form = $this->createForm(AccommodationBookingChangeAccommodationType::class, $booking, [
+ 'current_accommodation' => $booking->getAccommodation(),
+ 'hx_post' => $request->getRequestUri(),
+ ]);
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ $selected = $form->get('accommodation')->getData();
+
+ if ($selected instanceof Accommodation && $selected !== $booking->getAccommodation()) {
+ $this->bookingService->changeAccommodation($booking, $selected);
+
+ $this->addFlash('success', 'Das Gruppenhaus wurde geändert. Bitte wähle die Leistungen neu aus.');
+
+ $this->logger->info('Changed accommodation of accommodation booking', [
+ 'id' => $booking->getId(),
+ 'accommodation' => $selected->getName(),
+ ]);
+ }
+
+ // Back to the edit form, which is rebuilt from the new house: its board and
+ // additional service choices are resolved server-side and cannot be swapped in
+ // place, which is the whole reason this is a modal and not a field on that form.
+ return $this->htmxRedirect($request, $this->editUrl($booking, $request));
+ }
+
+ return $this->render('admin/accommodation_booking/modal_change_accommodation.html.twig', [
+ 'booking' => $booking,
+ 'form' => $form,
+ ]);
+ }
+
+ /**
+ * The return url is forwarded still encoded, the way return_url() handed it over, so that
+ * the edit page keeps leading back to the list the booking was opened from.
+ */
+ private function editUrl(AccommodationBooking $booking, Request $request): string
+ {
+ $parameters = ['id' => $booking->getId()];
+ $returnUrl = $request->query->getString('r');
+ if ('' !== $returnUrl) {
+ $parameters['r'] = $returnUrl;
+ }
+
+ return $this->generateUrl('app_admin_accommodationbooking_edit', $parameters);
+ }
+}
diff --git a/src/Form/Admin/Groups/AccommodationBookingChangeAccommodationType.php b/src/Form/Admin/Groups/AccommodationBookingChangeAccommodationType.php
new file mode 100644
index 0000000..51393a4
--- /dev/null
+++ b/src/Form/Admin/Groups/AccommodationBookingChangeAccommodationType.php
@@ -0,0 +1,54 @@
+
+ */
+class AccommodationBookingChangeAccommodationType extends AbstractType
+{
+ public function buildForm(FormBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->add('accommodation', EntityType::class, [
+ 'class' => Accommodation::class,
+ 'mapped' => false,
+ 'choice_label' => 'name',
+ 'label' => 'Gruppenhaus neu',
+ 'data' => $options['current_accommodation'],
+ ])
+ ;
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => AccommodationBooking::class,
+ // Only ever reached for an Entwurf, which is a scratch record the office fills in
+ // over time — the `edit` group would reject it over contact data this form does
+ // not even show.
+ 'validation_groups' => ['Default'],
+ 'current_accommodation' => null,
+ ]);
+ $resolver->setAllowedTypes('current_accommodation', ['null', Accommodation::class]);
+ }
+}
diff --git a/src/Service/AccommodationBookingService.php b/src/Service/AccommodationBookingService.php
index c77f324..104f4ce 100644
--- a/src/Service/AccommodationBookingService.php
+++ b/src/Service/AccommodationBookingService.php
@@ -530,6 +530,35 @@ class AccommodationBookingService
$this->sendCustomerConfirmationEmail($booking);
}
+ /**
+ * Moves a draft to another Gruppenhaus, for the case where the wrong one was picked at
+ * creation. The chosen Verpflegung and Zusatzleistungen belong to the old house's catalog —
+ * they carry its prices and its ids — so they are dropped rather than guessed at; the office
+ * picks them again from the new catalog on the edit page.
+ * Restricted to Entwurf: anything further along may already be in front of the customer via
+ * their access link, and swapping the house underneath them would rewrite what they were
+ * offered.
+ * Idempotent — a no-op for a booking that is not a draft or already sits at $accommodation.
+ */
+ public function changeAccommodation(AccommodationBooking $booking, Accommodation $accommodation): void
+ {
+ if (!$booking->isDraft() || $booking->getAccommodation() === $accommodation) {
+ return;
+ }
+
+ $booking->setAccommodation($accommodation);
+
+ $booking->setBoardServiceLabel(null);
+ $booking->setBoardServicePrice(null);
+ $booking->setBoardServiceOriginalId(null);
+ $booking->setAdditionalServices([]);
+
+ // Clears the snapshot by itself when the new house has no price for the booked range.
+ $this->refreshPriceSnapshot($booking);
+
+ $this->entityManager->flush();
+ }
+
/**
* Closes a booking that is not going to happen. Deliberately silent: the office tells
* the customer itself, so this only records the outcome.
diff --git a/templates/admin/accommodation_booking/edit.html.twig b/templates/admin/accommodation_booking/edit.html.twig
index f8fe803..39b26ee 100644
--- a/templates/admin/accommodation_booking/edit.html.twig
+++ b/templates/admin/accommodation_booking/edit.html.twig
@@ -1,8 +1,23 @@
{% extends 'layout_admin.html.twig' %}
{% block content %}
+ {% set back_params = { 'id': booking.id } %}
+ {% if app.request.query.get('r') %}
+ {% set back_params = back_params | merge({ 'r': app.request.query.get('r') }) %}
+ {% endif %}
+
{{ booking.recordLabel }} {{ booking.groupName }}
-
{{ booking.accommodation.name }}
+
+ {{ booking.accommodation.name }}
+ {# Only an Entwurf may still move house — see ChangeAccommodationController. #}
+ {% if booking.draft %}
+
+ {% endif %}
+
{% form_theme form 'forms_admin.html.twig' %}
@@ -69,10 +84,6 @@
- {% set back_params = { 'id': booking.id } %}
- {% if app.request.query.get('r') %}
- {% set back_params = back_params | merge({ 'r': app.request.query.get('r') }) %}
- {% endif %}
zurück
diff --git a/templates/admin/accommodation_booking/modal_change_accommodation.html.twig b/templates/admin/accommodation_booking/modal_change_accommodation.html.twig
new file mode 100644
index 0000000..22afec3
--- /dev/null
+++ b/templates/admin/accommodation_booking/modal_change_accommodation.html.twig
@@ -0,0 +1,20 @@
+{% extends 'htmx_modal_admin.html.twig' %}
+{% form_theme form 'forms_admin.html.twig' %}
+
+{% block content %}
+
+ Gruppenhaus ändern
+
+
+ Achtung: Die gewählte Verpflegung und alle Zusatzleistungen müssen anschließend neu ausgewählt werden.
+