772 lines
33 KiB
PHP
772 lines
33 KiB
PHP
<?php
|
|
|
|
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;
|
|
use App\Form\Model\BookingCreateDto;
|
|
use App\Form\Model\BookingDtoInterface;
|
|
use App\Form\Service\Abstract\AbstractFieldOptionsProvider;
|
|
use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory;
|
|
use App\Service\InsuranceMatchingService;
|
|
use App\Service\ServiceAvailabilityCalculator;
|
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|
|
|
/**
|
|
* Provides dynamic field options for participant form fields.
|
|
*
|
|
* This service generates context-aware Symfony form field options for
|
|
* dynamic fields in the participant form system. It handles fields that
|
|
* require options to be calculated based on the current booking context,
|
|
* participant data, and business logic.
|
|
*
|
|
* Key Responsibilities:
|
|
* - Manages field option providers for dynamic fields
|
|
* - Provides context-aware field configurations
|
|
* - Handles interdependent field relationships
|
|
* - Supports extensible field option generation
|
|
*
|
|
* The provider uses a callable pattern for field options, enabling
|
|
* lazy evaluation and complex conditional field behavior.
|
|
*/
|
|
class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|
{
|
|
/**
|
|
* Initializes the provider with required dependencies.
|
|
*
|
|
* The provider automatically registers all field option providers during
|
|
* construction to ensure they're available for form building. This approach
|
|
* keeps all field configuration logic centralized and makes it easy to add
|
|
* new dynamic fields.
|
|
*
|
|
* @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();
|
|
}
|
|
|
|
/**
|
|
* Registers all field option providers during service initialization.
|
|
*
|
|
* This method defines the option generation logic for each supported dynamic field.
|
|
* Each provider is a callable that receives the booking DTO and participant
|
|
* index and returns appropriate Symfony form field options.
|
|
*
|
|
* Adding new fields:
|
|
* To add support for a new dynamic field, simply add a new provider here:
|
|
*
|
|
* $this->fieldOptionProviders['newField'] = fn($bookingDto, $participantIndex) => [
|
|
* 'label' => 'New Field Label',
|
|
* 'choices' => $this->generateChoicesFor($bookingDto, $participantIndex),
|
|
* ];
|
|
*
|
|
* Provider Pattern Benefits:
|
|
* - Lazy evaluation (options only generated when needed)
|
|
* - Context-aware configuration
|
|
* - Easy to test individual field logic
|
|
* - Supports complex interdependencies
|
|
*/
|
|
protected function registerFieldOptionProviders(): void
|
|
{
|
|
// Room assignment field provider (only available for create workflow)
|
|
$this->fieldOptionProviders['assignedRoomId'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => 'Zimmer',
|
|
'placeholder' => 'Bitte wählen',
|
|
// Use factory to create context-aware choice loader that:
|
|
// - Shows only available rooms for this participant
|
|
// - Excludes rooms already assigned to other participants
|
|
// - Respects room capacity and booking constraints
|
|
'choice_loader' => $bookingDto instanceof BookingCreateDto
|
|
? $this->roomChoiceLoaderFactory->create(
|
|
$bookingDto->participants,
|
|
$bookingDto->getSelectedRooms(),
|
|
$participantIndex
|
|
)
|
|
: null,
|
|
];
|
|
|
|
// Courses field provider - provides age-appropriate courses from travel data
|
|
$this->fieldOptionProviders['courses'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => 'Kurse',
|
|
'multiple' => true,
|
|
'expanded' => true,
|
|
'required' => false,
|
|
'choices' => $this->filterServicesByAgeConstraints(
|
|
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES),
|
|
$bookingDto,
|
|
$participantIndex
|
|
),
|
|
'choice_value' => 'id',
|
|
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
|
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
|
if (null === $service) {
|
|
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
|
|
if ($this->isServiceUnavailableForParticipant($service, $bookingDto, $participantIndex)) {
|
|
$attributes['readonly'] = true;
|
|
$attributes['data-tooltip'] = 'ausgebucht';
|
|
}
|
|
|
|
return $attributes;
|
|
},
|
|
];
|
|
|
|
// Additional services field provider - provides age-appropriate additional services with mandatory pre-selection
|
|
$this->fieldOptionProviders['additionalServices'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => 'Zusatzleistungen',
|
|
'multiple' => true,
|
|
'expanded' => true,
|
|
'required' => false,
|
|
'choices' => $this->filterServicesByAgeConstraints(
|
|
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL),
|
|
$bookingDto,
|
|
$participantIndex
|
|
),
|
|
'choice_value' => 'id',
|
|
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
|
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
|
if (null === $service) {
|
|
return [];
|
|
}
|
|
|
|
$attributes = [];
|
|
|
|
// Make readonly and checked for mandatory services (pre-selection handled by service layer)
|
|
if (true === $service->mandatory) {
|
|
$attributes['checked'] = true;
|
|
$attributes['readonly'] = true;
|
|
$attributes['class'] = 'text-pink';
|
|
$attributes['data-tooltip'] = 'Diese Leistung ist nicht abwählbar';
|
|
}
|
|
|
|
// 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->isServiceUnavailableForParticipant($service, $bookingDto, $participantIndex)) {
|
|
$attributes['readonly'] = true;
|
|
$attributes['data-tooltip'] = 'ausgebucht';
|
|
}
|
|
|
|
return $attributes;
|
|
},
|
|
];
|
|
|
|
// Board field provider - provides age-appropriate board options from travel data
|
|
$this->fieldOptionProviders['board'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => 'Verpflegung',
|
|
'multiple' => true,
|
|
'expanded' => true,
|
|
'required' => false,
|
|
'choices' => $this->filterServicesByAgeConstraints(
|
|
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD),
|
|
$bookingDto,
|
|
$participantIndex
|
|
),
|
|
'choice_value' => 'id',
|
|
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
|
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
|
if (null === $service) {
|
|
return [];
|
|
}
|
|
|
|
$attributes = [];
|
|
|
|
// Make readonly if service is unavailable
|
|
if ($this->isServiceUnavailableForParticipant($service, $bookingDto, $participantIndex)) {
|
|
$attributes['readonly'] = true;
|
|
$attributes['data-tooltip'] = 'ausgebucht';
|
|
}
|
|
|
|
return $attributes;
|
|
},
|
|
];
|
|
|
|
// Rentals field provider - provides age-appropriate rental options filtered by selected skipass duration
|
|
$this->fieldOptionProviders['rentals'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => 'Leihmaterial',
|
|
'multiple' => true,
|
|
'expanded' => true,
|
|
'required' => false,
|
|
'choices' => $this->filterServicesByAgeConstraints(
|
|
$this->filterRentalsBySkiPassDuration(
|
|
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true, true),
|
|
$bookingDto,
|
|
$participantIndex
|
|
),
|
|
$bookingDto,
|
|
$participantIndex
|
|
),
|
|
'choice_value' => 'id',
|
|
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
|
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
|
if (null === $service) {
|
|
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
|
|
if ($this->isServiceUnavailableForParticipant($service, $bookingDto, $participantIndex)) {
|
|
$attributes['readonly'] = true;
|
|
$attributes['data-tooltip'] = 'ausgebucht';
|
|
}
|
|
|
|
return $attributes;
|
|
},
|
|
];
|
|
|
|
// Rental insurance field provider - provides rental insurance options when rental services are selected
|
|
$this->fieldOptionProviders['rentalInsurance'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => $this->getRentalInsuranceCheckboxLabel($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTAL_INSURANCE, true, true)),
|
|
'required' => false,
|
|
'property_path' => 'rentalInsuranceSelected',
|
|
'help' => $this->getRentalInsuranceDescription($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTAL_INSURANCE, true, true)),
|
|
];
|
|
|
|
// License plate field provider - provides text input for vehicle license plate when parking is selected
|
|
$this->fieldOptionProviders['licensePlate'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => 'Kennzeichen',
|
|
'required' => false,
|
|
'attr' => [
|
|
'placeholder' => 'z.B. AB-CD 123',
|
|
'maxlength' => 20,
|
|
],
|
|
'help' => 'Bitte gib das Kennzeichen deines Fahrzeugs an. Du kannst es aber auch später nachreichen.',
|
|
'sanitize_html' => true,
|
|
];
|
|
|
|
// Skipass field provider - provides age-appropriate skipass options from travel data filtered by date range
|
|
$this->fieldOptionProviders['skiPass'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => 'Skipass',
|
|
'multiple' => false,
|
|
'expanded' => true,
|
|
'required' => true,
|
|
'choices' => $this->filterServicesByAgeConstraints(
|
|
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true, true),
|
|
$bookingDto,
|
|
$participantIndex
|
|
),
|
|
'choice_value' => 'id',
|
|
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
|
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
|
if (null === $service) {
|
|
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
|
|
if ($this->isServiceUnavailableForParticipant($service, $bookingDto, $participantIndex)) {
|
|
$attributes['readonly'] = true;
|
|
$attributes['data-tooltip'] = 'ausgebucht';
|
|
}
|
|
|
|
return $attributes;
|
|
},
|
|
];
|
|
|
|
// Room remarks field provider - provides textarea for room-specific remarks (only for 'mbz' rooms)
|
|
$this->fieldOptionProviders['remarksRoom'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => 'Wünsche oder Anmerkungen zum Zimmer',
|
|
'required' => false,
|
|
'sanitize_html' => true,
|
|
'attr' => [
|
|
'rows' => 2,
|
|
],
|
|
];
|
|
|
|
// Transportation field providers - handles outbound/inbound transportation and pickup selection
|
|
|
|
// Outbound Transportation
|
|
$this->fieldOptionProviders['transportationOutbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => 'Hinfahrt',
|
|
'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL),
|
|
'choice_label' => fn (Service $service) => $this->formatTransportationServiceLabel($service),
|
|
'choice_value' => 'id',
|
|
'expanded' => true,
|
|
'multiple' => false,
|
|
'required' => true,
|
|
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
|
if (null === $service) {
|
|
return [];
|
|
}
|
|
|
|
$attributes = [];
|
|
|
|
// Make readonly if service is unavailable
|
|
if ($this->isServiceUnavailableForParticipant($service, $bookingDto, $participantIndex)) {
|
|
$attributes['readonly'] = true;
|
|
$attributes['data-tooltip'] = 'ausgebucht';
|
|
}
|
|
|
|
return $attributes;
|
|
},
|
|
'attr' => [
|
|
'hx-post' => $this->urlGenerator->generate('app_booking_create_step_2_refresh'),
|
|
'hx-swap' => 'none',
|
|
'hx-trigger' => 'change',
|
|
],
|
|
];
|
|
|
|
// Inbound Transportation
|
|
$this->fieldOptionProviders['transportationInbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => 'Rückfahrt',
|
|
'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL),
|
|
'choice_label' => fn (Service $service) => $this->formatTransportationServiceLabel($service),
|
|
'choice_value' => 'id',
|
|
'expanded' => true,
|
|
'multiple' => false,
|
|
'required' => true,
|
|
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
|
if (null === $service) {
|
|
return [];
|
|
}
|
|
|
|
$attributes = [];
|
|
|
|
// Make readonly if service is unavailable
|
|
if ($this->isServiceUnavailableForParticipant($service, $bookingDto, $participantIndex)) {
|
|
$attributes['readonly'] = true;
|
|
$attributes['data-tooltip'] = 'ausgebucht';
|
|
}
|
|
|
|
return $attributes;
|
|
},
|
|
'attr' => [
|
|
'hx-post' => $this->urlGenerator->generate('app_booking_create_step_2_refresh'),
|
|
'hx-swap' => 'none',
|
|
'hx-trigger' => 'change',
|
|
],
|
|
];
|
|
|
|
// Outbound Pickup (conditional - only shown when outbound transportation is bus)
|
|
$this->fieldOptionProviders['pickupOutbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => 'Zustieg Hinfahrt',
|
|
'choices' => $bookingDto->travel->pickupsOutbound,
|
|
'choice_label' => fn (?Pickup $pickup) => $this->formatPickupLabelWithPrice($pickup),
|
|
'choice_value' => 'id',
|
|
'expanded' => false, // Dropdown for pickups
|
|
'multiple' => false,
|
|
'required' => true,
|
|
'placeholder' => 'Zustieg auswählen',
|
|
];
|
|
|
|
// Inbound Pickup (conditional - only shown when inbound transportation is bus)
|
|
$this->fieldOptionProviders['pickupInbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => 'Ausstieg Rückfahrt',
|
|
'choices' => $bookingDto->travel->pickupsInbound,
|
|
'choice_label' => fn (?Pickup $pickup) => $this->formatPickupLabelWithPrice($pickup),
|
|
'choice_value' => 'id',
|
|
'expanded' => false,
|
|
'multiple' => false,
|
|
'required' => true,
|
|
'placeholder' => 'Ausstieg auswählen',
|
|
];
|
|
|
|
// Parking (conditional - only shown when outbound transportation is PKW)
|
|
// Simple checkbox since there's only ever one parking type
|
|
$this->fieldOptionProviders['parking'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => $this->getParkingCheckboxLabel($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_PARKING, true)),
|
|
'required' => false,
|
|
];
|
|
|
|
// Bulk insurance booking checkbox (applicant only - controls insurance assignment for all participants)
|
|
$this->fieldOptionProviders['bulkInsuranceBooking'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => 'Für alle Teilnehmer buchen',
|
|
'required' => false,
|
|
'attr' => [
|
|
'hx-post' => $this->urlGenerator->generate('app_booking_create_step_2_refresh'),
|
|
'hx-swap' => 'none',
|
|
'hx-trigger' => 'change',
|
|
],
|
|
];
|
|
|
|
// Insurance field provider - provides age and eligibility filtered insurances for participants
|
|
$this->fieldOptionProviders['insurance'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
|
'label' => 'Reiseversicherung',
|
|
'multiple' => false,
|
|
'expanded' => true,
|
|
'required' => false,
|
|
'choices' => array_merge(
|
|
[0 => null], // "no insurance" option
|
|
$this->getEligibleInsurances($bookingDto, $participantIndex)
|
|
),
|
|
'choice_label' => fn (?Insurance $insurance) => $this->formatInsuranceLabel($insurance),
|
|
'choice_value' => 'id',
|
|
'attr' => [
|
|
'hx-post' => $this->urlGenerator->generate('app_booking_create_step_2_refresh'),
|
|
'hx-swap' => 'none',
|
|
'hx-trigger' => 'change',
|
|
],
|
|
];
|
|
|
|
// Future field providers would be added here, for example:
|
|
//
|
|
// $this->fieldOptionProviders['mealPreference'] = fn($bookingDto, $participantIndex) => [
|
|
// 'label' => 'Meal Preference',
|
|
// 'choices' => [
|
|
// 'Standard' => 'standard',
|
|
// 'Vegetarian' => 'vegetarian',
|
|
// 'Vegan' => 'vegan',
|
|
// ],
|
|
// ];
|
|
}
|
|
|
|
/**
|
|
* Filters rental services by selected skipass duration.
|
|
*
|
|
* Only returns rentals that have exactly the same dateFrom and dateTo
|
|
* as the participant's selected skipass. This ensures rental equipment
|
|
* is only available for the exact duration of the skipass.
|
|
*
|
|
* @param array $rentals Array of rental Service objects to filter
|
|
* @param BookingDtoInterface $bookingDto The booking DTO containing participant data
|
|
* @param int $participantIndex Index of the participant to evaluate
|
|
*
|
|
* @return array Filtered array of rentals matching skipass duration
|
|
*/
|
|
private function filterRentalsBySkiPassDuration(array $rentals, BookingDtoInterface $bookingDto, int $participantIndex): array
|
|
{
|
|
$participant = $bookingDto->getParticipant($participantIndex);
|
|
if (null === $participant || null === $participant->skiPass) {
|
|
return []; // No skipass selected = no rentals available
|
|
}
|
|
|
|
$selectedSkiPass = $participant->skiPass;
|
|
|
|
// If skipass has no valid dates, return empty rentals
|
|
if (null === $selectedSkiPass->dateFrom || null === $selectedSkiPass->dateTo) {
|
|
return [];
|
|
}
|
|
|
|
return array_filter($rentals, function (Service $rental) use ($selectedSkiPass) {
|
|
// Rental must have valid dates to be considered
|
|
if (null === $rental->dateFrom || null === $rental->dateTo) {
|
|
return false;
|
|
}
|
|
|
|
// Exact date matching: rental dates must match skipass dates exactly
|
|
return $rental->dateFrom->format('Y-m-d') === $selectedSkiPass->dateFrom->format('Y-m-d')
|
|
&& $rental->dateTo->format('Y-m-d') === $selectedSkiPass->dateTo->format('Y-m-d');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Formats service label with pricing information.
|
|
*
|
|
* @param Service|null $service The service to format
|
|
*
|
|
* @return string The formatted label
|
|
*/
|
|
private function formatServiceLabelWithPrice(?Service $service): string
|
|
{
|
|
if (null === $service) {
|
|
return '';
|
|
}
|
|
|
|
if (null === $service->price || 0.0 === $service->price) {
|
|
return $service->label;
|
|
}
|
|
|
|
if ($service->price < 0) {
|
|
// Negative prices are discounts
|
|
return sprintf('%s (-%s€ Rabatt)', $service->label, number_format(abs($service->price), 2, ',', '.'));
|
|
}
|
|
|
|
return sprintf('%s (€%s)', $service->label, number_format($service->price, 2, ',', '.'));
|
|
}
|
|
|
|
/**
|
|
* Format transportation service labels with type indicator and pricing.
|
|
*
|
|
* Creates user-friendly labels for transportation services that include:
|
|
* - Transportation type icon (🚌 for bus, 🚗 for car)
|
|
* - Service name
|
|
* - Pricing (with discount indication for negative prices)
|
|
* - Availability warning for limited services
|
|
*
|
|
* @param Service $service The transportation service to format
|
|
*
|
|
* @return string The formatted transportation service label
|
|
*/
|
|
private function formatTransportationServiceLabel(Service $service): string
|
|
{
|
|
$label = $service->label;
|
|
|
|
if (null === $service->price || 0.0 === $service->price) {
|
|
return $service->label;
|
|
}
|
|
|
|
if ($service->price > 0) {
|
|
$label .= sprintf(' (€%s)', number_format($service->price, 2, ',', '.'));
|
|
} else {
|
|
$label .= sprintf(' (-%s€ Rabatt)', number_format(abs($service->price), 2, ',', '.'));
|
|
}
|
|
|
|
// Add availability warning if limited
|
|
if (null !== $service->available && $service->available <= 5) {
|
|
$label .= sprintf(' (nur %d verfügbar)', $service->available);
|
|
}
|
|
|
|
return $label;
|
|
}
|
|
|
|
/**
|
|
* Format pickup labels with city and street information.
|
|
*
|
|
* Creates user-friendly labels for pickup locations following the existing pattern:
|
|
* - Primary format: "City (Street)" if street is available
|
|
* - Fallback format: "City" if no street information
|
|
*
|
|
* @param Pickup $pickup The pickup location to format
|
|
*
|
|
* @return string The formatted pickup location label
|
|
*/
|
|
private function formatPickupLabel(Pickup $pickup): string
|
|
{
|
|
$label = $pickup->city ?? '';
|
|
|
|
if (null !== $pickup->street && '' !== trim($pickup->street)) {
|
|
$label .= ' ('.$pickup->street.')';
|
|
}
|
|
|
|
return $label;
|
|
}
|
|
|
|
private function formatPickupLabelWithPrice(?Pickup $pickup): string
|
|
{
|
|
if (null === $pickup) {
|
|
return '';
|
|
}
|
|
|
|
$label = $this->formatPickupLabel($pickup);
|
|
|
|
if (null === $pickup->price || 0.0 === $pickup->price) {
|
|
return $label;
|
|
}
|
|
|
|
if ($pickup->price < 0) {
|
|
// Negative prices are discounts
|
|
return sprintf('%s (-%s€ Rabatt)', $label, number_format(abs($pickup->price), 2, ',', '.'));
|
|
}
|
|
|
|
return sprintf('%s (€%s)', $label, number_format($pickup->price, 2, ',', '.'));
|
|
}
|
|
|
|
/**
|
|
* Gets the parking checkbox label with pricing information.
|
|
*
|
|
* Creates a checkbox label for the single parking service including pricing.
|
|
* Since there's only ever one parking type, we take the first available service.
|
|
*
|
|
* @param array $parkingServices Array of available parking services
|
|
*
|
|
* @return string The formatted checkbox label with pricing
|
|
*/
|
|
private function getParkingCheckboxLabel(array $parkingServices): string
|
|
{
|
|
if (empty($parkingServices)) {
|
|
return Constants::SERVICE_LABELS[Constants::TOKEN_PARKING];
|
|
}
|
|
|
|
$parkingService = reset($parkingServices); // Get the first (and only) parking service
|
|
|
|
return $this->formatServiceLabelWithPrice($parkingService);
|
|
}
|
|
|
|
/**
|
|
* Filters services based on remaining availability in the current booking session.
|
|
*
|
|
* Removes services that have been fully booked by other participants in
|
|
* the current booking session. This prevents overbooking within a single
|
|
* booking workflow while maintaining accurate availability counts.
|
|
*
|
|
* @param array $services Array of Service objects to filter
|
|
* @param BookingDtoInterface $bookingDto The booking DTO containing participant data
|
|
* @param int $participantIndex Index of the participant currently selecting services
|
|
*
|
|
* @return array Filtered array containing only services with remaining availability
|
|
*/
|
|
private function filterServicesByAvailability(array $services, BookingDtoInterface $bookingDto, int $participantIndex): array
|
|
{
|
|
if (!$bookingDto instanceof BookingCreateDto) {
|
|
// For non-create workflows, return all services (no availability tracking needed)
|
|
return $services;
|
|
}
|
|
|
|
return $this->serviceAvailabilityCalculator->filterAvailableServices($services, $bookingDto, $participantIndex);
|
|
}
|
|
|
|
/**
|
|
* Checks if a service should be rendered as read-only due to unavailability.
|
|
*
|
|
* @param Service $service The service to check
|
|
* @param BookingDtoInterface $bookingDto The booking DTO containing participant data
|
|
* @param int $participantIndex Index of the participant currently selecting services
|
|
*
|
|
* @return bool True if the service should be read-only due to unavailability
|
|
*/
|
|
private function isServiceUnavailableForParticipant(Service $service, BookingDtoInterface $bookingDto, int $participantIndex): bool
|
|
{
|
|
if (!$bookingDto instanceof BookingCreateDto) {
|
|
// For non-create workflows, don't apply availability restrictions
|
|
return false;
|
|
}
|
|
|
|
return $this->serviceAvailabilityCalculator->isServiceUnavailable($service->id, $bookingDto, $participantIndex);
|
|
}
|
|
|
|
/**
|
|
* Filters services based on participant's age constraints.
|
|
*
|
|
* Removes services that have age restrictions the participant doesn't meet.
|
|
* If no age evaluator is configured or participant has no birth date,
|
|
* returns empty array to be handled by field visibility conditions.
|
|
*
|
|
* @param array $services Array of Service objects to filter
|
|
* @param BookingDtoInterface $bookingDto The booking DTO containing participant data
|
|
* @param int $participantIndex Index of the participant to evaluate
|
|
*
|
|
* @return array Filtered array of available services
|
|
*/
|
|
private function filterServicesByAgeConstraints(array $services, BookingDtoInterface $bookingDto, int $participantIndex): array
|
|
{
|
|
$ageEvaluator = new ServiceAgeEvaluator();
|
|
|
|
$participant = $bookingDto->getParticipant($participantIndex);
|
|
|
|
// If no birthdate provided, return empty array (handled by field visibility conditions)
|
|
if (null === $participant || null === $participant->dateOfBirth) {
|
|
return [];
|
|
}
|
|
|
|
return array_filter($services, function (Service $service) use ($bookingDto, $participantIndex, $ageEvaluator) {
|
|
// No age constraints = available to all
|
|
if (false === $ageEvaluator->canEvaluate($service)) {
|
|
return true;
|
|
}
|
|
|
|
return $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generates label for rental insurance checkbox including pricing information.
|
|
*/
|
|
private function getRentalInsuranceCheckboxLabel(array $rentalInsuranceServices): string
|
|
{
|
|
if (empty($rentalInsuranceServices)) {
|
|
return Constants::SERVICE_LABELS[Constants::TOKEN_RENTAL_INSURANCE];
|
|
}
|
|
$rentalInsuranceService = reset($rentalInsuranceServices); // Get the first (and only) rental insurance service
|
|
|
|
return $this->formatServiceLabelWithPrice($rentalInsuranceService);
|
|
}
|
|
|
|
/**
|
|
* Gets the rental insurance description for help text.
|
|
*/
|
|
private function getRentalInsuranceDescription(array $rentalInsuranceServices): ?string
|
|
{
|
|
if (empty($rentalInsuranceServices)) {
|
|
return null;
|
|
}
|
|
$rentalInsuranceService = reset($rentalInsuranceServices); // Get the first (and only) rental insurance service
|
|
|
|
return $rentalInsuranceService->description;
|
|
}
|
|
|
|
/**
|
|
* Gets eligible insurances for a participant based on eligibility criteria.
|
|
*
|
|
* @param BookingDtoInterface $bookingDto The booking DTO containing travel and participant data
|
|
* @param int $participantIndex The index of the participant to get eligible insurances for
|
|
*
|
|
* @return array Array of eligible insurance objects filtered by age, family status, and other constraints
|
|
*/
|
|
private function getEligibleInsurances(BookingDtoInterface $bookingDto, int $participantIndex): array
|
|
{
|
|
$participant = $bookingDto->getParticipant($participantIndex);
|
|
if (null === $participant) {
|
|
return [];
|
|
}
|
|
|
|
$availableInsurances = $bookingDto->travel->insurances ?? [];
|
|
|
|
// Only apply insurance filtering for BookingCreateDto (creation workflow)
|
|
if (!$bookingDto instanceof BookingCreateDto) {
|
|
return $availableInsurances;
|
|
}
|
|
|
|
// Use insurance matching service to filter based on eligibility criteria
|
|
return $this->insuranceMatchingService->getEligibleInsurances(
|
|
$availableInsurances,
|
|
$participant,
|
|
$bookingDto
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Formats insurance label with pricing and type information.
|
|
*
|
|
* @param Insurance|null $insurance The insurance to format, or null for "No Insurance" option
|
|
*
|
|
* @return string The formatted insurance label
|
|
*/
|
|
private function formatInsuranceLabel(?Insurance $insurance): string
|
|
{
|
|
if (null === $insurance) {
|
|
return 'Keine Versicherung';
|
|
}
|
|
|
|
$label = $insurance->label;
|
|
|
|
// Add pricing information (consistent with other services)
|
|
if (null !== $insurance->price && $insurance->price > 0) {
|
|
$label .= sprintf(' (€%s)', number_format($insurance->price, 2, ',', '.'));
|
|
}
|
|
|
|
// Add type information if available using centralized mapping
|
|
if (null !== $insurance->subType && isset(Constants::INSURANCE_LABELS[$insurance->subType])) {
|
|
$label .= sprintf(' (%s)', Constants::INSURANCE_LABELS[$insurance->subType]);
|
|
}
|
|
|
|
return $label;
|
|
}
|
|
}
|