feat: unify pickup selection
This commit is contained in:
@@ -86,8 +86,7 @@ services:
|
|||||||
- 'App\Form\Service\ParticipantSkiPassFieldHandler'
|
- 'App\Form\Service\ParticipantSkiPassFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantTransportationOutboundFieldHandler'
|
- 'App\Form\Service\ParticipantTransportationOutboundFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantTransportationInboundFieldHandler'
|
- 'App\Form\Service\ParticipantTransportationInboundFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantPickupOutboundFieldHandler'
|
- 'App\Form\Service\ParticipantPickupFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantPickupInboundFieldHandler'
|
|
||||||
- 'App\Form\Service\ParticipantParkingFieldHandler'
|
- 'App\Form\Service\ParticipantParkingFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantRentalInsuranceFieldHandler'
|
- 'App\Form\Service\ParticipantRentalInsuranceFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantLicensePlateFieldHandler'
|
- 'App\Form\Service\ParticipantLicensePlateFieldHandler'
|
||||||
|
|||||||
@@ -178,7 +178,11 @@ class BookingDataProcessor
|
|||||||
*/
|
*/
|
||||||
private function processPickupLocations(object $participant, object $bookingData): void
|
private function processPickupLocations(object $participant, object $bookingData): void
|
||||||
{
|
{
|
||||||
if ('BUS' === $participant->transportationOutbound->subType && null !== $selectedPickup = $participant->pickupOutbound) {
|
// Check if either transportation direction is BUS and pickup is selected
|
||||||
|
$hasOutboundBus = null !== $participant->transportationOutbound && 'BUS' === $participant->transportationOutbound->subType;
|
||||||
|
$hasInboundBus = null !== $participant->transportationInbound && 'BUS' === $participant->transportationInbound->subType;
|
||||||
|
|
||||||
|
if (($hasOutboundBus || $hasInboundBus) && null !== $selectedPickup = $participant->pickup) {
|
||||||
if (false === isset($bookingData->pickupsOutbound[$selectedPickup->id])) {
|
if (false === isset($bookingData->pickupsOutbound[$selectedPickup->id])) {
|
||||||
$bookingData->pickupsOutbound[$selectedPickup->id] = $selectedPickup;
|
$bookingData->pickupsOutbound[$selectedPickup->id] = $selectedPickup;
|
||||||
}
|
}
|
||||||
@@ -729,8 +733,8 @@ class BookingDataProcessor
|
|||||||
/**
|
/**
|
||||||
* Collects pickup location mappings.
|
* Collects pickup location mappings.
|
||||||
*
|
*
|
||||||
* Only collects outbound pickups as the API doesn't support different pickups
|
* Collects the unified pickup selection which applies to both directions.
|
||||||
* for inbound direction. Both directions use the same pickup location.
|
* The API receives this as outbound pickup data.
|
||||||
*
|
*
|
||||||
* @return array<string, array<int>> Map of pickup ID to participant IDs
|
* @return array<string, array<int>> Map of pickup ID to participant IDs
|
||||||
*/
|
*/
|
||||||
@@ -741,9 +745,8 @@ class BookingDataProcessor
|
|||||||
foreach ($bookingDto->participants as $index => $participant) {
|
foreach ($bookingDto->participants as $index => $participant) {
|
||||||
$participantId = $index + 1;
|
$participantId = $index + 1;
|
||||||
|
|
||||||
// Only use outbound pickups (inbound uses same location)
|
if (null !== $participant->pickup) {
|
||||||
if (null !== $participant->pickupOutbound) {
|
$pickupMap[$participant->pickup->id][] = $participantId;
|
||||||
$pickupMap[$participant->pickupOutbound->id][] = $participantId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -231,9 +231,9 @@ class BookingEditParticipantType extends AbstractType
|
|||||||
'choice_attr' => $transportationChoiceAttributes,
|
'choice_attr' => $transportationChoiceAttributes,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Pickup
|
// Pickup (unified for both directions)
|
||||||
$form->add('pickupOutbound', ChoiceType::class, [
|
$form->add('pickup', ChoiceType::class, [
|
||||||
'label' => 'Zustieg',
|
'label' => 'Zu- und Ausstieg',
|
||||||
'multiple' => false,
|
'multiple' => false,
|
||||||
'expanded' => false,
|
'expanded' => false,
|
||||||
'choices' => $options['selectable_pickups'],
|
'choices' => $options['selectable_pickups'],
|
||||||
@@ -323,8 +323,8 @@ class BookingEditParticipantType extends AbstractType
|
|||||||
$transportation = $options['travel']->pickups[$transportationId] ?? null;
|
$transportation = $options['travel']->pickups[$transportationId] ?? null;
|
||||||
|
|
||||||
if (null !== $transportation && 'PKW' === $transportation->subType) {
|
if (null !== $transportation && 'PKW' === $transportation->subType) {
|
||||||
$form->remove('pickupOutbound');
|
$form->remove('pickup');
|
||||||
unset($data['pickupOutbound']);
|
unset($data['pickup']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// forcibly select mandatory services that potentially have been disabled in PRE_SET_DATA
|
// forcibly select mandatory services that potentially have been disabled in PRE_SET_DATA
|
||||||
|
|||||||
@@ -233,8 +233,7 @@ class BookingParticipantType extends AbstractType
|
|||||||
'skiPass' => ChoiceType::class,
|
'skiPass' => ChoiceType::class,
|
||||||
'transportationOutbound' => ChoiceType::class,
|
'transportationOutbound' => ChoiceType::class,
|
||||||
'transportationInbound' => ChoiceType::class,
|
'transportationInbound' => ChoiceType::class,
|
||||||
'pickupOutbound' => ChoiceType::class,
|
'pickup' => ChoiceType::class,
|
||||||
'pickupInbound' => ChoiceType::class,
|
|
||||||
'parking' => CheckboxType::class,
|
'parking' => CheckboxType::class,
|
||||||
'licensePlate' => TextType::class,
|
'licensePlate' => TextType::class,
|
||||||
'bulkInsuranceBooking' => CheckboxType::class,
|
'bulkInsuranceBooking' => CheckboxType::class,
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ class BookingEditDto implements BookingDtoInterface
|
|||||||
$participantData->transportationInbound = $inboundTransportation;
|
$participantData->transportationInbound = $inboundTransportation;
|
||||||
|
|
||||||
// Pickup handling (currently only supports outbound pickup)
|
// Pickup handling (currently only supports outbound pickup)
|
||||||
$pickupOutbound = $booking->getPickupForParticipant($index);
|
$pickup = $booking->getPickupForParticipant($index);
|
||||||
$participantData->pickupOutbound = $pickupOutbound;
|
$participantData->pickup = $pickup;
|
||||||
|
|
||||||
$instance->participants[$index] = $participantData;
|
$instance->participants[$index] = $participantData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ class ParticipantDto
|
|||||||
'skiPass',
|
'skiPass',
|
||||||
'transportationOutbound',
|
'transportationOutbound',
|
||||||
'transportationInbound',
|
'transportationInbound',
|
||||||
'pickupOutbound',
|
'pickup',
|
||||||
'pickupInbound',
|
|
||||||
'parking',
|
'parking',
|
||||||
'licensePlate',
|
'licensePlate',
|
||||||
'bulkInsuranceBooking',
|
'bulkInsuranceBooking',
|
||||||
@@ -86,9 +85,8 @@ class ParticipantDto
|
|||||||
public ?Service $transportationOutbound = null;
|
public ?Service $transportationOutbound = null;
|
||||||
public ?Service $transportationInbound = null;
|
public ?Service $transportationInbound = null;
|
||||||
|
|
||||||
// Pickup locations for each direction
|
// Pickup location (applies to both directions)
|
||||||
public ?Pickup $pickupOutbound = null;
|
public ?Pickup $pickup = null;
|
||||||
public ?Pickup $pickupInbound = null;
|
|
||||||
|
|
||||||
// Parking service for self-organized transportation (boolean: true if parking requested)
|
// Parking service for self-organized transportation (boolean: true if parking requested)
|
||||||
public bool $parking = false;
|
public bool $parking = false;
|
||||||
|
|||||||
@@ -179,17 +179,13 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Show outbound pickup only when transportation is BUS (hidden by default)
|
// Show pickup only when either outbound or inbound transportation is BUS (hidden by default)
|
||||||
$this->fieldStateConditions['pickupOutbound'] = [
|
$this->fieldStateConditions['pickup'] = [
|
||||||
'hidden' => CompositeCondition::not(
|
'hidden' => CompositeCondition::not(
|
||||||
ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_BUS_API)
|
CompositeCondition::or(
|
||||||
),
|
ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_BUS_API),
|
||||||
];
|
ServiceSubTypeCondition::equals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API)
|
||||||
|
)
|
||||||
// Show inbound pickup only when transportation is BUS (hidden by default)
|
|
||||||
$this->fieldStateConditions['pickupInbound'] = [
|
|
||||||
'hidden' => CompositeCondition::not(
|
|
||||||
ServiceSubTypeCondition::equals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API)
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -121,17 +121,13 @@ class EditFieldStateProvider extends AbstractFieldStateProvider
|
|||||||
'readonly' => $transportationServicesMutabilityCondition,
|
'readonly' => $transportationServicesMutabilityCondition,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Pickup fields - shown only when transportation is BUS, readonly if pickups not mutable
|
// Pickup field (unified) - shown only when either transportation direction is BUS, readonly if pickups not mutable
|
||||||
$this->fieldStateConditions['pickupOutbound'] = [
|
$this->fieldStateConditions['pickup'] = [
|
||||||
'hidden' => CompositeCondition::not(
|
'hidden' => CompositeCondition::not(
|
||||||
ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_BUS_API)
|
CompositeCondition::or(
|
||||||
),
|
ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_BUS_API),
|
||||||
'readonly' => $pickupsMutabilityCondition,
|
ServiceSubTypeCondition::equals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API)
|
||||||
];
|
)
|
||||||
|
|
||||||
$this->fieldStateConditions['pickupInbound'] = [
|
|
||||||
'hidden' => CompositeCondition::not(
|
|
||||||
ServiceSubTypeCondition::equals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API)
|
|
||||||
),
|
),
|
||||||
'readonly' => $pickupsMutabilityCondition,
|
'readonly' => $pickupsMutabilityCondition,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace App\Form\Service;
|
namespace App\Form\Service;
|
||||||
|
|
||||||
use App\BusProNet\Constants;
|
use App\BusProNet\Constants;
|
||||||
use App\BusProNet\Model\Insurance;
|
|
||||||
use App\BusProNet\Model\Pickup;
|
use App\BusProNet\Model\Pickup;
|
||||||
use App\BusProNet\Model\Service;
|
use App\BusProNet\Model\Service;
|
||||||
use App\BusProNet\Utility\DirectionMapper;
|
use App\BusProNet\Utility\DirectionMapper;
|
||||||
@@ -377,28 +376,17 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Outbound Pickup (conditional - only shown when outbound transportation is bus)
|
// Pickup (conditional - only shown when either transportation direction is bus)
|
||||||
$this->fieldOptionProviders['pickupOutbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
// Uses outbound pickups list, applies to both directions
|
||||||
'label' => 'Zustieg Hinfahrt',
|
$this->fieldOptionProviders['pickup'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||||
|
'label' => 'Zu- und Ausstieg',
|
||||||
'choices' => $bookingDto->travel->pickupsOutbound,
|
'choices' => $bookingDto->travel->pickupsOutbound,
|
||||||
'choice_label' => fn (?Pickup $pickup) => $this->formatPickupLabelWithPrice($pickup),
|
'choice_label' => fn (?Pickup $pickup) => $this->formatPickupLabelWithPrice($pickup),
|
||||||
'choice_value' => 'id',
|
'choice_value' => 'id',
|
||||||
'expanded' => false, // Dropdown for pickups
|
'expanded' => false, // Dropdown for pickups
|
||||||
'multiple' => false,
|
'multiple' => false,
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'placeholder' => 'Zustieg auswählen',
|
'placeholder' => 'Zu- und Ausstieg 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)
|
// Parking (conditional - only shown when outbound transportation is PKW)
|
||||||
@@ -574,29 +562,6 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
return $this->formatServiceLabelWithPrice($parkingService);
|
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.
|
* Checks if a service should be rendered as read-only due to unavailability.
|
||||||
*
|
*
|
||||||
@@ -710,27 +675,4 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
$bookingDto
|
$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, ',', '.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $label;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,8 +69,7 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
'board', // Affects travel price
|
'board', // Affects travel price
|
||||||
'transportationOutbound', // Affects travel price
|
'transportationOutbound', // Affects travel price
|
||||||
'transportationInbound', // Affects travel price
|
'transportationInbound', // Affects travel price
|
||||||
'pickupOutbound', // Affects travel price
|
'pickup', // Affects travel price
|
||||||
'pickupInbound', // Affects travel price
|
|
||||||
'parking', // Affects travel price
|
'parking', // Affects travel price
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-26
@@ -10,36 +10,26 @@ use App\Form\Model\BookingDtoInterface;
|
|||||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles outbound pickup location selection.
|
* Handles unified pickup location selection.
|
||||||
*
|
*
|
||||||
* Pickup selection is only processed when outbound transportation
|
* Pickup selection is only processed when either outbound or inbound
|
||||||
* is bus type. Automatically clears pickup when transportation
|
* transportation is bus type. Automatically clears pickup when both
|
||||||
* changes to self-organized (PKW).
|
* transportation services change to self-organized (PKW).
|
||||||
|
*
|
||||||
|
* The pickup applies to both directions and uses the outbound pickup list.
|
||||||
*/
|
*/
|
||||||
class ParticipantPickupOutboundFieldHandler extends AbstractParticipantFieldHandler
|
class ParticipantPickupFieldHandler extends AbstractParticipantFieldHandler
|
||||||
{
|
{
|
||||||
public function getFieldName(): string
|
public function getFieldName(): string
|
||||||
{
|
{
|
||||||
return 'pickupOutbound';
|
return 'pickup';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDependencies(): array
|
public function getDependencies(): array
|
||||||
{
|
{
|
||||||
return ['transportationOutbound']; // Must process transportation first
|
return ['transportationOutbound', 'transportationInbound'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if this handler should process the field based on submitted data.
|
|
||||||
*
|
|
||||||
* For pickup selection fields, we always need to process to handle cases
|
|
||||||
* where the pickup is cleared due to transportation changes. This ensures the
|
|
||||||
* participant DTO is updated correctly when transportation switches from bus to PKW.
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
|
||||||
* @param int $participantIndex The index of the participant being processed
|
|
||||||
*
|
|
||||||
* @return bool Always returns true for pickup selection fields
|
|
||||||
*/
|
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
return true; // Always process to handle clearing pickup
|
return true; // Always process to handle clearing pickup
|
||||||
@@ -52,10 +42,15 @@ class ParticipantPickupOutboundFieldHandler extends AbstractParticipantFieldHand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only process pickup if outbound transportation is bus
|
// Check if either outbound or inbound transportation is bus
|
||||||
if (null === $participant->transportationOutbound || DirectionMapper::SUBTYPE_BUS_API !== $participant->transportationOutbound->subType) {
|
$hasOutboundBus = null !== $participant->transportationOutbound
|
||||||
$participant->pickupOutbound = null; // Clear pickup for non-bus transport
|
&& DirectionMapper::SUBTYPE_BUS_API === $participant->transportationOutbound->subType;
|
||||||
$participant->pickup = null; // Backward compatibility
|
$hasInboundBus = null !== $participant->transportationInbound
|
||||||
|
&& DirectionMapper::SUBTYPE_BUS_API === $participant->transportationInbound->subType;
|
||||||
|
|
||||||
|
// Only process pickup if at least one direction has bus transport
|
||||||
|
if (false === $hasOutboundBus && false === $hasInboundBus) {
|
||||||
|
$participant->pickup = null; // Clear pickup for non-bus transport
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -68,8 +63,7 @@ class ParticipantPickupOutboundFieldHandler extends AbstractParticipantFieldHand
|
|||||||
$validSelection = $this->findValidPickup($selectedPickup, $bookingDto->travel->pickupsOutbound);
|
$validSelection = $this->findValidPickup($selectedPickup, $bookingDto->travel->pickupsOutbound);
|
||||||
}
|
}
|
||||||
|
|
||||||
$participant->pickupOutbound = $validSelection;
|
$participant->pickup = $validSelection;
|
||||||
$participant->pickup = $validSelection; // Backward compatibility
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,4 +90,4 @@ class ParticipantPickupOutboundFieldHandler extends AbstractParticipantFieldHand
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Form\Service;
|
|
||||||
|
|
||||||
use App\BusProNet\Model\Pickup;
|
|
||||||
use App\BusProNet\Utility\DirectionMapper;
|
|
||||||
use App\Form\Model\BookingDtoInterface;
|
|
||||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles inbound pickup location selection.
|
|
||||||
*
|
|
||||||
* Pickup selection is only processed when inbound transportation
|
|
||||||
* is bus type. Automatically clears pickup when transportation
|
|
||||||
* changes to self-organized (PKW).
|
|
||||||
*/
|
|
||||||
class ParticipantPickupInboundFieldHandler extends AbstractParticipantFieldHandler
|
|
||||||
{
|
|
||||||
public function getFieldName(): string
|
|
||||||
{
|
|
||||||
return 'pickupInbound';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDependencies(): array
|
|
||||||
{
|
|
||||||
return ['transportationInbound']; // Must process transportation first
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if this handler should process the field based on submitted data.
|
|
||||||
*
|
|
||||||
* For pickup selection fields, we always need to process to handle cases
|
|
||||||
* where the pickup is cleared due to transportation changes. This ensures the
|
|
||||||
* participant DTO is updated correctly when transportation switches from bus to PKW.
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
|
||||||
* @param int $participantIndex The index of the participant being processed
|
|
||||||
*
|
|
||||||
* @return bool Always returns true for pickup selection fields
|
|
||||||
*/
|
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
|
||||||
{
|
|
||||||
return true; // Always process to handle clearing pickup
|
|
||||||
}
|
|
||||||
|
|
||||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
|
||||||
{
|
|
||||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
|
||||||
if (null === $participant) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only process pickup if inbound transportation is bus
|
|
||||||
if (null === $participant->transportationInbound || DirectionMapper::SUBTYPE_BUS_API !== $participant->transportationInbound->subType) {
|
|
||||||
$participant->pickupInbound = null; // Clear pickup for non-bus transport
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$selectedPickup = $this->getFieldValue($submittedData, $this->getFieldName());
|
|
||||||
|
|
||||||
// Validate pickup selection against available inbound pickups
|
|
||||||
$validSelection = null;
|
|
||||||
if (null !== $selectedPickup) {
|
|
||||||
$validSelection = $this->findValidPickup($selectedPickup, $bookingDto->travel->pickupsInbound);
|
|
||||||
}
|
|
||||||
|
|
||||||
$participant->pickupInbound = $validSelection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a valid pickup from available pickups.
|
|
||||||
*
|
|
||||||
* @param mixed $selectedPickupId The submitted pickup ID
|
|
||||||
* @param array<int, Pickup> $availablePickups Array of available pickup locations
|
|
||||||
*
|
|
||||||
* @return Pickup|null The valid pickup object, or null if invalid
|
|
||||||
*/
|
|
||||||
private function findValidPickup(mixed $selectedPickupId, array $availablePickups): ?Pickup
|
|
||||||
{
|
|
||||||
if (null === $selectedPickupId || false === is_string($selectedPickupId) && false === is_int($selectedPickupId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pickupId = (int) $selectedPickupId;
|
|
||||||
|
|
||||||
foreach ($availablePickups as $pickup) {
|
|
||||||
if ($pickup->id === $pickupId) {
|
|
||||||
return $pickup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -333,14 +333,9 @@ class BookingPriceCalculatorService
|
|||||||
$participantPickupCost += $participant->transportationInbound->price;
|
$participantPickupCost += $participant->transportationInbound->price;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pickup pricing (outbound)
|
// Pickup pricing (unified for both directions)
|
||||||
if (null !== $participant->pickupOutbound && null !== $participant->pickupOutbound->price) {
|
if (null !== $participant->pickup && null !== $participant->pickup->price) {
|
||||||
$participantPickupCost += $participant->pickupOutbound->price;
|
$participantPickupCost += $participant->pickup->price;
|
||||||
}
|
|
||||||
|
|
||||||
// Pickup pricing (inbound)
|
|
||||||
if (null !== $participant->pickupInbound && null !== $participant->pickupInbound->price) {
|
|
||||||
$participantPickupCost += $participant->pickupInbound->price;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parking service pricing
|
// Parking service pricing
|
||||||
@@ -563,12 +558,8 @@ class BookingPriceCalculatorService
|
|||||||
$serviceTotal += $participant->transportationInbound->price;
|
$serviceTotal += $participant->transportationInbound->price;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $participant->pickupOutbound && null !== $participant->pickupOutbound->price) {
|
if (null !== $participant->pickup && null !== $participant->pickup->price) {
|
||||||
$serviceTotal += $participant->pickupOutbound->price;
|
$serviceTotal += $participant->pickup->price;
|
||||||
}
|
|
||||||
|
|
||||||
if (null !== $participant->pickupInbound && null !== $participant->pickupInbound->price) {
|
|
||||||
$serviceTotal += $participant->pickupInbound->price;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $participant->parkingService && null !== $participant->parkingService->price) {
|
if (null !== $participant->parkingService && null !== $participant->parkingService->price) {
|
||||||
|
|||||||
@@ -54,13 +54,16 @@ class ParticipantValidator extends ConstraintValidator
|
|||||||
|
|
||||||
public function assertPickupSelected(ParticipantDto $participant): void
|
public function assertPickupSelected(ParticipantDto $participant): void
|
||||||
{
|
{
|
||||||
if (
|
// Check if either outbound or inbound transportation is bus
|
||||||
null !== $participant->transportationOutbound
|
$hasOutboundBus = null !== $participant->transportationOutbound
|
||||||
&& 'BUS' === $participant->transportationOutbound->subType
|
&& 'BUS' === $participant->transportationOutbound->subType;
|
||||||
&& null === $participant->pickupOutbound
|
$hasInboundBus = null !== $participant->transportationInbound
|
||||||
) {
|
&& 'BUS' === $participant->transportationInbound->subType;
|
||||||
|
|
||||||
|
// Require pickup if at least one direction has bus transport
|
||||||
|
if (($hasOutboundBus || $hasInboundBus) && null === $participant->pickup) {
|
||||||
$this->context->buildViolation('Bitte auswählen')
|
$this->context->buildViolation('Bitte auswählen')
|
||||||
->atPath('pickupOutbound')
|
->atPath('pickup')
|
||||||
->addViolation()
|
->addViolation()
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,15 +103,12 @@
|
|||||||
{{ form_row(participant.mobile) }}
|
{{ form_row(participant.mobile) }}
|
||||||
</div>
|
</div>
|
||||||
{% if participant.address is defined %}
|
{% if participant.address is defined %}
|
||||||
<fieldset class="border border-gray-300 rounded p-4 mb-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<legend class="font-semibold px-2">{{ participant.address.vars.label }}</legend>
|
{{ form_row(participant.address.street) }}
|
||||||
<div class="grid grid-cols-2 gap-4">
|
{{ form_row(participant.address.postCode) }}
|
||||||
{{ form_row(participant.address.street) }}
|
{{ form_row(participant.address.city) }}
|
||||||
{{ form_row(participant.address.postCode) }}
|
{{ form_row(participant.address.country) }}
|
||||||
{{ form_row(participant.address.city) }}
|
</div>
|
||||||
{{ form_row(participant.address.country) }}
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if participant.bodyDimensions is defined %}
|
{% if participant.bodyDimensions is defined %}
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
@@ -268,34 +265,35 @@
|
|||||||
}
|
}
|
||||||
}) }}
|
}) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if participant.pickupOutbound is defined %}
|
{% if participant.pickup is defined or participant.parking is defined or participant.licensePlate is defined %}
|
||||||
<div class="mt-4">
|
{% if participant.pickup is defined %}
|
||||||
{{ form_row(participant.pickupOutbound, {
|
<div class="mt-4">
|
||||||
'attr': {
|
{{ form_row(participant.pickup, {
|
||||||
'hx-trigger': 'change',
|
'attr': {
|
||||||
'hx-post': path('app_booking_create_step_2_refresh'),
|
'hx-trigger': 'change',
|
||||||
'hx-swap': 'none'
|
'hx-post': path('app_booking_create_step_2_refresh'),
|
||||||
}
|
'hx-swap': 'none'
|
||||||
}) }}
|
}
|
||||||
</div>
|
}) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if participant.parking is defined %}
|
||||||
|
<div class="mt-4">
|
||||||
|
{{ form_row(participant.parking, {
|
||||||
|
'attr': {
|
||||||
|
'hx-trigger': 'change',
|
||||||
|
'hx-post': path('app_booking_create_step_2_refresh'),
|
||||||
|
'hx-swap': 'none'
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if participant.licensePlate is defined %}
|
||||||
|
<div class="mt-4">
|
||||||
|
{{ form_row(participant.licensePlate) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if participant.parking is defined %}
|
|
||||||
<div class="mt-4">
|
|
||||||
{{ form_row(participant.parking, {
|
|
||||||
'attr': {
|
|
||||||
'hx-trigger': 'change',
|
|
||||||
'hx-post': path('app_booking_create_step_2_refresh'),
|
|
||||||
'hx-swap': 'none'
|
|
||||||
}
|
|
||||||
}) }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if participant.licensePlate is defined %}
|
|
||||||
<div class="mt-4">
|
|
||||||
{{ form_row(participant.licensePlate) }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{% if participant.transportationInbound is defined %}
|
{% if participant.transportationInbound is defined %}
|
||||||
@@ -307,17 +305,6 @@
|
|||||||
}
|
}
|
||||||
}) }}
|
}) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if participant.pickupInbound is defined %}
|
|
||||||
<div class="mt-4">
|
|
||||||
{{ form_row(participant.pickupInbound, {
|
|
||||||
'attr': {
|
|
||||||
'hx-trigger': 'change',
|
|
||||||
'hx-post': path('app_booking_create_step_2_refresh'),
|
|
||||||
'hx-swap': 'none'
|
|
||||||
}
|
|
||||||
}) }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -128,17 +128,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Pickup Locations #}
|
{# Pickup Location #}
|
||||||
{% if participant.pickupOutbound %}
|
{% if participant.pickup %}
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-gray-600 font-semibold">Zustieg Hinfahrt</dt>
|
<dt class="text-gray-600 font-semibold">Zu- und Ausstieg</dt>
|
||||||
<dd>{{ participant.pickupOutbound.label }}</dd>
|
<dd>{{ participant.pickup.label }}</dd>
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if participant.pickupInbound %}
|
|
||||||
<div>
|
|
||||||
<dt class="text-gray-600 font-semibold">Zustieg Rückfahrt</dt>
|
|
||||||
<dd>{{ participant.pickupInbound.label }}</dd>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -336,9 +336,9 @@
|
|||||||
}
|
}
|
||||||
}) }}
|
}) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if participant.pickupOutbound is defined %}
|
{% if participant.pickup is defined %}
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
{{ form_row(participant.pickupOutbound, {
|
{{ form_row(participant.pickup, {
|
||||||
'attr': {
|
'attr': {
|
||||||
'hx-trigger': 'change',
|
'hx-trigger': 'change',
|
||||||
'hx-post': path('app_booking_edit_refresh', {'id': bookingData.id}),
|
'hx-post': path('app_booking_edit_refresh', {'id': bookingData.id}),
|
||||||
@@ -381,17 +381,7 @@
|
|||||||
}
|
}
|
||||||
}) }}
|
}) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if participant.pickupInbound is defined %}
|
{# Pickup inbound removed - now unified with pickup field #}
|
||||||
<div class="mt-4">
|
|
||||||
{{ form_row(participant.pickupInbound, {
|
|
||||||
'attr': {
|
|
||||||
'hx-trigger': 'change',
|
|
||||||
'hx-post': path('app_booking_edit_refresh', {'id': bookingData.id}),
|
|
||||||
'hx-swap': 'none'
|
|
||||||
}
|
|
||||||
}) }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ class BookingDataProcessorTest extends TestCase
|
|||||||
$busService = $this->createMockService(1);
|
$busService = $this->createMockService(1);
|
||||||
$busService->subType = 'BUS';
|
$busService->subType = 'BUS';
|
||||||
$participant->transportationOutbound = $busService;
|
$participant->transportationOutbound = $busService;
|
||||||
$participant->pickupOutbound = $this->createMockPickup(1);
|
$participant->pickup = $this->createMockPickup(1);
|
||||||
|
|
||||||
return $formData;
|
return $formData;
|
||||||
}
|
}
|
||||||
@@ -456,7 +456,7 @@ class BookingDataProcessorTest extends TestCase
|
|||||||
$participant->rentals = [];
|
$participant->rentals = [];
|
||||||
$participant->transportationOutbound = $this->createMockService(1);
|
$participant->transportationOutbound = $this->createMockService(1);
|
||||||
$participant->transportationInbound = $this->createMockService(2);
|
$participant->transportationInbound = $this->createMockService(2);
|
||||||
$participant->pickupOutbound = null;
|
$participant->pickup = null;
|
||||||
|
|
||||||
return $participant;
|
return $participant;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,12 +112,12 @@ class ParticipantValidatorTest extends ConstraintValidatorTestCase
|
|||||||
$busService = new Service();
|
$busService = new Service();
|
||||||
$busService->subType = 'BUS';
|
$busService->subType = 'BUS';
|
||||||
$participant->transportationOutbound = $busService;
|
$participant->transportationOutbound = $busService;
|
||||||
$participant->pickupOutbound = null;
|
$participant->pickup = null;
|
||||||
|
|
||||||
$this->validator->validate($participant, new Participant());
|
$this->validator->validate($participant, new Participant());
|
||||||
|
|
||||||
$this->buildViolation('Bitte auswählen')
|
$this->buildViolation('Bitte auswählen')
|
||||||
->atPath('property.path.pickupOutbound')
|
->atPath('property.path.pickup')
|
||||||
->assertRaised();
|
->assertRaised();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ class ParticipantValidatorTest extends ConstraintValidatorTestCase
|
|||||||
$trainService = new Service();
|
$trainService = new Service();
|
||||||
$trainService->subType = 'TRAIN';
|
$trainService->subType = 'TRAIN';
|
||||||
$participant->transportationOutbound = $trainService;
|
$participant->transportationOutbound = $trainService;
|
||||||
$participant->pickupOutbound = null; // Not required for non-bus
|
$participant->pickup = null; // Not required for non-bus
|
||||||
|
|
||||||
$this->validator->validate($participant, new Participant());
|
$this->validator->validate($participant, new Participant());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user