getAccommodation(); $boardServices = []; $additionalServices = []; $currentBoardService = null; $currentAdditionalServices = []; if (null !== $accommodation && null !== $booking->getDateFrom() && null !== $booking->getDateTo()) { $boardServices = $this->boardServiceRepo->findByAccommodationAndDateRange( $accommodation, $booking->getDateFrom(), $booking->getDateTo(), ); $additionalServices = $this->additionalServiceRepo->findByAccommodationAndDateRange( $accommodation, $booking->getDateFrom(), $booking->getDateTo(), ); // Pre-select current board service if it is still in the available choices foreach ($boardServices as $bs) { if ($bs->getId() === $booking->getBoardServiceOriginalId()) { $currentBoardService = $bs; break; } } // Pre-select current additional services that are still in the available choices $currentIds = array_column($booking->getAdditionalServices(), 'originalServiceId'); $currentAdditionalServices = array_values(array_filter( $additionalServices, fn (AdditionalService $s) => in_array($s->getId(), $currentIds, true), )); } // ROLE_GROUPS_ADMIN is granted explicitly since 6c1073e4 removed it from ROLE_ADMIN's // role_hierarchy — an administrator who needs this has to hold it in their own right. $assignableManagers = []; if ($this->isGranted('ROLE_GROUPS_ADMIN')) { $assignableManagers = $this->userRepo->findGroupsStaff(); // A user assigned earlier may have lost the role since — keep them in the choices // so the field can render the current value instead of failing to transform it. $currentManager = $booking->getManagedBy(); if (null !== $currentManager && !in_array($currentManager, $assignableManagers, true)) { $assignableManagers[] = $currentManager; } } $form = $this->createForm(AccommodationBookingType::class, $booking, [ 'max_adolescent_age' => $accommodation?->getMaxAdolescentAge() ?? 0, 'board_services' => $boardServices, 'additional_services' => $additionalServices, 'current_board_service' => $currentBoardService, 'current_additional_services' => $currentAdditionalServices, 'assignable_managers' => $assignableManagers, 'currency' => $booking->getPricingCurrency() ?? $accommodation?->getCurrency() ?? 'EUR', ]); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { if ($form->has('boardService')) { $selectedBoardService = $form->get('boardService')->getData(); if ($selectedBoardService instanceof BoardService) { $booking->setBoardServiceLabel($selectedBoardService->getLabel()); $booking->setBoardServicePrice($selectedBoardService->getPrice()); $booking->setBoardServiceOriginalId($selectedBoardService->getId()); } else { $booking->setBoardServiceLabel(null); $booking->setBoardServicePrice(null); $booking->setBoardServiceOriginalId(null); } } if ($form->has('selectedAdditionalServices')) { $booking->setAdditionalServices([]); foreach ($form->get('selectedAdditionalServices')->getData() as $service) { $booking->addAdditionalServiceSnapshot( $service->getLabel() ?? '', $service->getPrice() ?? 0, $service->getType(), $service->getId(), ); } } if (!$this->refreshPriceSnapshot($form, $booking)) { // The submitted changes stay on the entity but are never flushed, so the form // comes back filled in with the error on it. Logging is safe here: the database // handler writes through DBAL, not the EntityManager. return $this->render('admin/accommodation_booking/edit.html.twig', [ 'booking' => $booking, 'form' => $form, ]); } // Editing never moves the booking on and never mails the customer: every status // change is its own named action (Angebot senden, bestätigen, absagen), so the // status, its timestamp and its email can no longer drift apart. $this->entityManager->persist($booking); $this->entityManager->flush(); $this->addFlash('success', 'Die Buchung wurde aktualisiert'); $this->logger->info('Updated accommodation booking', [ 'id' => $booking->getId(), 'groupName' => $booking->getGroupName(), ]); // Back to the detail page rather than the form: that is where every named status // action lives, so it is the view a manager needs after saving. The return url is // forwarded still encoded, the way return_url() handed it over, so that the detail // page's "zurück" leads to the filtered list the edit was started from. $parameters = ['id' => $booking->getId()]; $returnUrl = $request->query->getString('r'); if ('' !== $returnUrl) { $parameters['r'] = $returnUrl; } return $this->redirectToRoute('app_admin_accommodationbooking_show', $parameters); } return $this->render('admin/accommodation_booking/edit.html.twig', [ 'booking' => $booking, 'form' => $form, ]); } /** * The price is computed after validation, so an implausible total cannot be a constraint on * the form — it is reported as one anyway, because the office's next move is to go and fix * the house's prices, not to read an error page. * * @param FormInterface $form */ private function refreshPriceSnapshot(FormInterface $form, AccommodationBooking $booking): bool { try { $this->bookingService->refreshPriceSnapshot($booking); return true; } catch (ImplausibleBookingPriceException $e) { $this->logger->error('Refused to store an implausible accommodation booking total', [ 'exception' => $e, 'id' => $booking->getId(), 'accommodation' => $booking->getAccommodation()?->getCalendarCode(), 'paxCount' => $booking->getPaxCount(), 'nights' => $booking->getNights(), ]); $form->addError(new FormError(ImplausibleBookingPriceException::FORM_ERROR_MESSAGE)); return false; } } }