wip: implement transportation, pickups and parking
This commit is contained in:
@@ -9,6 +9,7 @@ use App\Form\Service\Contract\FieldOptionsProviderInterface;
|
||||
use App\Form\Service\CreateFieldStateProvider;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
@@ -242,7 +243,7 @@ class BookingCreateParticipantType extends AbstractType
|
||||
'transportationInbound' => ChoiceType::class,
|
||||
'pickupOutbound' => ChoiceType::class,
|
||||
'pickupInbound' => ChoiceType::class,
|
||||
'parking' => ChoiceType::class,
|
||||
'parking' => CheckboxType::class,
|
||||
];
|
||||
|
||||
foreach ($dynamicFields as $fieldName => $fieldType) {
|
||||
|
||||
@@ -60,8 +60,8 @@ class ParticipantDto
|
||||
public ?Pickup $pickupOutbound = null;
|
||||
public ?Pickup $pickupInbound = null;
|
||||
|
||||
// Parking service for self-organized transportation
|
||||
public ?Service $parking = null;
|
||||
// Parking service for self-organized transportation (boolean: true if parking requested)
|
||||
public bool $parking = false;
|
||||
|
||||
// Deprecated properties for backward compatibility - will be removed in future version
|
||||
public ?Service $transportationServiceTo = null;
|
||||
|
||||
@@ -23,8 +23,8 @@ use App\Form\Service\Condition\ServiceSubTypeCondition;
|
||||
* Current field state conditions:
|
||||
* - Body dimension fields become required when rental services are selected
|
||||
* - Age-dependent service fields are hidden until birth date is provided
|
||||
* - Transportation pickup fields are hidden when transportation type is not BUS
|
||||
* - Parking field is hidden when outbound transportation is not PKW
|
||||
* - Transportation pickup fields are hidden by default, shown only when transportation type is BUS
|
||||
* - Parking field is hidden by default, shown only when outbound transportation is PKW
|
||||
*/
|
||||
class CreateFieldStateProvider extends AbstractFieldStateProvider
|
||||
{
|
||||
@@ -99,20 +99,26 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
|
||||
|
||||
// Transportation-related field conditions
|
||||
|
||||
// Hide outbound pickup when transportation is not BUS
|
||||
// Show outbound pickup only when transportation is BUS (hidden by default)
|
||||
$this->fieldStateConditions['pickupOutbound'] = [
|
||||
'hidden' => ServiceSubTypeCondition::notEquals('transportationOutbound', DirectionMapper::SUBTYPE_BUS_API),
|
||||
'hidden' => CompositeCondition::not(
|
||||
ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_BUS_API)
|
||||
),
|
||||
];
|
||||
|
||||
// Hide inbound pickup when transportation is not BUS
|
||||
// Show inbound pickup only when transportation is BUS (hidden by default)
|
||||
$this->fieldStateConditions['pickupInbound'] = [
|
||||
'hidden' => ServiceSubTypeCondition::notEquals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API),
|
||||
'hidden' => CompositeCondition::not(
|
||||
ServiceSubTypeCondition::equals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API)
|
||||
),
|
||||
];
|
||||
|
||||
// Hide parking when outbound transportation is not PKW (car)
|
||||
// Show parking only when outbound transportation is PKW (hidden by default)
|
||||
// Parking is offered at holiday destination for those arriving by car
|
||||
$this->fieldStateConditions['parking'] = [
|
||||
'hidden' => ServiceSubTypeCondition::notEquals('transportationOutbound', DirectionMapper::SUBTYPE_CAR_API),
|
||||
'hidden' => CompositeCondition::not(
|
||||
ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_CAR_API)
|
||||
),
|
||||
];
|
||||
|
||||
// Example field state conditions would be registered here
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Model\Pickup;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
@@ -339,6 +340,11 @@ class ParticipantFieldHandlerRegistry
|
||||
return $value->id;
|
||||
}
|
||||
|
||||
// Handle Pickup objects -> convert to ID
|
||||
if ($value instanceof Pickup) {
|
||||
return $value->id;
|
||||
}
|
||||
|
||||
// Handle DateTimeInterface -> convert to string format
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
return $value->format('Y-m-d');
|
||||
|
||||
@@ -227,7 +227,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
$this->fieldOptionProviders['pickupOutbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Zustieg Hinfahrt',
|
||||
'choices' => $bookingDto->travel->pickupsTo,
|
||||
'choice_label' => fn (Pickup $pickup) => $this->formatPickupLabel($pickup),
|
||||
'choice_label' => fn (?Pickup $pickup) => $this->formatPickupLabelWithPrice($pickup),
|
||||
'choice_value' => 'id',
|
||||
'expanded' => false, // Dropdown for pickups
|
||||
'multiple' => false,
|
||||
@@ -237,24 +237,20 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
|
||||
// Inbound Pickup (conditional - only shown when inbound transportation is bus)
|
||||
$this->fieldOptionProviders['pickupInbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Zustieg Rückfahrt',
|
||||
'label' => 'Ausstieg Rückfahrt',
|
||||
'choices' => $bookingDto->travel->pickupsFro,
|
||||
'choice_label' => fn (Pickup $pickup) => $this->formatPickupLabel($pickup),
|
||||
'choice_label' => fn (?Pickup $pickup) => $this->formatPickupLabelWithPrice($pickup),
|
||||
'choice_value' => 'id',
|
||||
'expanded' => false,
|
||||
'multiple' => false,
|
||||
'required' => true,
|
||||
'placeholder' => 'Zustieg auswählen',
|
||||
'placeholder' => 'Ausstieg auswählen',
|
||||
];
|
||||
|
||||
// Parking (conditional - only shown when at least one transportation direction is PKW)
|
||||
// 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' => 'Parkplatz',
|
||||
'choices' => $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_PARKING, true),
|
||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
||||
'choice_value' => 'id',
|
||||
'expanded' => true,
|
||||
'multiple' => false,
|
||||
'label' => $this->getParkingCheckboxLabel($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_PARKING, true)),
|
||||
'required' => false,
|
||||
];
|
||||
|
||||
@@ -287,6 +283,11 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
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, ',', '.'));
|
||||
}
|
||||
|
||||
@@ -307,16 +308,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
{
|
||||
$label = $service->label;
|
||||
|
||||
// Add transportation type indicator
|
||||
$typeIndicator = match ($service->subType) {
|
||||
'BUS' => '🚌',
|
||||
'PKW' => '🚗',
|
||||
default => '',
|
||||
};
|
||||
|
||||
if ($typeIndicator) {
|
||||
$label = $typeIndicator.' '.$label;
|
||||
}
|
||||
// Transportation type indicators removed for cleaner labels
|
||||
|
||||
// Add pricing with discount indication
|
||||
if (null !== $service->price) {
|
||||
@@ -357,6 +349,47 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
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 'Parkplatz';
|
||||
}
|
||||
|
||||
$parkingService = reset($parkingServices); // Get the first (and only) parking service
|
||||
|
||||
return $this->formatServiceLabelWithPrice($parkingService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters services based on participant's age constraints.
|
||||
*
|
||||
|
||||
@@ -4,8 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Utility\DirectionMapper;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
@@ -13,9 +11,9 @@ use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
/**
|
||||
* Handles parking service selection for self-organized transportation.
|
||||
*
|
||||
* Parking is only available when at least one direction uses
|
||||
* self-organized (PKW) transportation. Automatically clears parking
|
||||
* when both transportation directions are bus-only.
|
||||
* Parking is only available when outbound transportation is PKW (car).
|
||||
* This is because parking is needed at the destination for those arriving by car.
|
||||
* Automatically clears parking when outbound transportation is not PKW.
|
||||
*/
|
||||
class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler
|
||||
{
|
||||
@@ -53,28 +51,23 @@ class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if parking is applicable (at least one PKW direction)
|
||||
// Check if parking is applicable (outbound transportation is PKW)
|
||||
if (!$this->isParkingApplicable($participant)) {
|
||||
$participant->parking = null; // Clear parking for bus-only transport
|
||||
$participant->parking = false; // Clear parking when outbound is not PKW
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$selectedParking = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||
$parkingSelected = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||
|
||||
// Get available parking services (subtype PAR)
|
||||
$availableParkingServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_PARKING, true);
|
||||
|
||||
$validSelection = null;
|
||||
if (null !== $selectedParking) {
|
||||
$validSelection = $this->findValidParkingService($selectedParking, $availableParkingServices);
|
||||
}
|
||||
|
||||
$participant->parking = $validSelection;
|
||||
// Store boolean value directly (true if checkbox checked, false otherwise)
|
||||
$participant->parking = (bool) $parkingSelected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if parking is applicable based on transportation selections.
|
||||
* Checks if parking is applicable based on outbound transportation selection.
|
||||
*
|
||||
* Parking is only needed when arriving by car at the destination.
|
||||
*
|
||||
* @param object $participant The participant DTO
|
||||
*
|
||||
@@ -82,34 +75,6 @@ class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler
|
||||
*/
|
||||
private function isParkingApplicable(object $participant): bool
|
||||
{
|
||||
$outboundIsPkw = DirectionMapper::SUBTYPE_CAR_API === $participant->transportationOutbound?->subType;
|
||||
$inboundIsPkw = DirectionMapper::SUBTYPE_CAR_API === $participant->transportationInbound?->subType;
|
||||
|
||||
return $outboundIsPkw || $inboundIsPkw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a valid parking service from available parking services.
|
||||
*
|
||||
* @param mixed $selectedServiceId The submitted service ID
|
||||
* @param array<int, Service> $availableServices Array of available parking services
|
||||
*
|
||||
* @return Service|null The valid service object, or null if invalid
|
||||
*/
|
||||
private function findValidParkingService(mixed $selectedServiceId, array $availableServices): ?Service
|
||||
{
|
||||
if (null === $selectedServiceId || false === is_string($selectedServiceId) && false === is_int($selectedServiceId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$serviceId = (int) $selectedServiceId;
|
||||
|
||||
foreach ($availableServices as $service) {
|
||||
if ($service->id === $serviceId) {
|
||||
return $service;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return DirectionMapper::SUBTYPE_CAR_API === $participant->transportationOutbound?->subType;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user