feat: improved logic to control field visibility

addresses #869btdphv
This commit is contained in:
Björn Fromme
2026-01-15 10:28:26 +01:00
parent b18fccff9d
commit c793550af5
2 changed files with 381 additions and 295 deletions
+3 -34
View File
@@ -363,9 +363,9 @@ class BookingParticipantType extends AbstractType
// Get base field options // Get base field options
$fieldOptions = $this->fieldOptionsProvider->getFieldOptions($fieldName, $bookingDto, $participantIndex); $fieldOptions = $this->fieldOptionsProvider->getFieldOptions($fieldName, $bookingDto, $participantIndex);
// Skip choice fields without any choices // Skip fields where provider returns empty options
$isChoiceField = ChoiceType::class === $fieldType || RoomAssignmentType::class === $fieldType; // Convention: providers return [] when field has no valid data (no choices, no services, etc.)
if ($isChoiceField && false === $this->hasValidFieldOptions($fieldOptions)) { if (true === empty($fieldOptions)) {
continue; continue;
} }
@@ -386,37 +386,6 @@ class BookingParticipantType extends AbstractType
} }
} }
/**
* Checks if field options contain valid choices for rendering.
*
* This method validates that the field options contain either choices array,
* choice_loader, or other valid choice sources. Empty choice arrays or
* null choice loaders indicate the field should not be rendered.
*
* @param array<string, mixed> $fieldOptions The field options to validate
*
* @return bool True if the field has valid options for rendering, false otherwise
*/
private function hasValidFieldOptions(array $fieldOptions): bool
{
// Check if choices array exists and is not empty
if (false === empty($fieldOptions['choices'])) {
return true;
}
// Check if choice_loader exists and is not null
if (isset($fieldOptions['choice_loader'])) {
return true;
}
// Check for other valid choice sources
if (isset($fieldOptions['choice_list'])) {
return true;
}
return false;
}
/** /**
* Merges field state modifications into existing field options. * Merges field state modifications into existing field options.
* *
@@ -53,24 +53,39 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
* Each provider is a callable that receives the booking DTO and participant * Each provider is a callable that receives the booking DTO and participant
* index and returns appropriate Symfony form field options. * index and returns appropriate Symfony form field options.
* *
* Adding new fields: * Empty Array Convention:
* To add support for a new dynamic field, simply add a new provider here: * Providers MUST return an empty array when the field has no valid data to display.
* This signals to the form builder that the field should be skipped entirely.
* Examples: no choices available, no underlying services exist, etc.
* *
* $this->fieldOptionProviders['newField'] = fn($bookingDto, $participantIndex) => [ * Adding new fields:
* 'label' => 'New Field Label', * To add support for a new dynamic field, add a provider following this pattern:
* 'choices' => $this->generateChoicesFor($bookingDto, $participantIndex), *
* ]; * $this->fieldOptionProviders['newField'] = function (BookingDto $bookingDto, int $participantIndex): array {
* $choices = $this->getChoicesFor($bookingDto, $participantIndex);
*
* if (true === empty($choices)) {
* return [];
* }
*
* return [
* 'label' => 'New Field Label',
* 'choices' => $choices,
* ];
* };
* *
* Provider Pattern Benefits: * Provider Pattern Benefits:
* - Lazy evaluation (options only generated when needed) * - Lazy evaluation (options only generated when needed)
* - Context-aware configuration * - Context-aware configuration
* - Easy to test individual field logic * - Easy to test individual field logic
* - Supports complex interdependencies * - Supports complex interdependencies
* - Unified empty-check pattern for all field types
*/ */
protected function registerFieldOptionProviders(): void protected function registerFieldOptionProviders(): void
{ {
// Room assignment field provider (available for both create and edit workflows) // Room assignment field provider (available for both create and edit workflows)
$this->fieldOptionProviders['assignedRoomId'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []) { // Returns empty array when no room choices available
$this->fieldOptionProviders['assignedRoomId'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array {
$choices = []; $choices = [];
if (BookingDto::MODE_CREATE === $bookingDto->getMode()) { if (BookingDto::MODE_CREATE === $bookingDto->getMode()) {
@@ -85,6 +100,10 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
$choices = $this->convertBookedRoomsToSelectionDtos($bookingDto->booking->rooms); $choices = $this->convertBookedRoomsToSelectionDtos($bookingDto->booking->rooms);
} }
if (true === empty($choices)) {
return [];
}
$singleChoice = 1 === count($choices); $singleChoice = 1 === count($choices);
return [ return [
@@ -100,169 +119,202 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
}; };
// Courses field provider - provides age-appropriate courses from travel data // Courses field provider - provides age-appropriate courses from travel data
$this->fieldOptionProviders['courses'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [ // Returns empty array when no courses available
'label' => 'Kurse', $this->fieldOptionProviders['courses'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array {
'multiple' => true, $choices = $this->filterServicesByAgeConstraints(
'expanded' => true,
'required' => false,
'choices' => $this->filterServicesByAgeConstraints(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES), $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES),
$bookingDto, $bookingDto,
$participantIndex $participantIndex
), );
'choice_value' => 'id',
'choice_label' => fn (?Service $service) => $service?->label,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
$attributes = []; if (true === empty($choices)) {
return [];
}
// Add service description as data attribute for frontend use return [
if (null !== $service->description && '' !== trim($service->description)) { 'label' => 'Kurse',
$attributes['data-description'] = $service->description; 'multiple' => true,
} 'expanded' => true,
'required' => false,
'choices' => $choices,
'choice_value' => 'id',
'choice_label' => fn (?Service $service) => $service?->label,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
// Make readonly if service is unavailable (intelligently handles edit mode) $attributes = [];
if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'courses')) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = 'ausgebucht';
}
return $attributes; // Add service description as data attribute for frontend use
}, if (null !== $service->description && '' !== trim($service->description)) {
]; $attributes['data-description'] = $service->description;
}
// Make readonly if service is unavailable (intelligently handles edit mode)
if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'courses')) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = 'ausgebucht';
}
return $attributes;
},
];
};
// Additional services field provider - provides age-appropriate additional services with mandatory pre-selection // Additional services field provider - provides age-appropriate additional services with mandatory pre-selection
$this->fieldOptionProviders['additionalServices'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [ // Returns empty array when no additional services available
'label' => 'Zusatzleistungen', $this->fieldOptionProviders['additionalServices'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array {
'multiple' => true, $choices = $this->filterServicesByAgeConstraints(
'expanded' => true,
'required' => false,
'choices' => $this->filterServicesByAgeConstraints(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL), $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL),
$bookingDto, $bookingDto,
$participantIndex $participantIndex
), );
'choice_value' => 'id',
'choice_label' => fn (?Service $service) => $service?->label,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
$attributes = []; if (true === empty($choices)) {
return [];
}
// Make readonly and checked for mandatory services (pre-selection handled by service layer) return [
if (true === $service->mandatory) { 'label' => 'Zusatzleistungen',
$attributes['checked'] = true; 'multiple' => true,
$attributes['readonly'] = true; 'expanded' => true,
$attributes['data-tooltip'] = 'Diese Leistung ist nicht abwählbar'; 'required' => false,
} 'choices' => $choices,
'choice_value' => 'id',
'choice_label' => fn (?Service $service) => $service?->label,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
// Add service description as data attribute for frontend use $attributes = [];
if (null !== $service->description && '' !== trim($service->description)) {
$attributes['data-description'] = $service->description;
}
// Make readonly if service is unavailable (only if not already mandatory) // Make readonly and checked for mandatory services (pre-selection handled by service layer)
if (false === $service->mandatory && $this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'additionalServices')) { if (true === $service->mandatory) {
$attributes['readonly'] = true; $attributes['checked'] = true;
$attributes['data-tooltip'] = 'ausgebucht'; $attributes['readonly'] = true;
} $attributes['data-tooltip'] = 'Diese Leistung ist nicht abwählbar';
}
return $attributes; // Add service description as data attribute for frontend use
}, if (null !== $service->description && '' !== trim($service->description)) {
]; $attributes['data-description'] = $service->description;
}
// Make readonly if service is unavailable (only if not already mandatory)
if (false === $service->mandatory && $this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'additionalServices')) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = 'ausgebucht';
}
return $attributes;
},
];
};
// Board field provider - provides age-appropriate board options from travel data // Board field provider - provides age-appropriate board options from travel data
$this->fieldOptionProviders['board'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [ // Returns empty array when no board options available
'label' => 'Verpflegung', $this->fieldOptionProviders['board'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array {
'multiple' => true, $choices = $this->filterServicesByAgeConstraints(
'expanded' => true,
'required' => false,
'choices' => $this->filterServicesByAgeConstraints(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD), $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD),
$bookingDto, $bookingDto,
$participantIndex $participantIndex
), );
'choice_value' => 'id',
'choice_label' => fn (?Service $service) => $service?->label,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
$attributes = []; if (true === empty($choices)) {
return [];
}
// Add service description as data attribute for frontend use return [
if (null !== $service->description && '' !== trim($service->description)) { 'label' => 'Verpflegung',
$attributes['data-description'] = $service->description; 'multiple' => true,
} 'expanded' => true,
'required' => false,
'choices' => $choices,
'choice_value' => 'id',
'choice_label' => fn (?Service $service) => $service?->label,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
// Make readonly if service is unavailable (intelligently handles edit mode) $attributes = [];
if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'board')) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = 'ausgebucht';
}
return $attributes; // Add service description as data attribute for frontend use
}, if (null !== $service->description && '' !== trim($service->description)) {
]; $attributes['data-description'] = $service->description;
}
// Make readonly if service is unavailable (intelligently handles edit mode)
if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'board')) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = 'ausgebucht';
}
return $attributes;
},
];
};
// Veg (vegetarian/vegan) field provider - provides dietary preference options as radio buttons (mutually exclusive) // Veg (vegetarian/vegan) field provider - provides dietary preference options as radio buttons (mutually exclusive)
$this->fieldOptionProviders['veg'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [ // Returns empty array when no dietary options available
'label' => 'Verpflegungswunsch', $this->fieldOptionProviders['veg'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array {
'multiple' => false, $choices = $this->filterServicesByAgeConstraints(
'expanded' => true,
'required' => false,
'choices' => $this->filterServicesByAgeConstraints(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_VEG, true), $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_VEG, true),
$bookingDto, $bookingDto,
$participantIndex $participantIndex
), );
'choice_value' => 'id',
'choice_label' => fn (?Service $service) => $service?->label,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
$attributes = []; if (true === empty($choices)) {
return [];
}
// Add service description as data attribute for frontend use return [
if (null !== $service->description && '' !== trim($service->description)) { 'label' => 'Verpflegungswunsch',
$attributes['data-description'] = $service->description; 'multiple' => false,
} 'expanded' => true,
'required' => false,
'choices' => $choices,
'choice_value' => 'id',
'choice_label' => fn (?Service $service) => $service?->label,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
// Check age restriction first (takes precedence over availability) $attributes = [];
$ageEvaluator = new ServiceAgeEvaluator();
if ($ageEvaluator->canEvaluate($service) // Add service description as data attribute for frontend use
&& false === $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex)) { if (null !== $service->description && '' !== trim($service->description)) {
$attributes['readonly'] = true; $attributes['data-description'] = $service->description;
$attributes['data-tooltip'] = $this->getAgeRestrictionTooltip($service); }
// Check age restriction first (takes precedence over availability)
$ageEvaluator = new ServiceAgeEvaluator();
if ($ageEvaluator->canEvaluate($service)
&& false === $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex)) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = $this->getAgeRestrictionTooltip($service);
return $attributes;
}
// Make readonly if service is unavailable (intelligently handles edit mode)
if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'veg')) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = 'ausgebucht';
}
return $attributes; return $attributes;
} },
];
// Make readonly if service is unavailable (intelligently handles edit mode) };
if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'veg')) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = 'ausgebucht';
}
return $attributes;
},
];
// Rentals field provider - provides age-appropriate rental options filtered by selected skipass duration // Rentals field provider - provides age-appropriate rental options filtered by selected skipass duration
$this->fieldOptionProviders['rentals'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [ // Returns empty array when no rental options available
'label' => 'Leihmaterial', $this->fieldOptionProviders['rentals'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array {
'multiple' => true, $choices = $this->filterServicesByAgeConstraints(
'expanded' => true,
'required' => false,
'choices' => $this->filterServicesByAgeConstraints(
$this->filterRentalsBySkiPassDuration( $this->filterRentalsBySkiPassDuration(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true), $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true),
$bookingDto, $bookingDto,
@@ -270,30 +322,42 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
), ),
$bookingDto, $bookingDto,
$participantIndex $participantIndex
), );
'choice_value' => 'id',
'choice_label' => fn (?Service $service) => $service?->label,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
$attributes = []; if (true === empty($choices)) {
return [];
}
// Add service description as data attribute for frontend use return [
if (null !== $service->description && '' !== trim($service->description)) { 'label' => 'Leihmaterial',
$attributes['data-description'] = $service->description; 'multiple' => true,
} 'expanded' => true,
'required' => false,
'choices' => $choices,
'choice_value' => 'id',
'choice_label' => fn (?Service $service) => $service?->label,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
// Make readonly if service is unavailable (intelligently handles edit mode) $attributes = [];
if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'rentals')) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = 'ausgebucht';
}
return $attributes; // Add service description as data attribute for frontend use
}, if (null !== $service->description && '' !== trim($service->description)) {
]; $attributes['data-description'] = $service->description;
}
// Make readonly if service is unavailable (intelligently handles edit mode)
if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'rentals')) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = 'ausgebucht';
}
return $attributes;
},
];
};
// Rental insurance field provider - provides rental insurance options when rental services are selected // Rental insurance field provider - provides rental insurance options when rental services are selected
$this->fieldOptionProviders['rentalInsurance'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [ $this->fieldOptionProviders['rentalInsurance'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
@@ -319,49 +383,58 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
]; ];
// Skipass field provider - provides age-appropriate skipass options from travel data filtered by date range // Skipass field provider - provides age-appropriate skipass options from travel data filtered by date range
$this->fieldOptionProviders['skiPass'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [ // Returns empty array when no skipass options available
'label' => 'Skipass', $this->fieldOptionProviders['skiPass'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array {
'multiple' => false, $choices = $this->filterSkiPassChoices(
'expanded' => true,
'required' => true,
'choices' => $this->filterSkiPassChoices(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true), $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true),
$bookingDto, $bookingDto,
$participantIndex $participantIndex
), );
'choice_value' => 'id',
'choice_label' => fn (?Service $service) => $service?->label,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
$attributes = []; if (true === empty($choices)) {
return [];
}
// Add service description as data attribute for frontend use return [
if (null !== $service->description && '' !== trim($service->description)) { 'label' => 'Skipass',
$attributes['data-description'] = $service->description; 'multiple' => false,
} 'expanded' => true,
'required' => true,
'choices' => $choices,
'choice_value' => 'id',
'choice_label' => fn (?Service $service) => $service?->label,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
// Check age restriction first (takes precedence over availability) $attributes = [];
$ageEvaluator = new ServiceAgeEvaluator();
if ($ageEvaluator->canEvaluate($service) // Add service description as data attribute for frontend use
&& false === $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex)) { if (null !== $service->description && '' !== trim($service->description)) {
$attributes['readonly'] = true; $attributes['data-description'] = $service->description;
$attributes['data-tooltip'] = $this->getAgeRestrictionTooltip($service); }
// Check age restriction first (takes precedence over availability)
$ageEvaluator = new ServiceAgeEvaluator();
if ($ageEvaluator->canEvaluate($service)
&& false === $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex)) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = $this->getAgeRestrictionTooltip($service);
return $attributes;
}
// Make readonly if service is unavailable (intelligently handles edit mode)
if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'skiPass')) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = 'ausgebucht';
}
return $attributes; return $attributes;
} },
];
// Make readonly if service is unavailable (intelligently handles edit mode) };
if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'skiPass')) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = 'ausgebucht';
}
return $attributes;
},
];
// Room remarks field provider - provides textarea for room-specific remarks (only for 'mbz' rooms) // Room remarks field provider - provides textarea for room-specific remarks (only for 'mbz' rooms)
$this->fieldOptionProviders['remarksRoom'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [ $this->fieldOptionProviders['remarksRoom'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
@@ -376,89 +449,124 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
// Transportation field providers - handles outbound/inbound transportation and pickup selection // Transportation field providers - handles outbound/inbound transportation and pickup selection
// Outbound Transportation // Outbound Transportation
$this->fieldOptionProviders['transportationOutbound'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [ // Returns empty array when no transportation options available
'label' => 'Hinfahrt', $this->fieldOptionProviders['transportationOutbound'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array {
'choices' => $this->filterTransportationChoices( $choices = $this->filterTransportationChoices(
$bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL), $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL),
$bookingDto, $bookingDto,
$participantIndex $participantIndex
), );
'choice_label' => fn (Service $service) => $service?->label,
'choice_value' => 'id',
'expanded' => true,
'multiple' => false,
'required' => true,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
$attributes = []; if (true === empty($choices)) {
return [];
}
// Add service description as data attribute for frontend use return [
if (null !== $service->description && '' !== trim($service->description)) { 'label' => 'Hinfahrt',
$attributes['data-description'] = $service->description; 'choices' => $choices,
} 'choice_label' => fn (Service $service) => $service?->label,
'choice_value' => 'id',
'expanded' => true,
'multiple' => false,
'required' => true,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
// Make readonly if service is unavailable (intelligently handles edit mode) $attributes = [];
if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'transportationOutbound')) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = 'ausgebucht';
}
return $attributes; // Add service description as data attribute for frontend use
}, if (null !== $service->description && '' !== trim($service->description)) {
]; $attributes['data-description'] = $service->description;
}
// Make readonly if service is unavailable (intelligently handles edit mode)
if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'transportationOutbound')) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = 'ausgebucht';
}
return $attributes;
},
];
};
// Inbound Transportation // Inbound Transportation
$this->fieldOptionProviders['transportationInbound'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [ // Returns empty array when no transportation options available
'label' => 'Rückfahrt', $this->fieldOptionProviders['transportationInbound'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array {
'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL), $choices = $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL);
'choice_label' => fn (Service $service) => $service?->label,
'choice_value' => 'id',
'expanded' => true,
'multiple' => false,
'required' => true,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
$attributes = []; if (true === empty($choices)) {
return [];
}
// Add service description as data attribute for frontend use return [
if (null !== $service->description && '' !== trim($service->description)) { 'label' => 'Rückfahrt',
$attributes['data-description'] = $service->description; 'choices' => $choices,
} 'choice_label' => fn (Service $service) => $service?->label,
'choice_value' => 'id',
'expanded' => true,
'multiple' => false,
'required' => true,
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
if (null === $service) {
return [];
}
// Make readonly if service is unavailable (intelligently handles edit mode) $attributes = [];
if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'transportationInbound')) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = 'ausgebucht';
}
return $attributes; // Add service description as data attribute for frontend use
}, if (null !== $service->description && '' !== trim($service->description)) {
]; $attributes['data-description'] = $service->description;
}
// Make readonly if service is unavailable (intelligently handles edit mode)
if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'transportationInbound')) {
$attributes['readonly'] = true;
$attributes['data-tooltip'] = 'ausgebucht';
}
return $attributes;
},
];
};
// Pickup (conditional - only shown when either transportation direction is bus) // Pickup (conditional - only shown when either transportation direction is bus)
// Uses outbound pickups list, applies to both directions // Uses outbound pickups list, applies to both directions
$this->fieldOptionProviders['pickup'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [ // Returns empty array when no pickup options available
'label' => 'Zu- und Ausstieg', $this->fieldOptionProviders['pickup'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array {
'choices' => $bookingDto->travel->pickupsOutbound, $choices = $bookingDto->travel->pickupsOutbound;
'choice_label' => fn (?Pickup $pickup) => $pickup?->getLabel(),
'choice_value' => 'id', if (true === empty($choices)) {
'expanded' => true, // Radio buttons in table layout like other services return [];
'multiple' => false, }
'required' => true,
]; return [
'label' => 'Zu- und Ausstieg',
'choices' => $choices,
'choice_label' => fn (?Pickup $pickup) => $pickup?->getLabel(),
'choice_value' => 'id',
'expanded' => true, // Radio buttons in table layout like other services
'multiple' => false,
'required' => true,
];
};
// Parking (conditional - only shown when outbound transportation is PKW) // Parking (conditional - only shown when outbound transportation is PKW)
// Simple checkbox since there's only ever one parking type // Simple checkbox since there's only ever one parking type
$this->fieldOptionProviders['parking'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [ // Returns empty array when no parking services exist to prevent field from rendering
'label' => $this->getParkingCheckboxLabel($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_PARKING)), $this->fieldOptionProviders['parking'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array {
'required' => false, $parkingServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_PARKING);
]; if (true === empty($parkingServices)) {
return [];
}
return [
'label' => $this->getParkingCheckboxLabel($parkingServices),
'required' => false,
];
};
// Bulk insurance booking checkbox (applicant only - controls insurance assignment for all participants) // Bulk insurance booking checkbox (applicant only - controls insurance assignment for all participants)
// Only registered in create mode - insurance cannot be modified in edit mode due to API limitation // Only registered in create mode - insurance cannot be modified in edit mode due to API limitation
@@ -475,16 +583,25 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
// Insurance field provider - provides age and eligibility filtered insurances for participants // Insurance field provider - provides age and eligibility filtered insurances for participants
// Only registered in create mode - insurance cannot be modified in edit mode due to API limitation // Only registered in create mode - insurance cannot be modified in edit mode due to API limitation
$this->fieldOptionProviders['insurance'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [ // Returns empty array when no insurance options available
'label' => 'Reiseversicherung', $this->fieldOptionProviders['insurance'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array {
'multiple' => false, $choices = $this->getEligibleInsurances($bookingDto, $participantIndex);
'expanded' => true,
'required' => false, if (true === empty($choices)) {
'placeholder' => false, // Disable default placeholder - synthetic "keine Versicherung gewünscht" option injected instead return [];
'choices' => $this->getEligibleInsurances($bookingDto, $participantIndex), }
'choice_value' => 'id',
'choice_label' => fn (?Insurance $insurance) => $insurance?->label, return [
]; 'label' => 'Reiseversicherung',
'multiple' => false,
'expanded' => true,
'required' => false,
'placeholder' => false, // Disable default placeholder - synthetic "keine Versicherung gewünscht" option injected instead
'choices' => $choices,
'choice_value' => 'id',
'choice_label' => fn (?Insurance $insurance) => $insurance?->label,
];
};
// Purchase voucher field provider - redemption code for vouchers that apply to complete booking // Purchase voucher field provider - redemption code for vouchers that apply to complete booking
// Collected from all participants and aggregated into single <gutscheine> collection in booking payload // Collected from all participants and aggregated into single <gutscheine> collection in booking payload