feat: always show all available ski passes but readonly if age doesn't match

This commit is contained in:
Björn Fromme
2026-03-16 12:00:56 +01:00
parent b57d849ecd
commit 1fd9f8d1f8
6 changed files with 191 additions and 18 deletions
@@ -16,6 +16,7 @@ use App\Form\Service\Abstract\AbstractFieldOptionsProvider;
use App\Service\BookingPriceCalculatorService;
use App\Service\InsuranceService;
use App\Service\ServiceAvailabilityCalculator;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Provides dynamic field options for participant form fields.
@@ -40,6 +41,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
private readonly ServiceAvailabilityCalculator $serviceAvailabilityCalculator,
private readonly InsuranceService $insuranceService,
private readonly BookingPriceCalculatorService $priceCalculatorService,
private readonly TranslatorInterface $translator,
) {
parent::__construct();
}
@@ -278,7 +280,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
'multiple' => false,
'expanded' => true,
'required' => true,
'choices' => $this->filterServicesByAgeConstraints(
'choices' => $this->filterSkiPassChoices(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true),
$bookingDto,
$participantIndex
@@ -297,6 +299,16 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
$attributes['data-description'] = $service->description;
}
// 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;
@@ -764,6 +776,137 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
});
}
/**
* Filters ski pass choices for display.
*
* Unlike other services, ski passes are shown even when age-restricted
* (marked as readonly instead of hidden) to avoid user confusion about
* included ski passes not being visible.
*
* Baby filtering is preserved: babies only see services explicitly
* including their age range.
*
* @param Service[] $services Array of ski pass Service objects to filter
* @param BookingDto $bookingDto The booking DTO containing participant data
* @param int $participantIndex Index of the participant to evaluate
*
* @return Service[] Filtered array of ski pass services
*/
private function filterSkiPassChoices(array $services, BookingDto $bookingDto, int $participantIndex): array
{
$participant = $bookingDto->getParticipant($participantIndex);
// If no birthdate provided, return empty array (handled by field visibility conditions)
if (null === $participant || null === $participant->dateOfBirth) {
return [];
}
// Check if participant is a baby
$age = $participant->getAge($bookingDto->travel->dateFrom);
$isBaby = null !== $age && $age <= Constants::BABY_MAX_AGE;
// For babies: keep existing filtering logic (only show services with explicit baby age ranges)
if ($isBaby) {
return $this->filterServicesByAgeConstraints($services, $bookingDto, $participantIndex);
}
// For non-babies: return all services (age-restricted ones will be marked readonly in choice_attr)
return $services;
}
/**
* Generates a German tooltip explaining age restrictions for a service.
*
* @param Service $service The service with age restrictions
*
* @return string German tooltip text explaining the restriction
*/
private function getAgeRestrictionTooltip(Service $service): string
{
$constraint = $this->translateAgeConstraint($service);
return $this->translator->trans('service.age_constraint.prefix', ['%constraint%' => $constraint]);
}
/**
* Translates a service's age constraints to a localized description.
*
* @param Service $service The service with age constraints
*
* @return string Translated constraint description
*/
private function translateAgeConstraint(Service $service): string
{
return match ($service->ageConstraintType) {
'absolute_age' => $this->translateAbsoluteAgeConstraint($service),
'birth_year' => $this->translateBirthYearConstraint($service),
'mixed' => $this->translateAbsoluteAgeConstraint($service)
.$this->translator->trans('service.age_constraint.mixed.separator')
.$this->translateBirthYearConstraint($service),
default => '',
};
}
/**
* Translates absolute age constraints (ageFrom/ageTo).
*/
private function translateAbsoluteAgeConstraint(Service $service): string
{
if (null !== $service->ageFrom && null !== $service->ageTo) {
return $this->translator->trans('service.age_constraint.absolute_age.range', [
'%ageFrom%' => $service->ageFrom,
'%ageTo%' => $service->ageTo,
]);
}
if (null !== $service->ageFrom) {
return $this->translator->trans('service.age_constraint.absolute_age.min', [
'%ageFrom%' => $service->ageFrom,
]);
}
if (null !== $service->ageTo) {
return $this->translator->trans('service.age_constraint.absolute_age.max', [
'%ageTo%' => $service->ageTo,
]);
}
return '';
}
/**
* Translates birth year constraints (birthYearFrom/birthYearTo).
*/
private function translateBirthYearConstraint(Service $service): string
{
if (null !== $service->birthYearFrom && null !== $service->birthYearTo) {
if ($service->birthYearFrom === $service->birthYearTo) {
return $this->translator->trans('service.age_constraint.birth_year.single', [
'%year%' => $service->birthYearFrom,
]);
}
return $this->translator->trans('service.age_constraint.birth_year.range', [
'%yearFrom%' => $service->birthYearFrom,
'%yearTo%' => $service->birthYearTo,
]);
}
if (null !== $service->birthYearFrom) {
return $this->translator->trans('service.age_constraint.birth_year.min', [
'%yearFrom%' => $service->birthYearFrom,
]);
}
if (null !== $service->birthYearTo) {
return $this->translator->trans('service.age_constraint.birth_year.max', [
'%yearTo%' => $service->birthYearTo,
]);
}
return '';
}
/**
* Generates label for rental insurance checkbox including pricing information.
*/