feat: mark unavailable services as read-only and add tooltip
This commit is contained in:
@@ -97,18 +97,14 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
'multiple' => true,
|
'multiple' => true,
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'choices' => $this->filterServicesByAvailability(
|
'choices' => $this->filterServicesByAgeConstraints(
|
||||||
$this->filterServicesByAgeConstraints(
|
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES),
|
||||||
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES),
|
|
||||||
$bookingDto,
|
|
||||||
$participantIndex
|
|
||||||
),
|
|
||||||
$bookingDto,
|
$bookingDto,
|
||||||
$participantIndex
|
$participantIndex
|
||||||
),
|
),
|
||||||
'choice_value' => 'id',
|
'choice_value' => 'id',
|
||||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
||||||
'choice_attr' => function (?Service $service) {
|
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
||||||
if (null === $service) {
|
if (null === $service) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -120,6 +116,12 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
$attributes['data-description'] = $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;
|
return $attributes;
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -130,18 +132,14 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
'multiple' => true,
|
'multiple' => true,
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'choices' => $this->filterServicesByAvailability(
|
'choices' => $this->filterServicesByAgeConstraints(
|
||||||
$this->filterServicesByAgeConstraints(
|
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL),
|
||||||
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL),
|
|
||||||
$bookingDto,
|
|
||||||
$participantIndex
|
|
||||||
),
|
|
||||||
$bookingDto,
|
$bookingDto,
|
||||||
$participantIndex
|
$participantIndex
|
||||||
),
|
),
|
||||||
'choice_value' => 'id',
|
'choice_value' => 'id',
|
||||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
||||||
'choice_attr' => function (?Service $service) {
|
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
||||||
if (null === $service) {
|
if (null === $service) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -160,6 +158,12 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
$attributes['data-description'] = $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;
|
return $attributes;
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -170,17 +174,28 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
'multiple' => true,
|
'multiple' => true,
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'choices' => $this->filterServicesByAvailability(
|
'choices' => $this->filterServicesByAgeConstraints(
|
||||||
$this->filterServicesByAgeConstraints(
|
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD),
|
||||||
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD),
|
|
||||||
$bookingDto,
|
|
||||||
$participantIndex
|
|
||||||
),
|
|
||||||
$bookingDto,
|
$bookingDto,
|
||||||
$participantIndex
|
$participantIndex
|
||||||
),
|
),
|
||||||
'choice_value' => 'id',
|
'choice_value' => 'id',
|
||||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
'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
|
// Rentals field provider - provides age-appropriate rental options filtered by selected skipass duration
|
||||||
@@ -189,13 +204,9 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
'multiple' => true,
|
'multiple' => true,
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'choices' => $this->filterServicesByAvailability(
|
'choices' => $this->filterServicesByAgeConstraints(
|
||||||
$this->filterServicesByAgeConstraints(
|
$this->filterRentalsBySkiPassDuration(
|
||||||
$this->filterRentalsBySkiPassDuration(
|
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true, true),
|
||||||
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true, true),
|
|
||||||
$bookingDto,
|
|
||||||
$participantIndex
|
|
||||||
),
|
|
||||||
$bookingDto,
|
$bookingDto,
|
||||||
$participantIndex
|
$participantIndex
|
||||||
),
|
),
|
||||||
@@ -204,7 +215,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
),
|
),
|
||||||
'choice_value' => 'id',
|
'choice_value' => 'id',
|
||||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
||||||
'choice_attr' => function (?Service $service) {
|
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
||||||
if (null === $service) {
|
if (null === $service) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -216,6 +227,12 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
$attributes['data-description'] = $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;
|
return $attributes;
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -246,18 +263,14 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
'multiple' => false,
|
'multiple' => false,
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'choices' => $this->filterServicesByAvailability(
|
'choices' => $this->filterServicesByAgeConstraints(
|
||||||
$this->filterServicesByAgeConstraints(
|
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true, true),
|
||||||
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true, true),
|
|
||||||
$bookingDto,
|
|
||||||
$participantIndex
|
|
||||||
),
|
|
||||||
$bookingDto,
|
$bookingDto,
|
||||||
$participantIndex
|
$participantIndex
|
||||||
),
|
),
|
||||||
'choice_value' => 'id',
|
'choice_value' => 'id',
|
||||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
||||||
'choice_attr' => function (?Service $service) {
|
'choice_attr' => function (?Service $service) use ($bookingDto, $participantIndex) {
|
||||||
if (null === $service) {
|
if (null === $service) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -269,6 +282,12 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
$attributes['data-description'] = $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;
|
return $attributes;
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -288,16 +307,27 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
// Outbound Transportation
|
// Outbound Transportation
|
||||||
$this->fieldOptionProviders['transportationOutbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
$this->fieldOptionProviders['transportationOutbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||||
'label' => 'Hinfahrt',
|
'label' => 'Hinfahrt',
|
||||||
'choices' => $this->filterServicesByAvailability(
|
'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL),
|
||||||
$bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL),
|
|
||||||
$bookingDto,
|
|
||||||
$participantIndex
|
|
||||||
),
|
|
||||||
'choice_label' => fn (Service $service) => $this->formatTransportationServiceLabel($service),
|
'choice_label' => fn (Service $service) => $this->formatTransportationServiceLabel($service),
|
||||||
'choice_value' => 'id',
|
'choice_value' => 'id',
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'multiple' => false,
|
'multiple' => false,
|
||||||
'required' => true,
|
'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' => [
|
'attr' => [
|
||||||
'hx-post' => '#', // Will be configured when HTMX integration is implemented
|
'hx-post' => '#', // Will be configured when HTMX integration is implemented
|
||||||
'hx-target' => '#booking-summary',
|
'hx-target' => '#booking-summary',
|
||||||
@@ -308,16 +338,27 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
// Inbound Transportation
|
// Inbound Transportation
|
||||||
$this->fieldOptionProviders['transportationInbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
$this->fieldOptionProviders['transportationInbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||||
'label' => 'Rückfahrt',
|
'label' => 'Rückfahrt',
|
||||||
'choices' => $this->filterServicesByAvailability(
|
'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL),
|
||||||
$bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL),
|
|
||||||
$bookingDto,
|
|
||||||
$participantIndex
|
|
||||||
),
|
|
||||||
'choice_label' => fn (Service $service) => $this->formatTransportationServiceLabel($service),
|
'choice_label' => fn (Service $service) => $this->formatTransportationServiceLabel($service),
|
||||||
'choice_value' => 'id',
|
'choice_value' => 'id',
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'multiple' => false,
|
'multiple' => false,
|
||||||
'required' => true,
|
'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' => [
|
'attr' => [
|
||||||
'hx-post' => '#', // Will be configured when HTMX integration is implemented
|
'hx-post' => '#', // Will be configured when HTMX integration is implemented
|
||||||
'hx-target' => '#booking-summary',
|
'hx-target' => '#booking-summary',
|
||||||
@@ -553,6 +594,25 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
return $this->serviceAvailabilityCalculator->filterAvailableServices($services, $bookingDto, $participantIndex);
|
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.
|
* Filters services based on participant's age constraints.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -74,19 +74,34 @@ class ServiceAvailabilityCalculator
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a specific service is available for the current participant.
|
* Check if a specific service is unavailable (sold out) for the current participant.
|
||||||
*
|
*
|
||||||
* @param int $serviceId The ID of the service to check
|
* @param int $serviceId The ID of the service to check
|
||||||
* @param BookingCreateDto $bookingDto The booking data with participant selections
|
* @param BookingCreateDto $bookingDto The booking data with participant selections
|
||||||
* @param int $participantIndex The index of the participant currently filling the form
|
* @param int $participantIndex The index of the participant currently filling the form
|
||||||
*
|
*
|
||||||
* @return bool True if the service has remaining availability
|
* @return bool True if the service is unavailable (has availability limit and remaining is 0)
|
||||||
*/
|
*/
|
||||||
public function isServiceAvailable(int $serviceId, BookingCreateDto $bookingDto, int $participantIndex): bool
|
public function isServiceUnavailable(int $serviceId, BookingCreateDto $bookingDto, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
|
$allServices = $this->getAllServicesFromTravel($bookingDto);
|
||||||
|
$service = null;
|
||||||
|
|
||||||
|
foreach ($allServices as $serviceObj) {
|
||||||
|
if ($serviceObj->id === $serviceId) {
|
||||||
|
$service = $serviceObj;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If service not found or has no availability limit, it's not unavailable
|
||||||
|
if (null === $service || null === $service->available || $service->available <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$remainingAvailability = $this->calculateRemainingAvailability($bookingDto, $participantIndex);
|
$remainingAvailability = $this->calculateRemainingAvailability($bookingDto, $participantIndex);
|
||||||
|
|
||||||
return ($remainingAvailability[$serviceId] ?? 0) > 0;
|
return ($remainingAvailability[$serviceId] ?? $service->available) <= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user