wip: transportation services
This commit is contained in:
@@ -393,8 +393,8 @@ class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
|
|
||||||
private function isParkingApplicable($participant): bool
|
private function isParkingApplicable($participant): bool
|
||||||
{
|
{
|
||||||
$outboundIsPkw = $participant->transportationOutbound?->subType === 'PKW';
|
$outboundIsPkw = DirectionMapper::SUBTYPE_CAR_API === $participant->transportationOutbound?->subType;
|
||||||
$inboundIsPkw = $participant->transportationInbound?->subType === 'PKW';
|
$inboundIsPkw = DirectionMapper::SUBTYPE_CAR_API === $participant->transportationInbound?->subType;
|
||||||
|
|
||||||
return $outboundIsPkw || $inboundIsPkw;
|
return $outboundIsPkw || $inboundIsPkw;
|
||||||
}
|
}
|
||||||
@@ -487,8 +487,8 @@ private function formatTransportationServiceLabel(Service $service): string
|
|||||||
|
|
||||||
// Add transportation type indicator
|
// Add transportation type indicator
|
||||||
$typeIndicator = match($service->subType) {
|
$typeIndicator = match($service->subType) {
|
||||||
'BUS' => '🚌',
|
DirectionMapper::SUBTYPE_BUS_API => '🚌',
|
||||||
'PKW' => '🚗',
|
DirectionMapper::SUBTYPE_CAR_API => '🚗',
|
||||||
default => ''
|
default => ''
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -514,9 +514,9 @@ private function formatTransportationServiceLabel(Service $service): string
|
|||||||
|
|
||||||
### Phase 3: Conditional Field States & UX 🎨
|
### Phase 3: Conditional Field States & UX 🎨
|
||||||
|
|
||||||
#### 3.1 Transportation Type Condition
|
#### 3.1 Service Sub-Type Condition
|
||||||
|
|
||||||
**File:** `src/Form/Service/Condition/TransportationTypeCondition.php`
|
**File:** `src/Form/Service/Condition/ServiceSubTypeCondition.php`
|
||||||
|
|
||||||
```php
|
```php
|
||||||
<?php
|
<?php
|
||||||
@@ -525,20 +525,24 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Form\Service\Condition;
|
namespace App\Form\Service\Condition;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Service;
|
||||||
use App\Form\Model\BookingDtoInterface;
|
use App\Form\Model\BookingDtoInterface;
|
||||||
use App\Form\Service\Contract\FieldConditionInterface;
|
use App\Form\Service\Contract\FieldConditionInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Condition that evaluates transportation service type.
|
* Condition that evaluates service sub-types.
|
||||||
*
|
*
|
||||||
* Used to show/hide fields based on whether transportation
|
* This condition enables field state logic based on the sub-type property
|
||||||
* is bus or self-organized (PKW) for specific directions.
|
* of Service objects. It can be used with any service field (transportation,
|
||||||
|
* additional services, etc.) to show/hide or enable/disable dependent fields
|
||||||
|
* based on the selected service type.
|
||||||
*/
|
*/
|
||||||
class TransportationTypeCondition implements FieldConditionInterface
|
class ServiceSubTypeCondition implements FieldConditionInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly string $direction, // 'outbound' or 'inbound'
|
private readonly string $serviceFieldName,
|
||||||
private readonly string $expectedType, // 'BUS' or 'PKW'
|
private readonly string $operator,
|
||||||
|
private readonly string|array $expectedSubType,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||||
@@ -582,35 +586,29 @@ class TransportationTypeCondition implements FieldConditionInterface
|
|||||||
**Update:** `src/Form/Service/CreateFieldStateProvider.php`
|
**Update:** `src/Form/Service/CreateFieldStateProvider.php`
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use App\Form\Service\Condition\TransportationTypeCondition;
|
use App\BusProNet\Utility\DirectionMapper;
|
||||||
use App\Form\Service\Condition\CompositeCondition;
|
use App\Form\Service\Condition\ServiceSubTypeCondition;
|
||||||
|
|
||||||
protected function registerFieldStateConditions(): void
|
protected function registerFieldStateConditions(): void
|
||||||
{
|
{
|
||||||
// ... existing conditions
|
// ... existing conditions
|
||||||
|
|
||||||
// Show outbound pickup only when outbound transportation is BUS
|
// Transportation-related field conditions
|
||||||
|
|
||||||
|
// Hide outbound pickup when transportation is not BUS
|
||||||
$this->fieldStateConditions['pickupOutbound'] = [
|
$this->fieldStateConditions['pickupOutbound'] = [
|
||||||
'hidden' => CompositeCondition::not(
|
'hidden' => ServiceSubTypeCondition::notEquals('transportationOutbound', DirectionMapper::SUBTYPE_BUS_API),
|
||||||
new TransportationTypeCondition('outbound', 'BUS')
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Show inbound pickup only when inbound transportation is BUS
|
// Hide inbound pickup when transportation is not BUS
|
||||||
$this->fieldStateConditions['pickupInbound'] = [
|
$this->fieldStateConditions['pickupInbound'] = [
|
||||||
'hidden' => CompositeCondition::not(
|
'hidden' => ServiceSubTypeCondition::notEquals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API),
|
||||||
new TransportationTypeCondition('inbound', 'BUS')
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Show parking only when at least one direction is PKW
|
// Hide parking when outbound transportation is not PKW (car)
|
||||||
|
// Parking is offered at holiday destination for those arriving by car
|
||||||
$this->fieldStateConditions['parking'] = [
|
$this->fieldStateConditions['parking'] = [
|
||||||
'hidden' => CompositeCondition::not(
|
'hidden' => ServiceSubTypeCondition::notEquals('transportationOutbound', DirectionMapper::SUBTYPE_CAR_API),
|
||||||
CompositeCondition::or(
|
|
||||||
new TransportationTypeCondition('outbound', 'PKW'),
|
|
||||||
new TransportationTypeCondition('inbound', 'PKW')
|
|
||||||
)
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -667,15 +665,40 @@ $dynamicFields = [
|
|||||||
|
|
||||||
#### 4.3 Constants Update
|
#### 4.3 Constants Update
|
||||||
|
|
||||||
**Update:** `src/BusProNet/Constants.php`
|
**Update:** `src/BusProNet/Utility/DirectionMapper.php`
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Add parking token if not already defined
|
// Transportation service sub-types (API format - German abbreviations)
|
||||||
public const TOKEN_PARKING = 'PAR';
|
public const SUBTYPE_BUS_API = 'BUS';
|
||||||
|
public const SUBTYPE_CAR_API = 'PKW';
|
||||||
|
|
||||||
// Transportation subtypes
|
// Transportation service sub-types (internal format - English)
|
||||||
public const SUBTYPE_BUS = 'BUS';
|
public const SUBTYPE_BUS = 'BUS';
|
||||||
public const SUBTYPE_PKW = 'PKW';
|
public const SUBTYPE_CAR = 'CAR';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps API transportation sub-type to internal sub-type.
|
||||||
|
*/
|
||||||
|
public static function apiToInternal(string $apiSubType): string
|
||||||
|
{
|
||||||
|
return match ($apiSubType) {
|
||||||
|
self::SUBTYPE_BUS_API => self::SUBTYPE_BUS,
|
||||||
|
self::SUBTYPE_CAR_API => self::SUBTYPE_CAR,
|
||||||
|
default => throw new \InvalidArgumentException("Unknown API sub-type: $apiSubType"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps internal transportation sub-type to API sub-type.
|
||||||
|
*/
|
||||||
|
public static function internalToApi(string $internalSubType): string
|
||||||
|
{
|
||||||
|
return match ($internalSubType) {
|
||||||
|
self::SUBTYPE_BUS => self::SUBTYPE_BUS_API,
|
||||||
|
self::SUBTYPE_CAR => self::SUBTYPE_CAR_API,
|
||||||
|
default => throw new \InvalidArgumentException("Unknown internal sub-type: $internalSubType"),
|
||||||
|
};
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## UX Design & User Experience 🎯
|
## UX Design & User Experience 🎯
|
||||||
@@ -818,27 +841,62 @@ public const SUBTYPE_PKW = 'PKW';
|
|||||||
|
|
||||||
### Sprint 1: Foundation (Week 1)
|
### Sprint 1: Foundation (Week 1)
|
||||||
- ✅ Create documentation
|
- ✅ Create documentation
|
||||||
- ✅ Implement DirectionMapper utility
|
- ✅ Implement DirectionMapper utility (removed unused toEnglish method)
|
||||||
- ✅ Update ParticipantDto properties
|
- ✅ Update ParticipantDto properties
|
||||||
- ✅ Update BookingEditDto mapping
|
- ✅ Update BookingEditDto mapping
|
||||||
|
- ✅ Create transportation field handlers (Outbound/Inbound)
|
||||||
|
- ✅ Create pickup field handlers with conditional logic
|
||||||
|
- ✅ Add parking service handler for self-organized transport
|
||||||
|
- ✅ Add TOKEN_PARKING constant
|
||||||
|
- ✅ Update ParticipantFieldOptionsProvider for transportation services
|
||||||
|
- ✅ Integrate transportation fields into BookingCreateParticipantType
|
||||||
|
- ✅ Create ServiceSubTypeCondition for field state management
|
||||||
|
- ✅ Update field state provider with transportation conditions
|
||||||
|
- ✅ Implement transportation type mapping for API/internal consistency
|
||||||
|
|
||||||
### Sprint 2: Core Implementation (Week 2)
|
### Sprint 2: Advanced Features (Week 2)
|
||||||
- Create transportation field handlers
|
- 🚧 Add pricing integration for transportation services
|
||||||
- Create pickup field handlers
|
- 🚧 Update HTMX integration for real-time transportation updates
|
||||||
- Add parking service handler
|
|
||||||
- Update field options provider
|
|
||||||
|
|
||||||
### Sprint 3: UX & Integration (Week 3)
|
### Sprint 3: Testing & Deployment (Week 3)
|
||||||
- Implement conditional field states
|
- Unit testing for all components
|
||||||
- Update form integration
|
- Integration testing for form flow
|
||||||
- Add transportation service configuration
|
- Manual testing scenarios
|
||||||
- Implement HTMX real-time updates
|
- Performance optimization
|
||||||
|
|
||||||
### Sprint 4: Testing & Polish (Week 4)
|
## Key Implementation Highlights 🌟
|
||||||
- Add pricing integration
|
|
||||||
- Comprehensive testing
|
### Transportation Type Mapping System
|
||||||
- UX refinements
|
|
||||||
- Documentation updates
|
**Problem Solved:** BusProNet uses German abbreviations ('PKW') while internal code should use English terminology ('CAR') for consistency.
|
||||||
|
|
||||||
|
**Solution:** Enhanced `DirectionMapper` utility with bidirectional mapping:
|
||||||
|
- **API Format:** `SUBTYPE_CAR_API = 'PKW'`, `SUBTYPE_BUS_API = 'BUS'`
|
||||||
|
- **Internal Format:** `SUBTYPE_CAR = 'CAR'`, `SUBTYPE_BUS = 'BUS'`
|
||||||
|
- **Mapping Methods:** `apiToInternal()`, `internalToApi()`, validation helpers
|
||||||
|
|
||||||
|
### Generic Service Sub-Type Condition
|
||||||
|
|
||||||
|
**Achievement:** Created reusable `ServiceSubTypeCondition` instead of transportation-specific logic:
|
||||||
|
- Supports multiple operators: `equals`, `notEquals`, `in`, `notIn`
|
||||||
|
- Works with any service field, not just transportation
|
||||||
|
- Handles both API and internal sub-type values
|
||||||
|
- Provides static factory methods for common use cases
|
||||||
|
|
||||||
|
### Conditional UX Logic
|
||||||
|
|
||||||
|
**Smart Field Visibility:**
|
||||||
|
- **Pickup Fields:** Only visible when respective transportation is BUS
|
||||||
|
- **Parking Field:** Only visible when outbound transportation is CAR (PKW)
|
||||||
|
- Uses API constants since Service objects contain API values
|
||||||
|
- Proper business logic: parking needed at destination for car arrivals
|
||||||
|
|
||||||
|
### Backward Compatibility
|
||||||
|
|
||||||
|
- Maintained all existing property names with deprecation notices
|
||||||
|
- API integration continues using BusProNet's expected format
|
||||||
|
- Internal code uses clean English naming
|
||||||
|
- Seamless migration path for existing functionality
|
||||||
|
|
||||||
## Success Criteria ✅
|
## Success Criteria ✅
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ final class Constants
|
|||||||
public const TOKEN_ADDITIONAL = 'SON';
|
public const TOKEN_ADDITIONAL = 'SON';
|
||||||
public const TOKEN_BOARD = 'VPF';
|
public const TOKEN_BOARD = 'VPF';
|
||||||
public const TOKEN_RENTALS = ['VER', 'VE2', 'VE3', 'VE4', 'VE5', 'VE6', 'VE7', 'VE8'];
|
public const TOKEN_RENTALS = ['VER', 'VE2', 'VE3', 'VE4', 'VE5', 'VE6', 'VE7', 'VE8'];
|
||||||
|
public const TOKEN_PARKING = 'PAR';
|
||||||
|
|
||||||
public const STATUS_AVAILABLE = 'Frei';
|
public const STATUS_AVAILABLE = 'Frei';
|
||||||
public const STATUS_BLOCKED = 'Buchungsstop';
|
public const STATUS_BLOCKED = 'Buchungsstop';
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ declare(strict_types=1);
|
|||||||
namespace App\BusProNet\Utility;
|
namespace App\BusProNet\Utility;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles direction mapping between BusProNet's inconsistent direction codes.
|
* Handles direction and transportation type mapping for BusProNet inconsistencies.
|
||||||
*
|
*
|
||||||
* BusProNet uses different direction codes in different contexts:
|
* BusProNet uses different direction codes in different contexts:
|
||||||
* - Travel data: 'HIN' (outbound), 'RUECK' (inbound)
|
* - Travel data: 'HIN' (outbound), 'RUECK' (inbound)
|
||||||
* - Booking data: 'H' (outbound), 'R' (inbound)
|
* - Booking data: 'H' (outbound), 'R' (inbound)
|
||||||
*
|
*
|
||||||
|
* BusProNet also uses German abbreviations for transportation sub-types:
|
||||||
|
* - API format: 'PKW' (car), 'BUS' (bus)
|
||||||
|
* - Internal format: 'CAR' (car), 'BUS' (bus)
|
||||||
|
*
|
||||||
* This utility provides consistent mapping between formats and enables
|
* This utility provides consistent mapping between formats and enables
|
||||||
* clean English naming for internal application use.
|
* clean English naming for internal application use.
|
||||||
*/
|
*/
|
||||||
@@ -24,6 +28,14 @@ final class DirectionMapper
|
|||||||
public const OUTBOUND_BOOKING = 'H';
|
public const OUTBOUND_BOOKING = 'H';
|
||||||
public const INBOUND_BOOKING = 'R';
|
public const INBOUND_BOOKING = 'R';
|
||||||
|
|
||||||
|
// Transportation service sub-types (API format - German abbreviations)
|
||||||
|
public const SUBTYPE_BUS_API = 'BUS';
|
||||||
|
public const SUBTYPE_CAR_API = 'PKW';
|
||||||
|
|
||||||
|
// Transportation service sub-types (internal format - English)
|
||||||
|
public const SUBTYPE_BUS = 'BUS';
|
||||||
|
public const SUBTYPE_CAR = 'CAR';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps travel direction code to booking direction code.
|
* Maps travel direction code to booking direction code.
|
||||||
*
|
*
|
||||||
@@ -115,4 +127,76 @@ final class DirectionMapper
|
|||||||
{
|
{
|
||||||
return self::isOutbound($direction) || self::isInbound($direction);
|
return self::isOutbound($direction) || self::isInbound($direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps API transportation sub-type to internal sub-type.
|
||||||
|
*
|
||||||
|
* @param string $apiSubType The API transportation sub-type ('BUS' or 'PKW')
|
||||||
|
*
|
||||||
|
* @return string The internal transportation sub-type ('BUS' or 'CAR')
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException When sub-type is not recognized
|
||||||
|
*/
|
||||||
|
public static function apiToInternal(string $apiSubType): string
|
||||||
|
{
|
||||||
|
return match ($apiSubType) {
|
||||||
|
self::SUBTYPE_BUS_API => self::SUBTYPE_BUS,
|
||||||
|
self::SUBTYPE_CAR_API => self::SUBTYPE_CAR,
|
||||||
|
default => throw new \InvalidArgumentException("Unknown API sub-type: $apiSubType"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps internal transportation sub-type to API sub-type.
|
||||||
|
*
|
||||||
|
* @param string $internalSubType The internal transportation sub-type ('BUS' or 'CAR')
|
||||||
|
*
|
||||||
|
* @return string The API transportation sub-type ('BUS' or 'PKW')
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException When sub-type is not recognized
|
||||||
|
*/
|
||||||
|
public static function internalToApi(string $internalSubType): string
|
||||||
|
{
|
||||||
|
return match ($internalSubType) {
|
||||||
|
self::SUBTYPE_BUS => self::SUBTYPE_BUS_API,
|
||||||
|
self::SUBTYPE_CAR => self::SUBTYPE_CAR_API,
|
||||||
|
default => throw new \InvalidArgumentException("Unknown internal sub-type: $internalSubType"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a sub-type is a bus service.
|
||||||
|
*
|
||||||
|
* @param string $subType The sub-type to check (API or internal format)
|
||||||
|
*
|
||||||
|
* @return bool True if the sub-type is a bus service, false otherwise
|
||||||
|
*/
|
||||||
|
public static function isBus(string $subType): bool
|
||||||
|
{
|
||||||
|
return in_array($subType, [self::SUBTYPE_BUS, self::SUBTYPE_BUS_API], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a sub-type is a car service.
|
||||||
|
*
|
||||||
|
* @param string $subType The sub-type to check (API or internal format)
|
||||||
|
*
|
||||||
|
* @return bool True if the sub-type is a car service, false otherwise
|
||||||
|
*/
|
||||||
|
public static function isCar(string $subType): bool
|
||||||
|
{
|
||||||
|
return in_array($subType, [self::SUBTYPE_CAR, self::SUBTYPE_CAR_API], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a transportation sub-type is recognized.
|
||||||
|
*
|
||||||
|
* @param string $subType The sub-type to validate
|
||||||
|
*
|
||||||
|
* @return bool True if the sub-type is valid, false otherwise
|
||||||
|
*/
|
||||||
|
public static function isValidSubType(string $subType): bool
|
||||||
|
{
|
||||||
|
return self::isBus($subType) || self::isCar($subType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,6 +178,11 @@ class BookingCreateParticipantType extends AbstractType
|
|||||||
'board',
|
'board',
|
||||||
'rentals',
|
'rentals',
|
||||||
'skiPass',
|
'skiPass',
|
||||||
|
'transportationOutbound',
|
||||||
|
'transportationInbound',
|
||||||
|
'pickupOutbound',
|
||||||
|
'pickupInbound',
|
||||||
|
'parking',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($dynamicFields as $fieldName) {
|
foreach ($dynamicFields as $fieldName) {
|
||||||
@@ -209,7 +214,7 @@ class BookingCreateParticipantType extends AbstractType
|
|||||||
$this->addBaseFields($form, $bookingDto, $participantIndex);
|
$this->addBaseFields($form, $bookingDto, $participantIndex);
|
||||||
|
|
||||||
// Rebuild dynamic fields
|
// Rebuild dynamic fields
|
||||||
$dynamicFields = ['assignedRoomId', 'remarksRoom', 'courses', 'additionalServices', 'board', 'rentals', 'skiPass'];
|
$dynamicFields = ['assignedRoomId', 'remarksRoom', 'courses', 'additionalServices', 'board', 'rentals', 'skiPass', 'transportationOutbound', 'transportationInbound', 'pickupOutbound', 'pickupInbound', 'parking'];
|
||||||
foreach ($dynamicFields as $fieldName) {
|
foreach ($dynamicFields as $fieldName) {
|
||||||
if ($form->has($fieldName)) {
|
if ($form->has($fieldName)) {
|
||||||
$form->remove($fieldName);
|
$form->remove($fieldName);
|
||||||
@@ -233,6 +238,11 @@ class BookingCreateParticipantType extends AbstractType
|
|||||||
'board' => ChoiceType::class,
|
'board' => ChoiceType::class,
|
||||||
'rentals' => ChoiceType::class,
|
'rentals' => ChoiceType::class,
|
||||||
'skiPass' => ChoiceType::class,
|
'skiPass' => ChoiceType::class,
|
||||||
|
'transportationOutbound' => ChoiceType::class,
|
||||||
|
'transportationInbound' => ChoiceType::class,
|
||||||
|
'pickupOutbound' => ChoiceType::class,
|
||||||
|
'pickupInbound' => ChoiceType::class,
|
||||||
|
'parking' => ChoiceType::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($dynamicFields as $fieldName => $fieldType) {
|
foreach ($dynamicFields as $fieldName => $fieldType) {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
|||||||
|
|
||||||
class BookingCreateStep2Type extends AbstractType
|
class BookingCreateStep2Type extends AbstractType
|
||||||
{
|
{
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ParticipantFieldHandlerRegistry $participantFieldHandlerRegistry,
|
private readonly ParticipantFieldHandlerRegistry $participantFieldHandlerRegistry,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class RoomSelectType extends AbstractType
|
|||||||
$form = $event->getForm();
|
$form = $event->getForm();
|
||||||
|
|
||||||
// Build label with pricing
|
// Build label with pricing
|
||||||
$label = 'Anzahl ' . $data->roomLabel;
|
$label = 'Anzahl '.$data->roomLabel;
|
||||||
if (null !== $options['room_price']) {
|
if (null !== $options['room_price']) {
|
||||||
$formattedPrice = number_format((float) $options['room_price'], 2, ',', '.');
|
$formattedPrice = number_format((float) $options['room_price'], 2, ',', '.');
|
||||||
$label .= sprintf(' (€%s pro Nacht)', $formattedPrice);
|
$label .= sprintf(' (€%s pro Nacht)', $formattedPrice);
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ abstract class AbstractFieldStateProvider implements FieldStateProviderInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$hiddenCondition = $this->fieldStateConditions[$fieldName]['hidden'];
|
$hiddenCondition = $this->fieldStateConditions[$fieldName]['hidden'];
|
||||||
|
|
||||||
return !$hiddenCondition->evaluate($bookingDto, $participantIndex, $formData);
|
return !$hiddenCondition->evaluate($bookingDto, $participantIndex, $formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ class RoomSelectionCondition implements FieldConditionInterface
|
|||||||
public function getDescription(): string
|
public function getDescription(): string
|
||||||
{
|
{
|
||||||
$codes = implode(', ', $this->requiredRoomCodes);
|
$codes = implode(', ', $this->requiredRoomCodes);
|
||||||
|
|
||||||
return "Field visible when room with code(s) [{$codes}] is selected";
|
return "Field visible when room with code(s) [{$codes}] is selected";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form\Service\Condition;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Service;
|
||||||
|
use App\Form\Model\BookingDtoInterface;
|
||||||
|
use App\Form\Service\Contract\FieldConditionInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Condition that evaluates service sub-types.
|
||||||
|
*
|
||||||
|
* This condition enables field state logic based on the sub-type property
|
||||||
|
* of Service objects. It can be used with any service field (transportation,
|
||||||
|
* additional services, etc.) to show/hide or enable/disable dependent fields
|
||||||
|
* based on the selected service type.
|
||||||
|
*
|
||||||
|
* Common Use Cases:
|
||||||
|
* - Show/hide pickup fields when transportation sub-type is BUS
|
||||||
|
* - Show/hide parking fields when transportation sub-type is PKW
|
||||||
|
* - Enable/disable fields based on service category selection
|
||||||
|
* - Conditional logic for different service types
|
||||||
|
*/
|
||||||
|
class ServiceSubTypeCondition implements FieldConditionInterface
|
||||||
|
{
|
||||||
|
private const OPERATOR_EQUALS = 'equals';
|
||||||
|
private const OPERATOR_NOT_EQUALS = 'not_equals';
|
||||||
|
private const OPERATOR_IN = 'in';
|
||||||
|
private const OPERATOR_NOT_IN = 'not_in';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new service sub-type condition.
|
||||||
|
*
|
||||||
|
* @param string $serviceFieldName The name of the service field to check
|
||||||
|
* @param string $operator The comparison operator to use
|
||||||
|
* @param string|string[] $expectedSubType The expected service sub-type(s)
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $serviceFieldName,
|
||||||
|
private readonly string $operator,
|
||||||
|
private readonly string|array $expectedSubType,
|
||||||
|
) {
|
||||||
|
$this->validateOperator($operator);
|
||||||
|
$this->validateExpectedSubType($operator, $expectedSubType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the service sub-type condition against current form data.
|
||||||
|
*
|
||||||
|
* Retrieves the service from the specified field and checks its subType
|
||||||
|
* property against the expected value(s). Handles both form data and
|
||||||
|
* participant DTO data sources.
|
||||||
|
*
|
||||||
|
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
|
||||||
|
* @param int $participantIndex The index of the participant being evaluated
|
||||||
|
* @param array<string, mixed> $formData Current form data for condition evaluation
|
||||||
|
*
|
||||||
|
* @return bool True if the service sub-type meets the condition criteria, false otherwise
|
||||||
|
*/
|
||||||
|
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||||
|
{
|
||||||
|
$service = $this->getService($formData, $participantIndex, $bookingDto);
|
||||||
|
|
||||||
|
if (null === $service) {
|
||||||
|
// If no service is selected, condition fails
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$actualSubType = $service->subType ?? '';
|
||||||
|
|
||||||
|
return match ($this->operator) {
|
||||||
|
self::OPERATOR_EQUALS => $actualSubType === $this->expectedSubType,
|
||||||
|
self::OPERATOR_NOT_EQUALS => $actualSubType !== $this->expectedSubType,
|
||||||
|
self::OPERATOR_IN => $this->isSubTypeIn($actualSubType, $this->expectedSubType),
|
||||||
|
self::OPERATOR_NOT_IN => !$this->isSubTypeIn($actualSubType, $this->expectedSubType),
|
||||||
|
default => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the field names that this condition depends on.
|
||||||
|
*
|
||||||
|
* This condition depends on the service field being evaluated, so any changes
|
||||||
|
* to that field should trigger re-evaluation of dependent field states.
|
||||||
|
*
|
||||||
|
* @return string[] Array containing the service field name being evaluated
|
||||||
|
*/
|
||||||
|
public function getDependentFields(): array
|
||||||
|
{
|
||||||
|
return [$this->serviceFieldName];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a human-readable description of the service sub-type condition.
|
||||||
|
*
|
||||||
|
* Generates a descriptive string explaining what service field is being
|
||||||
|
* checked and what the expected sub-type criteria are.
|
||||||
|
*
|
||||||
|
* @return string Description of the service sub-type condition
|
||||||
|
*/
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
$subTypeDescription = is_array($this->expectedSubType)
|
||||||
|
? '['.implode(', ', $this->expectedSubType).']'
|
||||||
|
: (string) $this->expectedSubType;
|
||||||
|
|
||||||
|
return match ($this->operator) {
|
||||||
|
self::OPERATOR_EQUALS => sprintf('Service "%s" sub-type equals %s', $this->serviceFieldName, $subTypeDescription),
|
||||||
|
self::OPERATOR_NOT_EQUALS => sprintf('Service "%s" sub-type does not equal %s', $this->serviceFieldName, $subTypeDescription),
|
||||||
|
self::OPERATOR_IN => sprintf('Service "%s" sub-type is in %s', $this->serviceFieldName, $subTypeDescription),
|
||||||
|
self::OPERATOR_NOT_IN => sprintf('Service "%s" sub-type is not in %s', $this->serviceFieldName, $subTypeDescription),
|
||||||
|
default => sprintf('Service "%s" sub-type %s %s', $this->serviceFieldName, $this->operator, $subTypeDescription),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a condition for service sub-type equality.
|
||||||
|
*
|
||||||
|
* @param string $serviceFieldName The service field to check
|
||||||
|
* @param string $expectedSubType The expected service sub-type
|
||||||
|
*/
|
||||||
|
public static function equals(string $serviceFieldName, string $expectedSubType): self
|
||||||
|
{
|
||||||
|
return new self($serviceFieldName, self::OPERATOR_EQUALS, $expectedSubType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a condition for service sub-type non-equality.
|
||||||
|
*
|
||||||
|
* @param string $serviceFieldName The service field to check
|
||||||
|
* @param string $expectedSubType The sub-type that should not match
|
||||||
|
*/
|
||||||
|
public static function notEquals(string $serviceFieldName, string $expectedSubType): self
|
||||||
|
{
|
||||||
|
return new self($serviceFieldName, self::OPERATOR_NOT_EQUALS, $expectedSubType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a condition for service sub-type inclusion.
|
||||||
|
*
|
||||||
|
* @param string $serviceFieldName The service field to check
|
||||||
|
* @param string[] $allowedSubTypes The allowed sub-types
|
||||||
|
*/
|
||||||
|
public static function in(string $serviceFieldName, array $allowedSubTypes): self
|
||||||
|
{
|
||||||
|
return new self($serviceFieldName, self::OPERATOR_IN, $allowedSubTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a condition for service sub-type exclusion.
|
||||||
|
*
|
||||||
|
* @param string $serviceFieldName The service field to check
|
||||||
|
* @param string[] $excludedSubTypes The excluded sub-types
|
||||||
|
*/
|
||||||
|
public static function notIn(string $serviceFieldName, array $excludedSubTypes): self
|
||||||
|
{
|
||||||
|
return new self($serviceFieldName, self::OPERATOR_NOT_IN, $excludedSubTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves service from form data or participant data.
|
||||||
|
*/
|
||||||
|
private function getService(array $formData, int $participantIndex, BookingDtoInterface $bookingDto): ?Service
|
||||||
|
{
|
||||||
|
// First check participant-specific form data
|
||||||
|
if (isset($formData['participants'][$participantIndex][$this->serviceFieldName])) {
|
||||||
|
$serviceData = $formData['participants'][$participantIndex][$this->serviceFieldName];
|
||||||
|
|
||||||
|
// Form data might contain Service objects or service IDs
|
||||||
|
if ($serviceData instanceof Service) {
|
||||||
|
return $serviceData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check participant DTO data
|
||||||
|
$participant = $bookingDto->getParticipant($participantIndex);
|
||||||
|
if (null !== $participant && property_exists($participant, $this->serviceFieldName)) {
|
||||||
|
return $participant->{$this->serviceFieldName};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if sub-type is in the expected array.
|
||||||
|
*/
|
||||||
|
private function isSubTypeIn(string $actualSubType, string|array $expectedSubTypes): bool
|
||||||
|
{
|
||||||
|
if (is_string($expectedSubTypes)) {
|
||||||
|
return $actualSubType === $expectedSubTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array($actualSubType, $expectedSubTypes, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that the operator is supported.
|
||||||
|
*/
|
||||||
|
private function validateOperator(string $operator): void
|
||||||
|
{
|
||||||
|
$validOperators = [
|
||||||
|
self::OPERATOR_EQUALS,
|
||||||
|
self::OPERATOR_NOT_EQUALS,
|
||||||
|
self::OPERATOR_IN,
|
||||||
|
self::OPERATOR_NOT_IN,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!in_array($operator, $validOperators, true)) {
|
||||||
|
throw new \InvalidArgumentException(sprintf('Invalid operator "%s". Valid operators: %s', $operator, implode(', ', $validOperators)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates expected sub-type based on operator requirements.
|
||||||
|
*/
|
||||||
|
private function validateExpectedSubType(string $operator, string|array $expectedSubType): void
|
||||||
|
{
|
||||||
|
if (in_array($operator, [self::OPERATOR_IN, self::OPERATOR_NOT_IN], true) && !is_array($expectedSubType)) {
|
||||||
|
throw new \InvalidArgumentException(sprintf('Operator "%s" requires expectedSubType to be an array', $operator));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($operator, [self::OPERATOR_EQUALS, self::OPERATOR_NOT_EQUALS], true) && !is_string($expectedSubType)) {
|
||||||
|
throw new \InvalidArgumentException(sprintf('Operator "%s" requires expectedSubType to be a string', $operator));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,11 +4,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Form\Service;
|
namespace App\Form\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Utility\DirectionMapper;
|
||||||
use App\Form\Service\Abstract\AbstractFieldStateProvider;
|
use App\Form\Service\Abstract\AbstractFieldStateProvider;
|
||||||
use App\Form\Service\Condition\CompositeCondition;
|
use App\Form\Service\Condition\CompositeCondition;
|
||||||
use App\Form\Service\Condition\DateOfBirthProvidedCondition;
|
use App\Form\Service\Condition\DateOfBirthProvidedCondition;
|
||||||
|
use App\Form\Service\Condition\FieldValueCondition;
|
||||||
use App\Form\Service\Condition\RentalSelectionCondition;
|
use App\Form\Service\Condition\RentalSelectionCondition;
|
||||||
use App\Form\Service\Condition\RoomSelectionCondition;
|
use App\Form\Service\Condition\RoomSelectionCondition;
|
||||||
|
use App\Form\Service\Condition\ServiceSubTypeCondition;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Field state provider for the booking create workflow.
|
* Field state provider for the booking create workflow.
|
||||||
@@ -20,6 +23,8 @@ use App\Form\Service\Condition\RoomSelectionCondition;
|
|||||||
* Current field state conditions:
|
* Current field state conditions:
|
||||||
* - Body dimension fields become required when rental services are selected
|
* - Body dimension fields become required when rental services are selected
|
||||||
* - Age-dependent service fields are hidden until birth date is provided
|
* - 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
|
||||||
*/
|
*/
|
||||||
class CreateFieldStateProvider extends AbstractFieldStateProvider
|
class CreateFieldStateProvider extends AbstractFieldStateProvider
|
||||||
{
|
{
|
||||||
@@ -92,6 +97,24 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
|
|||||||
'hidden' => CompositeCondition::not($mbzRoomCondition),
|
'hidden' => CompositeCondition::not($mbzRoomCondition),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Transportation-related field conditions
|
||||||
|
|
||||||
|
// Hide outbound pickup when transportation is not BUS
|
||||||
|
$this->fieldStateConditions['pickupOutbound'] = [
|
||||||
|
'hidden' => ServiceSubTypeCondition::notEquals('transportationOutbound', DirectionMapper::SUBTYPE_BUS_API),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Hide inbound pickup when transportation is not BUS
|
||||||
|
$this->fieldStateConditions['pickupInbound'] = [
|
||||||
|
'hidden' => ServiceSubTypeCondition::notEquals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Hide parking when outbound transportation is not PKW (car)
|
||||||
|
// Parking is offered at holiday destination for those arriving by car
|
||||||
|
$this->fieldStateConditions['parking'] = [
|
||||||
|
'hidden' => ServiceSubTypeCondition::notEquals('transportationOutbound', DirectionMapper::SUBTYPE_CAR_API),
|
||||||
|
];
|
||||||
|
|
||||||
// Example field state conditions would be registered here
|
// Example field state conditions would be registered here
|
||||||
// For demonstration purposes, here are some example patterns:
|
// For demonstration purposes, here are some example patterns:
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ declare(strict_types=1);
|
|||||||
namespace App\Form\Service;
|
namespace App\Form\Service;
|
||||||
|
|
||||||
use App\BusProNet\Constants;
|
use App\BusProNet\Constants;
|
||||||
|
use App\BusProNet\Model\Pickup;
|
||||||
use App\BusProNet\Model\Service;
|
use App\BusProNet\Model\Service;
|
||||||
|
use App\BusProNet\Utility\DirectionMapper;
|
||||||
use App\Form\Model\BookingCreateDto;
|
use App\Form\Model\BookingCreateDto;
|
||||||
use App\Form\Model\BookingDtoInterface;
|
use App\Form\Model\BookingDtoInterface;
|
||||||
use App\Form\Service\Abstract\AbstractFieldOptionsProvider;
|
use App\Form\Service\Abstract\AbstractFieldOptionsProvider;
|
||||||
@@ -187,6 +189,75 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
'attr' => [
|
||||||
|
'hx-post' => '#', // Will be configured when HTMX integration is implemented
|
||||||
|
'hx-target' => '#booking-summary',
|
||||||
|
'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,
|
||||||
|
'attr' => [
|
||||||
|
'hx-post' => '#', // Will be configured when HTMX integration is implemented
|
||||||
|
'hx-target' => '#booking-summary',
|
||||||
|
'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->pickupsTo,
|
||||||
|
'choice_label' => fn (Pickup $pickup) => $this->formatPickupLabel($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' => 'Zustieg Rückfahrt',
|
||||||
|
'choices' => $bookingDto->travel->pickupsFro,
|
||||||
|
'choice_label' => fn (Pickup $pickup) => $this->formatPickupLabel($pickup),
|
||||||
|
'choice_value' => 'id',
|
||||||
|
'expanded' => false,
|
||||||
|
'multiple' => false,
|
||||||
|
'required' => true,
|
||||||
|
'placeholder' => 'Zustieg auswählen',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Parking (conditional - only shown when at least one transportation direction is PKW)
|
||||||
|
$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,
|
||||||
|
'required' => false,
|
||||||
|
];
|
||||||
|
|
||||||
// Future field providers would be added here, for example:
|
// Future field providers would be added here, for example:
|
||||||
//
|
//
|
||||||
// $this->fieldOptionProviders['mealPreference'] = fn($bookingDto, $participantIndex) => [
|
// $this->fieldOptionProviders['mealPreference'] = fn($bookingDto, $participantIndex) => [
|
||||||
@@ -219,6 +290,73 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
return sprintf('%s (€%s)', $service->label, number_format($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;
|
||||||
|
|
||||||
|
// Add transportation type indicator
|
||||||
|
$typeIndicator = match ($service->subType) {
|
||||||
|
'BUS' => '🚌',
|
||||||
|
'PKW' => '🚗',
|
||||||
|
default => '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($typeIndicator) {
|
||||||
|
$label = $typeIndicator.' '.$label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add pricing with discount indication
|
||||||
|
if (null !== $service->price) {
|
||||||
|
if ($service->price > 0) {
|
||||||
|
$label .= sprintf(' (+€%.2f)', $service->price);
|
||||||
|
} elseif ($service->price < 0) {
|
||||||
|
$label .= sprintf(' (-€%.2f Discount)', abs($service->price));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters services based on participant's age constraints.
|
* Filters services based on participant's age constraints.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler
|
||||||
|
{
|
||||||
|
public function getFieldName(): string
|
||||||
|
{
|
||||||
|
return 'parking';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDependencies(): array
|
||||||
|
{
|
||||||
|
return ['transportationOutbound', 'transportationInbound'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if this handler should process the field based on submitted data.
|
||||||
|
*
|
||||||
|
* For parking selection fields, we always need to process to handle cases
|
||||||
|
* where the parking is cleared due to transportation changes. This ensures the
|
||||||
|
* participant DTO is updated correctly when transportation switches to bus-only.
|
||||||
|
*
|
||||||
|
* @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 parking selection fields
|
||||||
|
*/
|
||||||
|
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
||||||
|
{
|
||||||
|
return true; // Always process for state changes
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||||
|
{
|
||||||
|
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||||
|
if (null === $participant) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if parking is applicable (at least one PKW direction)
|
||||||
|
if (!$this->isParkingApplicable($participant)) {
|
||||||
|
$participant->parking = null; // Clear parking for bus-only transport
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedParking = $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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if parking is applicable based on transportation selections.
|
||||||
|
*
|
||||||
|
* @param object $participant The participant DTO
|
||||||
|
*
|
||||||
|
* @return bool True if parking is applicable, false otherwise
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<?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->pickupsFro);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<?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 outbound pickup location selection.
|
||||||
|
*
|
||||||
|
* Pickup selection is only processed when outbound transportation
|
||||||
|
* is bus type. Automatically clears pickup when transportation
|
||||||
|
* changes to self-organized (PKW).
|
||||||
|
*/
|
||||||
|
class ParticipantPickupOutboundFieldHandler extends AbstractParticipantFieldHandler
|
||||||
|
{
|
||||||
|
public function getFieldName(): string
|
||||||
|
{
|
||||||
|
return 'pickupOutbound';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDependencies(): array
|
||||||
|
{
|
||||||
|
return ['transportationOutbound']; // 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 outbound transportation is bus
|
||||||
|
if (null === $participant->transportationOutbound || DirectionMapper::SUBTYPE_BUS_API !== $participant->transportationOutbound->subType) {
|
||||||
|
$participant->pickupOutbound = null; // Clear pickup for non-bus transport
|
||||||
|
$participant->pickup = null; // Backward compatibility
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedPickup = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||||
|
|
||||||
|
// Validate pickup selection against available outbound pickups
|
||||||
|
$validSelection = null;
|
||||||
|
if (null !== $selectedPickup) {
|
||||||
|
$validSelection = $this->findValidPickup($selectedPickup, $bookingDto->travel->pickupsTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$participant->pickupOutbound = $validSelection;
|
||||||
|
$participant->pickup = $validSelection; // Backward compatibility
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,7 +109,6 @@ class ParticipantSkiPassFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
$participant->skiPass = $validSelection;
|
$participant->skiPass = $validSelection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates if a selected skipass is still valid for the participant.
|
* Validates if a selected skipass is still valid for the participant.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Service;
|
||||||
|
use App\BusProNet\Utility\DirectionMapper;
|
||||||
|
use App\Form\Model\BookingDtoInterface;
|
||||||
|
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles processing of inbound transportation service selection.
|
||||||
|
*
|
||||||
|
* This handler manages inbound (RUECK) transportation options including bus and
|
||||||
|
* self-organized (PKW) services with pricing and availability validation.
|
||||||
|
* It processes the transportationInbound field from form submissions and updates
|
||||||
|
* the participant DTO with validated selections.
|
||||||
|
*/
|
||||||
|
class ParticipantTransportationInboundFieldHandler extends AbstractParticipantFieldHandler
|
||||||
|
{
|
||||||
|
public function getFieldName(): string
|
||||||
|
{
|
||||||
|
return 'transportationInbound';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if this handler should process the field based on submitted data.
|
||||||
|
*
|
||||||
|
* For transportation selection fields, we always need to process to handle cases
|
||||||
|
* where the selection is cleared (field not present in data). This ensures the
|
||||||
|
* participant DTO is updated with null when no transportation is selected.
|
||||||
|
*
|
||||||
|
* @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 transportation selection fields
|
||||||
|
*/
|
||||||
|
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
||||||
|
{
|
||||||
|
return true; // Always process to handle deselection cases
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||||
|
{
|
||||||
|
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||||
|
if (null === $participant) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedTransportation = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||||
|
|
||||||
|
// Get available inbound transportation services
|
||||||
|
$availableServices = $bookingDto->travel->getTransportationServicesByDirection(
|
||||||
|
DirectionMapper::INBOUND_TRAVEL,
|
||||||
|
true // filter available
|
||||||
|
);
|
||||||
|
|
||||||
|
// Validate and convert selection to Service object
|
||||||
|
$validSelection = null;
|
||||||
|
if (null !== $selectedTransportation) {
|
||||||
|
$validSelection = $this->findValidTransportationService(
|
||||||
|
$selectedTransportation,
|
||||||
|
$availableServices
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update participant with validated selection
|
||||||
|
$participant->transportationInbound = $validSelection;
|
||||||
|
|
||||||
|
// Backward compatibility: also update deprecated property
|
||||||
|
$participant->transportationServiceFro = $validSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a valid transportation service from available services.
|
||||||
|
*
|
||||||
|
* @param mixed $selectedServiceId The submitted service ID
|
||||||
|
* @param array<int, Service> $availableServices Array of available transportation services
|
||||||
|
*
|
||||||
|
* @return Service|null The valid service object, or null if invalid
|
||||||
|
*/
|
||||||
|
private function findValidTransportationService(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Service;
|
||||||
|
use App\BusProNet\Utility\DirectionMapper;
|
||||||
|
use App\Form\Model\BookingDtoInterface;
|
||||||
|
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles processing of outbound transportation service selection.
|
||||||
|
*
|
||||||
|
* This handler manages outbound (HIN) transportation options including bus and
|
||||||
|
* self-organized (PKW) services with pricing and availability validation.
|
||||||
|
* It processes the transportationOutbound field from form submissions and updates
|
||||||
|
* the participant DTO with validated selections.
|
||||||
|
*/
|
||||||
|
class ParticipantTransportationOutboundFieldHandler extends AbstractParticipantFieldHandler
|
||||||
|
{
|
||||||
|
public function getFieldName(): string
|
||||||
|
{
|
||||||
|
return 'transportationOutbound';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if this handler should process the field based on submitted data.
|
||||||
|
*
|
||||||
|
* For transportation selection fields, we always need to process to handle cases
|
||||||
|
* where the selection is cleared (field not present in data). This ensures the
|
||||||
|
* participant DTO is updated with null when no transportation is selected.
|
||||||
|
*
|
||||||
|
* @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 transportation selection fields
|
||||||
|
*/
|
||||||
|
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
||||||
|
{
|
||||||
|
return true; // Always process to handle deselection cases
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||||
|
{
|
||||||
|
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||||
|
if (null === $participant) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedTransportation = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||||
|
|
||||||
|
// Get available outbound transportation services
|
||||||
|
$availableServices = $bookingDto->travel->getTransportationServicesByDirection(
|
||||||
|
DirectionMapper::OUTBOUND_TRAVEL,
|
||||||
|
true // filter available
|
||||||
|
);
|
||||||
|
|
||||||
|
// Validate and convert selection to Service object
|
||||||
|
$validSelection = null;
|
||||||
|
if (null !== $selectedTransportation) {
|
||||||
|
$validSelection = $this->findValidTransportationService(
|
||||||
|
$selectedTransportation,
|
||||||
|
$availableServices
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update participant with validated selection
|
||||||
|
$participant->transportationOutbound = $validSelection;
|
||||||
|
|
||||||
|
// Backward compatibility: also update deprecated property
|
||||||
|
$participant->transportationServiceTo = $validSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a valid transportation service from available services.
|
||||||
|
*
|
||||||
|
* @param mixed $selectedServiceId The submitted service ID
|
||||||
|
* @param array<int, Service> $availableServices Array of available transportation services
|
||||||
|
*
|
||||||
|
* @return Service|null The valid service object, or null if invalid
|
||||||
|
*/
|
||||||
|
private function findValidTransportationService(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user