feat: improved logic to control field visibility
addresses #869btdphv
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user