Files
myep/src/Controller/Booking/Create/Step2Controller.php
T

211 lines
8.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Controller\Booking\Create;
use App\Controller\Booking\Traits\BookingCreateTrait;
use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
use App\Controller\Booking\Traits\ParticipantCardFlowTrait;
use App\Form\BookingCreateStep2Type;
use App\Form\Model\BookingDto;
use App\Form\Service\ParticipantFieldOptionsProvider;
use App\Htmx\HxTrait;
use App\Service\BookingService;
use App\Service\BookingSummaryDataService;
use App\Service\ParticipantCardDataService;
use App\Service\ParticipantPrepopulationService;
use App\Service\RoomAssignmentService;
use App\Service\TravelDataService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
/**
* Handles the second step of the booking creation process using card-based UI.
*
* This controller uses a card overview and lazy-loaded individual participant forms
* for better performance and UX with large groups (50+ participants).
*/
class Step2Controller extends AbstractController
{
use BookingCreateTrait;
use BookingExceptionHandlerTrait;
use HxTrait;
use ParticipantCardFlowTrait;
public function __construct(
private readonly BookingService $bookingService,
private readonly BookingSummaryDataService $summaryDataService,
private readonly TravelDataService $travelDataService,
private readonly RoomAssignmentService $roomAssignmentService,
private readonly ParticipantCardDataService $participantCardService,
private readonly ParticipantFieldOptionsProvider $fieldOptionsProvider,
private readonly ParticipantPrepopulationService $prepopulationService,
) {
}
/**
* Display card grid for all participants.
*/
#[Route('/bookings/create/participants', name: 'app_booking_create_step_2')]
public function index(Request $request): Response
{
// Load or create booking DTO
$result = $this->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);
// 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');
}
// Get complete summary data (pricing, rooms, CMS data)
$summaryData = $this->summaryDataService->getSummaryData($bookingDto);
$templateData = [
'form' => $form->createView(),
'participantIndex' => $index,
'bookingDto' => $bookingDto,
'summaryData' => $summaryData,
'refreshRouteName' => 'app_booking_create_step_2_participant_refresh',
];
return $this->render('booking/create/step_2_participant.html.twig', $templateData);
}
/**
* 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'
);
}
}