getOrCreateBookingCreateDto($this->bookingService, $request); if ($result instanceof Response) { return $result; } $bookingCreateDto = $result; // Validate step access if ($redirect = $this->validateStepAccess($bookingCreateDto, 2)) { return $redirect; } // Enrich with fresh availability data $this->travelDataService->enrichWithFreshAvailabilities($bookingCreateDto->travel); // Ensure correct number of participants with prepopulation callback $this->bookingService->ensureCorrectNumberOfParticipants( $bookingCreateDto, $this->getUser(), fn ($user, $participant) => $this->prepopulationService->prepopulateApplicantFromUser($user, $participant) ); // Validate room assignments against current selection (handles back-navigation from step 2 to step 1) $this->roomAssignmentService->validateAndResetInvalidAssignments($bookingCreateDto); // Auto-assign rooms if needed $this->roomAssignmentService->assignRoomsIfNeeded($bookingCreateDto); // Preselect mandatory services $this->bookingService->preselectMandatoryServices($bookingCreateDto); // Save BookingDto to session $this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE); // Create validation form $form = $this->createForm(BookingCreateStep2Type::class, $bookingCreateDto); $form->handleRequest($request); // Handle form submission if (true === $form->isSubmitted() && true === $form->isValid()) { // All participants validated successfully, update current step $bookingCreateDto->currentStep = 3; $this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE); // Proceed to Step 3 return $this->redirectToRoute('app_booking_create_step_3'); } // Always generate card data with validation state to show completeness $cardsData = $this->participantCardService->getAllCardsDataWithValidation($bookingCreateDto); // Get complete summary data (pricing, rooms, CMS data) $summaryData = $this->summaryDataService->getSummaryData($bookingCreateDto); $templateData = [ 'form' => $form->createView(), 'bookingDto' => $bookingCreateDto, 'cardsData' => $cardsData, 'summaryData' => $summaryData, 'isSubmitted' => $form->isSubmitted(), ]; return $this->render('booking/create/step_2.html.twig', $templateData); } /** * Show or submit individual participant form. */ #[Route( path: '/bookings/create/participants/{index}', name: 'app_booking_create_step_2_participant', requirements: ['index' => '\d+'] )] public function editParticipant(int $index, Request $request): Response { $result = $this->loadBookingDtoOrRedirect($request, BookingDto::MODE_CREATE); if ($result instanceof Response) { return $result; } $bookingDto = $result; // Validate participant index if (false === isset($bookingDto->participants[$index])) { throw $this->createNotFoundException(sprintf('Participant at index %d does not exist', $index)); } // Use cached availability data (populated during booking init) $this->travelDataService->enrichWithFreshAvailabilities($bookingDto->travel); // Create form with booking_context option $form = $this->createParticipantForm($bookingDto, $index); $form->handleRequest($request); // Detect dummy data fill token — render pre-filled form immediately, skipping validation $isDummyDataFill = $this ->dummyDataFillService ->isTokenMatch($bookingDto->participants[$index], $bookingDto->getMode()) ; if (true === $form->isSubmitted() && true === $isDummyDataFill) { $this->dummyDataFillService->fill($bookingDto->participants[$index], $index); $bookingDto->bookingStatus = Constants::BOOKING_STATUS_OPEN; $this->bookingService->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE); // Recreate form with filled DTO so the view shows the dummy data $form = $this->createParticipantForm($bookingDto, $index); return $this->renderParticipantForm($form, $index, $bookingDto); } // Collect notifications from field handlers (run during PRE_SUBMIT) $notifications = $this->collectAndClearNotifications($bookingDto); if (true === $form->isSubmitted() && true === $form->isValid()) { // Save BookingDto to session $this->bookingService->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE); // Add notifications as flash messages (redirects destroy HTMX-triggered toasts) $this->addNotificationsAsFlashMessages($notifications); // HTMX redirect to cards view return $this->redirectToRoute('app_booking_create_step_2'); } return $this->renderParticipantForm($form, $index, $bookingDto); } /** * HTMX refresh endpoint for individual participant form. */ #[Route( path: '/bookings/create/participants/{index}/refresh', name: 'app_booking_create_step_2_participant_refresh', requirements: ['index' => '\d+'], methods: ['POST'] )] public function refreshParticipantForm(int $index, Request $request): Response { $result = $this->loadBookingDtoOrRedirect($request, BookingDto::MODE_CREATE); if ($result instanceof Response) { return $result; } $bookingDto = $result; // Validate participant index if (false === isset($bookingDto->participants[$index])) { throw $this->createNotFoundException(sprintf('Participant at index %d does not exist', $index)); } // Enrich with fresh availability data $this->travelDataService->enrichWithFreshAvailabilities($bookingDto->travel); // Use trait method for refresh handling return $this->handleParticipantRefresh( $request, $bookingDto, $index, 'app_booking_create_step_2_participant_refresh' ); } private function renderParticipantForm( FormInterface $form, int $index, BookingDto $bookingDto, ): Response { return $this->render('booking/create/step_2_participant.html.twig', [ 'form' => $form->createView(), 'participantIndex' => $index, 'bookingDto' => $bookingDto, 'summaryData' => $this->summaryDataService->getSummaryData($bookingDto), 'refreshRouteName' => 'app_booking_create_step_2_participant_refresh', ]); } }