feat: selectable drop-offs for inbound bus travel
This commit is contained in:
@@ -341,6 +341,8 @@ class BookingParticipantType extends AbstractType
|
||||
'transportationOutbound' => ChoiceType::class,
|
||||
'transportationInbound' => ChoiceType::class,
|
||||
'pickup' => ChoiceType::class,
|
||||
'differentDropOff' => CheckboxType::class,
|
||||
'dropOff' => ChoiceType::class,
|
||||
'parking' => CheckboxType::class,
|
||||
'licensePlate' => TextType::class,
|
||||
];
|
||||
|
||||
@@ -32,6 +32,8 @@ class ParticipantDto
|
||||
'transportationOutbound',
|
||||
'transportationInbound',
|
||||
'pickup',
|
||||
'differentDropOff',
|
||||
'dropOff',
|
||||
'parking',
|
||||
'licensePlate',
|
||||
'bulkInsuranceBooking',
|
||||
@@ -106,6 +108,12 @@ class ParticipantDto
|
||||
// Pickup location (applies to both directions)
|
||||
public ?Pickup $pickup = null;
|
||||
|
||||
// Drop-off location (for inbound/return direction)
|
||||
public ?Pickup $dropOff = null;
|
||||
|
||||
// UI-only flag: true when user selected a different drop-off location (BUS+BUS scenario)
|
||||
public bool $differentDropOff = false;
|
||||
|
||||
// Parking service for self-organized transportation (boolean: true if parking requested)
|
||||
public bool $parking = false;
|
||||
|
||||
@@ -242,6 +250,19 @@ class ParticipantDto
|
||||
return $this->dateOfBirth->diff($referenceDate)->y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any transportation-related data is present for summary display.
|
||||
*/
|
||||
public function hasTransportationData(): bool
|
||||
{
|
||||
return null !== $this->transportationOutbound
|
||||
|| null !== $this->transportationInbound
|
||||
|| null !== $this->pickup
|
||||
|| null !== $this->dropOff
|
||||
|| true === $this->parking
|
||||
|| null !== $this->licensePlate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the participant has selected an insurance.
|
||||
*/
|
||||
|
||||
@@ -260,6 +260,23 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
|
||||
),
|
||||
];
|
||||
|
||||
// Show differentDropOff checkbox only when BOTH outbound and inbound are BUS
|
||||
$this->fieldStateConditions['differentDropOff'] = [
|
||||
'hidden' => CompositeCondition::not(
|
||||
CompositeCondition::and(
|
||||
ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_BUS_API),
|
||||
ServiceSubTypeCondition::equals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API)
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
// Show drop-off when inbound is BUS (options provider further gates on checkbox state in BUS+BUS)
|
||||
$this->fieldStateConditions['dropOff'] = [
|
||||
'hidden' => CompositeCondition::not(
|
||||
ServiceSubTypeCondition::equals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API)
|
||||
),
|
||||
];
|
||||
|
||||
// Show parking only when outbound transportation is PKW AND participant is 18+ (hidden by default)
|
||||
// Parking is offered at holiday destination for those arriving by car
|
||||
// Participants under 18 cannot drive in Germany, so parking is not relevant for them
|
||||
|
||||
@@ -197,6 +197,25 @@ class EditFieldStateProvider extends AbstractFieldStateProvider
|
||||
'readonly' => $pickupsMutabilityCondition,
|
||||
];
|
||||
|
||||
// Show differentDropOff checkbox only when BOTH outbound and inbound are BUS, readonly if pickups not mutable
|
||||
$this->fieldStateConditions['differentDropOff'] = [
|
||||
'hidden' => CompositeCondition::not(
|
||||
CompositeCondition::and(
|
||||
ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_BUS_API),
|
||||
ServiceSubTypeCondition::equals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API)
|
||||
)
|
||||
),
|
||||
'readonly' => $pickupsMutabilityCondition,
|
||||
];
|
||||
|
||||
// Show drop-off when inbound is BUS (options provider further gates on checkbox state in BUS+BUS)
|
||||
$this->fieldStateConditions['dropOff'] = [
|
||||
'hidden' => CompositeCondition::not(
|
||||
ServiceSubTypeCondition::equals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API)
|
||||
),
|
||||
'readonly' => $pickupsMutabilityCondition,
|
||||
];
|
||||
|
||||
// Parking - shown only when outbound transportation is PKW AND participant is 18+, readonly if transportation not mutable
|
||||
// Participants under 18 cannot drive in Germany, so parking is not relevant for them
|
||||
$minimumDrivingAgeCondition = new AgeRangeCondition(18);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Utility\DirectionMapper;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
* Handles drop-off location selection for the inbound bus journey.
|
||||
*
|
||||
* Drop-off behavior depends on the transportation combination:
|
||||
* - BUS+BUS: drop-off is gated behind a "differentDropOff" checkbox.
|
||||
* When unchecked, BusProNet defaults to the same location as the pickup.
|
||||
* - PKW+BUS: drop-off is shown directly (no pickup field, no checkbox needed).
|
||||
* - BUS+PKW / PKW+PKW: no drop-off applicable, fields are cleared.
|
||||
*/
|
||||
class ParticipantDropOffFieldHandler extends AbstractParticipantFieldHandler
|
||||
{
|
||||
public function getFieldName(): string
|
||||
{
|
||||
return 'dropOff';
|
||||
}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
return ['transportationOutbound', 'transportationInbound', 'pickup'];
|
||||
}
|
||||
|
||||
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||
{
|
||||
return true; // Always process to handle clearing drop-off
|
||||
}
|
||||
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
if (null === $participant) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hasOutboundBus = null !== $participant->transportationOutbound
|
||||
&& DirectionMapper::SUBTYPE_BUS_API === $participant->transportationOutbound->subType;
|
||||
$hasInboundBus = null !== $participant->transportationInbound
|
||||
&& DirectionMapper::SUBTYPE_BUS_API === $participant->transportationInbound->subType;
|
||||
|
||||
// Drop-off only applies when inbound is BUS
|
||||
if (false === $hasInboundBus) {
|
||||
$participant->dropOff = null;
|
||||
$participant->differentDropOff = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($hasOutboundBus) {
|
||||
// BUS+BUS: gated by differentDropOff checkbox
|
||||
$checkboxValue = $this->getFieldValue($submittedData, 'differentDropOff');
|
||||
$isChecked = true === $checkboxValue || '1' === $checkboxValue || 1 === $checkboxValue;
|
||||
|
||||
if (false === $isChecked) {
|
||||
$participant->dropOff = null;
|
||||
$participant->differentDropOff = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$participant->differentDropOff = true;
|
||||
}
|
||||
|
||||
// PKW+BUS or BUS+BUS with checkbox checked: validate drop-off selection
|
||||
$selectedDropOff = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||
$participant->dropOff = $this->findItemById($selectedDropOff, $bookingDto->travel->dropOffs);
|
||||
|
||||
// For PKW+BUS, differentDropOff is not relevant (no checkbox), keep it false
|
||||
if (false === $hasOutboundBus) {
|
||||
$participant->differentDropOff = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,6 +225,7 @@ class ParticipantFieldHandlerRegistry
|
||||
'additionalServices',
|
||||
'board',
|
||||
'pickup',
|
||||
'dropOff',
|
||||
];
|
||||
|
||||
foreach ($serviceFields as $fieldName) {
|
||||
|
||||
@@ -547,14 +547,14 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
// Uses outbound pickups list, applies to both directions
|
||||
// Returns empty array when no pickup options available
|
||||
$this->fieldOptionProviders['pickup'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array {
|
||||
$choices = $bookingDto->travel->pickupsOutbound;
|
||||
$choices = $bookingDto->travel->pickups;
|
||||
|
||||
if (true === empty($choices)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'label' => 'Zu- und Ausstieg',
|
||||
'label' => 'Zustieg',
|
||||
'choices' => $choices,
|
||||
'choice_label' => fn (?Pickup $pickup) => $pickup?->getLabel(),
|
||||
'choice_value' => 'id',
|
||||
@@ -564,6 +564,43 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
];
|
||||
};
|
||||
|
||||
// "Different drop-off" checkbox (BUS+BUS only)
|
||||
$this->fieldOptionProviders['differentDropOff'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'abweichender Ausstieg',
|
||||
'required' => false,
|
||||
];
|
||||
|
||||
// Drop-off location (inbound direction)
|
||||
// In BUS+BUS mode, gated by the differentDropOff checkbox state
|
||||
// In PKW+BUS mode, shown directly (no checkbox involved)
|
||||
$this->fieldOptionProviders['dropOff'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []): array {
|
||||
$choices = $bookingDto->travel->dropOffs;
|
||||
|
||||
if (true === empty($choices)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
|
||||
// In BUS+BUS mode: only show drop-off choices when checkbox is checked
|
||||
$hasOutboundBus = null !== $participant?->transportationOutbound
|
||||
&& DirectionMapper::SUBTYPE_BUS_API === $participant->transportationOutbound->subType;
|
||||
|
||||
if ($hasOutboundBus && false === ($participant->differentDropOff ?? false)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'label' => 'Ausstieg',
|
||||
'choices' => $choices,
|
||||
'choice_label' => fn (?Pickup $pickup) => $pickup?->getLabel(),
|
||||
'choice_value' => 'id',
|
||||
'expanded' => true,
|
||||
'multiple' => false,
|
||||
'required' => true,
|
||||
];
|
||||
};
|
||||
|
||||
// Parking (conditional - only shown when outbound transportation is PKW)
|
||||
// Simple checkbox since there's only ever one parking type
|
||||
// Returns empty array when no parking services exist to prevent field from rendering
|
||||
|
||||
@@ -57,6 +57,6 @@ class ParticipantPickupFieldHandler extends AbstractParticipantFieldHandler
|
||||
$selectedPickup = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||
|
||||
// Validate pickup selection against available outbound pickups
|
||||
$participant->pickup = $this->findItemById($selectedPickup, $bookingDto->travel->pickupsOutbound);
|
||||
$participant->pickup = $this->findItemById($selectedPickup, $bookingDto->travel->pickups);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user