wip: finalize implementation
This commit is contained in:
@@ -37,26 +37,21 @@ class BookingParticipantType extends AbstractType
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
// Capture booking context for use in event listeners
|
||||
/** @var BookingDto $bookingContext */
|
||||
$bookingContext = $options['booking_context'];
|
||||
|
||||
// Select field state provider based on edit_mode option
|
||||
$this->fieldStateProvider = $options['edit_mode']
|
||||
$this->fieldStateProvider = BookingDto::MODE_EDIT === $bookingContext->getMode()
|
||||
? $this->editFieldStateProvider
|
||||
: $this->createFieldStateProvider;
|
||||
|
||||
// Capture booking context for use in event listeners
|
||||
$bookingContext = $options['booking_context'];
|
||||
|
||||
$builder
|
||||
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($bookingContext) {
|
||||
$this->onPreSetData($event, $bookingContext);
|
||||
})
|
||||
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($bookingContext) {
|
||||
// Process field handlers FIRST (before form binding and validation)
|
||||
// This ensures data is cleaned before Symfony processes it
|
||||
if (null !== $bookingContext) {
|
||||
$this->processFieldHandlers($event, $bookingContext);
|
||||
}
|
||||
|
||||
// Then rebuild fields with updated states
|
||||
$this->processFieldHandlers($event, $bookingContext);
|
||||
$this->onPreSubmit($event, $bookingContext);
|
||||
});
|
||||
}
|
||||
@@ -99,12 +94,13 @@ class BookingParticipantType extends AbstractType
|
||||
{
|
||||
/** @var ParticipantDto|null $participantData */
|
||||
$participantData = $event->getData();
|
||||
$form = $event->getForm();
|
||||
|
||||
if (null === $participantData) {
|
||||
return;
|
||||
}
|
||||
|
||||
$form = $event->getForm();
|
||||
|
||||
// Card flow: BookingDto passed via options
|
||||
// Accordion flow (if we had one): traverse form tree
|
||||
$bookingDto = $bookingContext ?? $this->fieldStateProvider->getBookingDtoFromForm($form);
|
||||
@@ -128,10 +124,6 @@ class BookingParticipantType extends AbstractType
|
||||
$submittedData = $event->getData();
|
||||
$form = $event->getForm();
|
||||
|
||||
if (false === is_array($submittedData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Card flow: BookingDto passed via options
|
||||
// Accordion flow (if we had one): traverse form tree
|
||||
$bookingDto = $bookingContext ?? $this->fieldStateProvider->getBookingDtoFromForm($form);
|
||||
@@ -148,6 +140,8 @@ class BookingParticipantType extends AbstractType
|
||||
|
||||
// Rebuild all fields with updated states based on submitted data
|
||||
$this->rebuildFieldsWithStates($form, $bookingDto, $participantData->index, $submittedData);
|
||||
|
||||
$event->setData($submittedData);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,13 +213,17 @@ class BookingParticipantType extends AbstractType
|
||||
* @param FormInterface $form The form to modify
|
||||
* @param BookingDto $bookingDto The booking data for context
|
||||
* @param int $participantIndex The participant index
|
||||
* @param array<string, mixed> $formData Submitted form data for state calculation
|
||||
* @param array<string, mixed> $submittedData Submitted form data for state calculation
|
||||
*/
|
||||
private function removeExcludedFields(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array $formData = []): void
|
||||
private function removeExcludedFields(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array &$submittedData = []): void
|
||||
{
|
||||
foreach (ParticipantDto::DYNAMIC_FIELDS as $fieldName) {
|
||||
if ($form->has($fieldName) && !$this->fieldStateProvider->shouldIncludeField($fieldName, $bookingDto, $participantIndex, $formData)) {
|
||||
if (
|
||||
true === $form->has($fieldName)
|
||||
&& false === $this->fieldStateProvider->shouldIncludeField($fieldName, $bookingDto, $participantIndex, $submittedData)
|
||||
) {
|
||||
$form->remove($fieldName);
|
||||
unset($submittedData[$fieldName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,13 +231,11 @@ class BookingParticipantType extends AbstractType
|
||||
/**
|
||||
* Rebuilds all fields with updated states based on submitted data.
|
||||
*/
|
||||
private function rebuildFieldsWithStates(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array $submittedData): void
|
||||
private function rebuildFieldsWithStates(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array &$submittedData): void
|
||||
{
|
||||
// First, remove fields that should be excluded entirely
|
||||
$this->removeExcludedFields($form, $bookingDto, $participantIndex, $submittedData);
|
||||
|
||||
// Clear the form and rebuild from scratch with updated states
|
||||
|
||||
// Rebuild base fields with updated states
|
||||
$baseFields = ['firstName', 'lastName', 'dateOfBirth', 'gender', 'nationality', 'email', 'mobile', 'address', 'bodyDimensions'];
|
||||
foreach ($baseFields as $fieldName) {
|
||||
@@ -286,16 +282,16 @@ class BookingParticipantType extends AbstractType
|
||||
// Insurance fields only available in create mode (API limitation)
|
||||
if (BookingDto::MODE_CREATE === $bookingDto->getMode()) {
|
||||
$dynamicFields['bulkInsuranceBooking'] = CheckboxType::class;
|
||||
$dynamicFields['insurance'] = InsuranceChoiceType::class;
|
||||
$dynamicFields['insurance'] = ChoiceType::class;
|
||||
}
|
||||
|
||||
foreach ($dynamicFields as $fieldName => $fieldType) {
|
||||
if ($this->fieldOptionsProvider->hasFieldOptions($fieldName)) {
|
||||
// Check if field should be included in the form at all
|
||||
if (false === $this->fieldStateProvider->shouldIncludeField($fieldName, $bookingDto, $participantIndex)) {
|
||||
continue;
|
||||
}
|
||||
// Check if field should be included in the form at all
|
||||
if (false === $this->fieldStateProvider->shouldIncludeField($fieldName, $bookingDto, $participantIndex)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->fieldOptionsProvider->hasFieldOptions($fieldName)) {
|
||||
// Get base field options
|
||||
$fieldOptions = $this->fieldOptionsProvider->getFieldOptions($fieldName, $bookingDto, $participantIndex);
|
||||
|
||||
@@ -377,12 +373,10 @@ class BookingParticipantType extends AbstractType
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ParticipantDto::class,
|
||||
'selected_rooms' => [],
|
||||
'edit_mode' => false,
|
||||
'booking_context' => null,
|
||||
]);
|
||||
|
||||
$resolver->setAllowedTypes('selected_rooms', 'array');
|
||||
$resolver->setAllowedTypes('edit_mode', 'bool');
|
||||
$resolver->setAllowedTypes('booking_context', ['null', BookingDto::class]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\BusProNet\Model\Insurance;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Custom form type for insurance selection that provides full Insurance objects to templates.
|
||||
*
|
||||
* This type extends ChoiceType to pass complete Insurance model data to the template layer,
|
||||
* enabling flexible rendering of insurance details including URLs, pricing, and coverage information.
|
||||
*/
|
||||
class InsuranceChoiceType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setRequired('insurances');
|
||||
$resolver->setAllowedTypes('insurances', 'array');
|
||||
|
||||
$resolver->setDefault('choices', function (Options $options) {
|
||||
// Prepend "no insurance" option to eligible insurances
|
||||
return array_merge(
|
||||
[0 => null],
|
||||
$options['insurances']
|
||||
);
|
||||
});
|
||||
|
||||
$resolver->setDefault('choice_value', function ($insurance) {
|
||||
// Handle "no insurance" option (null value at index 0)
|
||||
// instanceof check required because closures in configureOptions receive mixed types
|
||||
if ($insurance instanceof Insurance) {
|
||||
return (string) $insurance->id;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
$resolver->setDefault('choice_label', function ($insurance) {
|
||||
// Handle "no insurance" option (null value at index 0)
|
||||
// instanceof check required because closures in configureOptions receive mixed types
|
||||
if (!$insurance instanceof Insurance) {
|
||||
return 'Keine Versicherung';
|
||||
}
|
||||
|
||||
$label = $insurance->label;
|
||||
|
||||
if (null !== $insurance->price && $insurance->price > 0) {
|
||||
$label .= sprintf(' (€%s)', number_format($insurance->price, 2, ',', '.'));
|
||||
}
|
||||
|
||||
return $label;
|
||||
});
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
// Index insurances by ID for template lookup
|
||||
$insurances = [];
|
||||
foreach ($options['insurances'] as $insurance) {
|
||||
if ($insurance instanceof Insurance) {
|
||||
$insurances[(string) $insurance->id] = $insurance;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass Insurance objects to template indexed by choice value
|
||||
$view->vars['insurances'] = $insurances;
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'insurance_choice';
|
||||
}
|
||||
}
|
||||
@@ -186,7 +186,7 @@ class ParticipantDto
|
||||
*
|
||||
* @return bool True if an insurance is selected
|
||||
*/
|
||||
public function hasInsurance(): bool
|
||||
public function hasInsuranceSelected(): bool
|
||||
{
|
||||
return null !== $this->insurance;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Insurance;
|
||||
use App\BusProNet\Model\Pickup;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Utility\DirectionMapper;
|
||||
@@ -45,13 +46,11 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
* @param ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory Factory for creating room choice loaders
|
||||
* @param ServiceAvailabilityCalculator $serviceAvailabilityCalculator Service for calculating dynamic availability
|
||||
* @param InsuranceMatchingService $insuranceMatchingService Service for matching insurances to participants
|
||||
* @param UrlGeneratorInterface $urlGenerator URL generator for HTMX endpoints
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory,
|
||||
private readonly ServiceAvailabilityCalculator $serviceAvailabilityCalculator,
|
||||
private readonly InsuranceMatchingService $insuranceMatchingService,
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -435,7 +434,18 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
'insurances' => $this->getEligibleInsurances($bookingDto, $participantIndex),
|
||||
'choices' => $this->getEligibleInsurances($bookingDto, $participantIndex),
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => function (?Insurance $insurance) {
|
||||
$label = $insurance->label;
|
||||
|
||||
if (null !== $insurance->price && $insurance->price > 0) {
|
||||
$label .= sprintf(' (€%s)', number_format($insurance->price, 2, ',', '.'));
|
||||
}
|
||||
|
||||
return $label;
|
||||
},
|
||||
'placeholder' => 'Keine Versicherung gewünscht',
|
||||
];
|
||||
|
||||
// Future field providers would be added here, for example:
|
||||
|
||||
@@ -76,7 +76,7 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||
/**
|
||||
* Determines if this handler should process the field based on submitted data.
|
||||
*
|
||||
* Insurance handler should NOT process in edit mode because the BPN API does not
|
||||
* Insurance handler should NOT procefss in edit mode because the BPN API does not
|
||||
* return insurance data. In edit mode, insurance data must be preserved as-is
|
||||
* and passed through to the update endpoint unchanged.
|
||||
*
|
||||
@@ -172,8 +172,6 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||
// Insurance not found - clear selection
|
||||
$participant->insurance = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle form resubmission with existing insurance (automatic reassignment check)
|
||||
@@ -241,15 +239,15 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||
/**
|
||||
* Finds an insurance by ID from the available insurances array.
|
||||
*
|
||||
* @param array<Insurance> $insurances Array of available insurances
|
||||
* @param string|int $insuranceId The insurance ID to find
|
||||
* @param array<Insurance> $insurances Array of available insurances
|
||||
* @param string $insuranceId The insurance ID to find
|
||||
*
|
||||
* @return Insurance|null The found insurance or null if not found
|
||||
*/
|
||||
private function findInsuranceById(array $insurances, string|int $insuranceId): ?Insurance
|
||||
private function findInsuranceById(array $insurances, string $insuranceId): ?Insurance
|
||||
{
|
||||
foreach ($insurances as $insurance) {
|
||||
if ($insurance->id === $insuranceId || (string) $insurance->id === (string) $insuranceId) {
|
||||
if ($insurance->id === $insuranceId) {
|
||||
return $insurance;
|
||||
}
|
||||
}
|
||||
@@ -268,7 +266,7 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||
private function isInsuranceInList(Insurance $targetInsurance, array $insuranceList): bool
|
||||
{
|
||||
foreach ($insuranceList as $insurance) {
|
||||
if ($insurance->id === $targetInsurance->id || (string) $insurance->id === (string) $targetInsurance->id) {
|
||||
if ($insurance->id === $targetInsurance->id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -282,14 +280,14 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||
* This is used to distinguish between a new user selection and a form resubmission
|
||||
* with the existing insurance selection (e.g., when user adds rentals that change travel price).
|
||||
*
|
||||
* @param string|int $selectedInsuranceId The insurance ID from form submission
|
||||
* @param string $selectedInsuranceId The insurance ID from form submission
|
||||
* @param Insurance $currentInsurance The currently assigned insurance from DTO
|
||||
*
|
||||
* @return bool True if they represent the same insurance
|
||||
*/
|
||||
private function isSameInsurance(string|int $selectedInsuranceId, Insurance $currentInsurance): bool
|
||||
private function isSameInsurance(string $selectedInsuranceId, Insurance $currentInsurance): bool
|
||||
{
|
||||
return (string) $currentInsurance->id === (string) $selectedInsuranceId;
|
||||
return $currentInsurance->id === $selectedInsuranceId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -79,10 +79,9 @@ class ParticipantSkiPassFieldHandler extends AbstractParticipantFieldHandler
|
||||
* selection. If the skipass is no longer appropriate for the participant's
|
||||
* age or exceeds the travel date range, it is automatically cleared.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||
* @param BookingDto $bookingDto The booking DTO to update (create or edit)
|
||||
* @param int $participantIndex The index of the participant being processed
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDto $bookingDto The booking DTO to update (create or edit)
|
||||
* @param int $participantIndex The index of the participant being processed
|
||||
*/
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
|
||||
@@ -68,9 +68,6 @@ class ParticipantTransportationInboundFieldHandler extends AbstractParticipantFi
|
||||
|
||||
// Update participant with validated selection
|
||||
$participant->transportationInbound = $validSelection;
|
||||
|
||||
// Backward compatibility: also update deprecated property
|
||||
$participant->transportationServiceFro = $validSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,9 +68,6 @@ class ParticipantTransportationOutboundFieldHandler extends AbstractParticipantF
|
||||
|
||||
// Update participant with validated selection
|
||||
$participant->transportationOutbound = $validSelection;
|
||||
|
||||
// Backward compatibility: also update deprecated property
|
||||
$participant->transportationServiceTo = $validSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user