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->enrichWithFreshAvailabilities($bookingCreateDto); // Ensure correct number of participants $this->ensureCorrectNumberOfParticipants($bookingCreateDto); // Auto-assign rooms if needed $this->autoAssignRoomsIfNeeded($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, [ 'validation_groups' => ['booking_create_step_2'], ]); $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->hxRedirect($request, $this->generateUrl('app_booking_create_step_3')); } // Extract participant indices with validation errors $participantErrors = []; if (true === $form->isSubmitted() && false === $form->isValid()) { $participantErrors = $this->extractParticipantErrorIndices($form); } // Generate cards data $cardsData = $this->generateAllCardsData($bookingCreateDto); // Calculate summary data $summaryData = $this->calculateSummaryData($bookingCreateDto); // Get detailed pricing data for summary sidebar $summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto); $templateData = [ 'form' => $form->createView(), 'bookingDto' => $bookingCreateDto, 'cardsData' => $cardsData, 'summaryData' => $summaryData, 'pricingData' => $summary['pricing'], 'participantErrors' => $participantErrors, ]; // HTMX request: render blocks only if ($this->isHxRequest($request)) { return $this->htmxOobResponse( 'booking/create/step_2.html.twig', ['participant_cards', 'booking_summary'], $templateData ); } // Regular request: render full template 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 { $bookingDto = $this->loadBookingDtoOrFail($request, BookingDto::MODE_CREATE); // 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->enrichWithFreshAvailabilities($bookingDto); // Create form with booking_context option $form = $this->createParticipantForm($bookingDto, $index, [ 'validation_groups' => ['booking_create_step_2'], ]); $form->handleRequest($request); if (true === $form->isSubmitted() && true === $form->isValid()) { // Save BookingDto to session $this->bookingService->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE); // HTMX redirect to cards view return $this->hxRedirect($request, $this->generateUrl('app_booking_create_step_2')); } // Calculate summary data for sidebar $summaryData = $this->calculateSummaryData($bookingDto); // Get detailed pricing data for summary sidebar $summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingDto); // Render form and sidebar with OOB swap using htmxOobResponse // This ensures both initial load and refresh use the same block-based rendering return $this->htmxOobResponse( 'booking/_participant_form.html.twig', ['participant_form', 'booking_summary'], [ 'form' => $form->createView(), 'participantIndex' => $index, 'bookingDto' => $bookingDto, 'summaryData' => $summaryData, 'pricingData' => $summary['pricing'], 'refreshRouteName' => 'app_booking_create_step_2_participant_refresh', 'submitRouteName' => 'app_booking_create_step_2_participant', ] ); } /** * 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 { $bookingDto = $this->loadBookingDtoOrFail($request, BookingDto::MODE_CREATE); // 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->enrichWithFreshAvailabilities($bookingDto); // Use trait method for refresh handling return $this->handleParticipantRefresh( $request, $bookingDto, $index, 'app_booking_create_step_2_participant_refresh', 'app_booking_create_step_2_participant' ); } /** * Ensures the booking DTO has the correct number of participant objects. */ private function ensureCorrectNumberOfParticipants(BookingDto $bookingCreateDto): void { $participantsCount = $this->getParticipantsCount($bookingCreateDto); $participants = $bookingCreateDto->participants; $bookingCreateDto->participants = []; for ($i = 0; $i < $participantsCount; ++$i) { $participant = $participants[$i] ?? new ParticipantDto(); $participant->index = $i; $bookingCreateDto->participants[$i] = $participant; } } /** * Enriches travel data with cached availability information from BusProNet API. */ private function enrichWithFreshAvailabilities(BookingDto $bookingCreateDto): void { $dateId = $bookingCreateDto->travel->id; $availabilities = $this->travelDataService->getAvailabilityData($dateId, true); if (null !== $availabilities) { $this->travelDataService->patchAvailabilities($bookingCreateDto->travel, $availabilities); } } /** * Automatically assigns participants to rooms if they don't have room assignments yet. */ private function autoAssignRoomsIfNeeded(BookingDto $bookingCreateDto): void { // Check if any participants need room assignment $needsAssignment = false; foreach ($bookingCreateDto->participants as $participant) { if (null === $participant->assignedRoomId) { $needsAssignment = true; break; } } if ($needsAssignment) { $this->roomAssignmentService->assignParticipantsToRooms($bookingCreateDto); } } }