getOrCreateBookingCreateDto($this->bookingService, $request); if ($result instanceof Response) { return $result; } $bookingCreateDto = $result; // Enrich with fresh availability data $this->enrichWithFreshAvailabilities($bookingCreateDto); // Validate step access if ($redirect = $this->validateStepAccess($bookingCreateDto, 2)) { return $redirect; } // Ensure correct number of participants $this->ensureCorrectNumberOfParticipants($bookingCreateDto); $participantsCount = $this->getParticipantsCount($bookingCreateDto); // Auto-assign participants to rooms if not already assigned $this->autoAssignRoomsIfNeeded($bookingCreateDto); // Pre-select mandatory services for participants with birth dates $this->bookingService->preselectMandatoryServices($bookingCreateDto); $this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE); $form = $this->createForm(BookingCreateStep2Type::class, $bookingCreateDto, [ 'attr' => ['novalidate' => 'novalidate'], 'validation_groups' => ['booking_create_step_2'], ]); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $bookingCreateDto->currentStep = 3; $this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE); return $this->redirectToRoute('app_booking_create_step_3'); } $summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto); $roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($bookingCreateDto); $availableRooms = $bookingCreateDto->travel->getAvailableRooms(); $groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($bookingCreateDto->getSelectedRooms(), $availableRooms); $participantPrices = $this->priceCalculator->calculateAllParticipantIndividualPrices($bookingCreateDto); return $this->render('booking/create_step_2.html.twig', [ 'bookingCreateDto' => $bookingCreateDto, 'participantsCount' => $participantsCount, 'pricingData' => $summary['pricing'], 'assignmentCounts' => $roomAssignmentCounts, 'participantPrices' => $participantPrices, 'form' => $form->createView(), 'groupedSelectedRooms' => $groupedSelectedRooms, ]); } /** * Handles HTMX requests for dynamic form updates when room selections change by submitting the form * without validation and returning a freshly rendered instance. */ #[Route('/bookings/create/participants/refresh', name: 'app_booking_create_step_2_refresh', methods: ['POST'])] public function refreshParticipantForm(Request $request): Response { $result = $this->getOrCreateBookingCreateDtoForHtmx($this->bookingService, $request); if ($result instanceof Response) { return $result; } $bookingCreateDto = $result; // Enrich with fresh availability data $this->enrichWithFreshAvailabilities($bookingCreateDto); $participantsCount = $this->getParticipantsCount($bookingCreateDto); // Process form data without validation to capture current state $form = $this->createForm(BookingCreateStep2Type::class, $bookingCreateDto, [ 'attr' => ['novalidate' => 'novalidate'], 'validation_groups' => false, ]); $form->handleRequest($request); // Pre-select mandatory services after form processing but before pricing calculation $this->bookingService->preselectMandatoryServices($bookingCreateDto); $this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE); // Collect notifications from all participants $notifications = $this->collectParticipantNotifications($bookingCreateDto); $summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto); $roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($bookingCreateDto); $availableRooms = $bookingCreateDto->travel->getAvailableRooms(); $groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($bookingCreateDto->getSelectedRooms(), $availableRooms); $participantPrices = $this->priceCalculator->calculateAllParticipantIndividualPrices($bookingCreateDto); // The DTO is now updated with the latest selection and submitted data has been cleaned. // We can now render the blocks with the fresh data. $response = $this->htmxOobResponse( 'booking/create_step_2.html.twig', ['participants_form', 'booking_summary'], [ 'form' => $form->createView(), 'bookingCreateDto' => $bookingCreateDto, 'participantsCount' => $participantsCount, 'pricingData' => $summary['pricing'], 'assignmentCounts' => $roomAssignmentCounts, 'participantPrices' => $participantPrices, 'groupedSelectedRooms' => $groupedSelectedRooms, ] ); // Add notifications to HTMX trigger header if any exist if ([] !== $notifications) { $response->headers->set('HX-Trigger', json_encode([ 'showNotifications' => ['notifications' => $notifications], ])); } return $response; } /** * Ensures the booking DTO has the correct number of participant objects. * * Creates or reuses participant DTOs to match the required participant count * based on room selections. Preserves existing participant data when possible * and assigns proper index values. * * @param BookingDto $bookingCreateDto The booking DTO to update */ 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. * * Fetches availability data with short-term caching and patches the travel object * to ensure service availability is reasonably up-to-date while reducing API calls. * This is essential for accurate pricing and service selection during the booking process. * * @param BookingDto $bookingCreateDto The booking DTO containing travel data to enrich */ 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. * * This is called when entering Step 2 to ensure all participants have room assignments * based on the selected rooms from Step 1. Only assigns if participants are unassigned. * * @param BookingDto $bookingCreateDto The booking DTO with participants and room selections */ 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); } } /** * Collects all notifications from participants and clears them. * * @param BookingDto $bookingCreateDto The booking DTO containing participants * * @return array Array of notification messages */ private function collectParticipantNotifications(BookingDto $bookingCreateDto): array { $notifications = []; foreach ($bookingCreateDto->participants as $participant) { if ([] !== $participant->notifications) { foreach ($participant->notifications as $notification) { $notifications[] = $notification; } // Clear notifications after collection $participant->notifications = []; } } return $notifications; } }