'\d+'])] #[IsGranted('ROLE_USER')] public function edit(int $id, Request $request): Response { /** @var User $user */ $user = $this->getUser(); $email = $user->getEmail(); $password = $this->crypt->decrypt($user->getPassword()); // Fetch original booking data via API and cache result for a short ttl $bookingData = $this->fetchBookingData($email, $password, $id); if (null === $bookingData || $bookingData instanceof Notification) { $this->addFlash('error', 'Buchungsdaten nicht (mehr) verfügbar'); return $this->redirectToRoute('app_bookings'); } $this->denyAccessUnlessGranted('EDIT', $bookingData); // Load according travel data $travelData = $this->travelDataService->getTravelData($bookingData->dateId); if (null === $travelData) { $this->addFlash('error', 'Reisedaten nicht (mehr) verfügbar'); return $this->redirectToRoute('app_bookings'); } // Fetch mutability and availability information via service $mutableData = $this->travelDataService->getMutabilityData($bookingData->dateId); $availabilities = $this->travelDataService->getAvailabilityData($bookingData->dateId); if (null === $mutableData || null === $availabilities) { $this->addFlash('error', 'Reisedaten nicht (mehr) verfügbar'); return $this->redirectToRoute('app_bookings'); } // Patch travel data with additional information from above $this->travelDataService->patchAvailabilities($travelData, $availabilities); $this->travelDataService->patchMutability($travelData, $mutableData); // Create DTO for form $formData = $this->bookingDataProcessor->createBookingDtoFromBooking($bookingData, $travelData); // Calculate pricing data for template $summary = $this->bookingService->getRoomSummaryAndParticipantCount($formData); $roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($formData); $participantPrices = $this->priceCalculator->calculateAllParticipantIndividualPrices($formData); // Group selected rooms for summary display $availableRooms = $formData->travel->getAvailableRooms(); $groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType( $formData->getSelectedRooms(), $availableRooms ); $form = $this->createForm(BookingEditType::class, $formData, [ 'attr' => ['novalidate' => 'novalidate'], 'validation_groups' => ['booking_edit'], ]); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->logger->info('Initiated booking update', [ 'email' => $email, 'booking_id' => $id, ]); try { $response = $this->apiClient->updateBooking($formData, true); if ($response instanceof Notification) { if (true === $response->isError()) { $this->addFlash('error', $response->message); } else { $this->addFlash('info', $response->message); } $this->logger->error('Booking update not successful', [ 'email' => $email, 'booking_id' => $id, 'message' => $response->message, ]); } else { try { $cacheKey = sprintf('bpn_booking_%d', $id); $this->cache->delete($cacheKey); } catch (InvalidArgumentException $e) { } $this->addFlash('success', 'Buchung erfolgreich aktualisiert'); $this->logger->info('Booking update successful', [ 'email' => $email, 'booking_id' => $id, ]); return $this->redirectToRoute('app_booking_edit', ['id' => $id]); } } catch (ApiClientException $e) { $this->addFlash('error', 'Es ist ein Fehler in der Kommunikation mit dem Buchungssystem aufgetreten'); } } return $this->render('booking/edit.html.twig', [ 'bookingData' => $bookingData, 'bookingEditDto' => $formData, 'mutableData' => $mutableData, 'form' => $form->createView(), 'pricingData' => $summary['pricing'], 'participantCount' => $summary['participantCount'], 'assignmentCounts' => $roomAssignmentCounts, 'participantPrices' => $participantPrices, 'groupedSelectedRooms' => $groupedSelectedRooms, ]); } /** * HTMX endpoint for refreshing the participant form without validation. */ #[Route('/bookings/{id}/edit/refresh', name: 'app_booking_edit_refresh', requirements: ['id' => '\d+'], methods: ['POST'])] #[IsGranted('ROLE_USER')] public function refreshParticipantForm(int $id, Request $request): Response { /** @var User $user */ $user = $this->getUser(); $email = $user->getEmail(); $password = $this->crypt->decrypt($user->getPassword()); // Fetch original booking data via API and cache result for a short ttl $bookingData = $this->fetchBookingData($email, $password, $id); if (null === $bookingData || $bookingData instanceof Notification) { return new Response('Buchungsdaten nicht verfügbar', Response::HTTP_BAD_REQUEST); } $this->denyAccessUnlessGranted('EDIT', $bookingData); // Load travel data $travelData = $this->travelDataService->getTravelData($bookingData->dateId); if (null === $travelData) { return new Response('Reisedaten nicht verfügbar', Response::HTTP_BAD_REQUEST); } // Fetch mutability and availability information $mutableData = $this->travelDataService->getMutabilityData($bookingData->dateId); $availabilities = $this->travelDataService->getAvailabilityDataCached($bookingData->dateId); if (null === $mutableData || null === $availabilities) { return new Response('Reisedaten nicht verfügbar', Response::HTTP_BAD_REQUEST); } // Patch travel data $this->travelDataService->patchAvailabilities($travelData, $availabilities); $this->travelDataService->patchMutability($travelData, $mutableData); // Create fresh DTO from booking data $formData = $this->bookingDataProcessor->createBookingDtoFromBooking($bookingData, $travelData); // Process form data without validation to capture current state $form = $this->createForm(BookingEditType::class, $formData, [ 'attr' => ['novalidate' => 'novalidate'], 'validation_groups' => false, ]); $form->handleRequest($request); // Collect notifications from all participants $notifications = $this->collectParticipantNotifications($formData); // Calculate pricing and summary data $summary = $this->bookingService->getRoomSummaryAndParticipantCount($formData); $roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($formData); $participantPrices = $this->priceCalculator->calculateAllParticipantIndividualPrices($formData); // Group selected rooms for summary display $availableRooms = $formData->travel->getAvailableRooms(); $groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType( $formData->getSelectedRooms(), $availableRooms ); // Render updated blocks with fresh data $response = $this->htmxOobResponse( 'booking/edit.html.twig', ['participants_form', 'booking_summary'], [ 'form' => $form->createView(), 'bookingEditDto' => $formData, 'bookingData' => $bookingData, 'pricingData' => $summary['pricing'], 'participantCount' => $summary['participantCount'], 'assignmentCounts' => $roomAssignmentCounts, 'participantPrices' => $participantPrices, 'groupedSelectedRooms' => $groupedSelectedRooms, 'mutableData' => $mutableData, ] ); // Add notifications to HTMX trigger header if any exist if ([] !== $notifications) { $response->headers->set('HX-Trigger', json_encode([ 'showNotifications' => ['notifications' => $notifications], ])); } return $response; } /** * Collects all notifications from participants and clears them. * * @return array Array of notification messages */ private function collectParticipantNotifications(BookingDto $bookingDto): array { $notifications = []; foreach ($bookingDto->participants as $participant) { if ([] !== $participant->notifications) { foreach ($participant->notifications as $notification) { $notifications[] = $notification; } // Clear notifications after collecting $participant->notifications = []; } } return $notifications; } }