wip: additional services with constraints
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(php -l:*)"
|
"Bash(php -l:*)",
|
||||||
|
"Bash(./vendor/bin/phpunit --testdox)",
|
||||||
|
"Bash(./vendor/bin/php-cs-fixer fix:*)",
|
||||||
|
"Bash(./vendor/bin/php-cs-fixer fix:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ class BookingDataProcessor
|
|||||||
$payload['zusatzleistungen']['zusatzleistung'][] = [
|
$payload['zusatzleistungen']['zusatzleistung'][] = [
|
||||||
'@idleistung' => $service->id,
|
'@idleistung' => $service->id,
|
||||||
'@anzahl' => count($service->mapping),
|
'@anzahl' => count($service->mapping),
|
||||||
'@zuordnung' => implode(',', array_map(fn($index) => $index + 1, $service->mapping)),
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $service->mapping)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@ class BookingDataProcessor
|
|||||||
$payload['beförderungen']['beförderung'][] = [
|
$payload['beförderungen']['beförderung'][] = [
|
||||||
'@idleistung' => $service->id,
|
'@idleistung' => $service->id,
|
||||||
'@anzahl' => count($service->mapping),
|
'@anzahl' => count($service->mapping),
|
||||||
'@zuordnung' => implode(',', array_map(fn($index) => $index + 1, $service->mapping)),
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $service->mapping)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +378,7 @@ class BookingDataProcessor
|
|||||||
'@anreise' => $room->dateFrom ? $room->dateFrom->format('d.m.Y') : null,
|
'@anreise' => $room->dateFrom ? $room->dateFrom->format('d.m.Y') : null,
|
||||||
'@abreise' => $room->dateTo ? $room->dateTo->format('d.m.Y') : null,
|
'@abreise' => $room->dateTo ? $room->dateTo->format('d.m.Y') : null,
|
||||||
'@anzahl' => $room->totalCount,
|
'@anzahl' => $room->totalCount,
|
||||||
'@zuordnung' => implode(',', array_map(fn($index) => $index + 1, $room->mapping)),
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $room->mapping)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -399,7 +399,7 @@ class BookingDataProcessor
|
|||||||
$payload['zustiege']['zustieg'][] = [
|
$payload['zustiege']['zustieg'][] = [
|
||||||
'@idzustieg' => $pickup->id,
|
'@idzustieg' => $pickup->id,
|
||||||
'@anzahl' => count($pickup->mapping),
|
'@anzahl' => count($pickup->mapping),
|
||||||
'@zuordnung' => implode(',', array_map(fn($index) => $index + 1, $pickup->mapping)),
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $pickup->mapping)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class Room
|
|||||||
if (1 === preg_match('/bett/i', $this->label)) {
|
if (1 === preg_match('/bett/i', $this->label)) {
|
||||||
return self::SELECTION_TYPE_BY_PAX;
|
return self::SELECTION_TYPE_BY_PAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::SELECTION_TYPE_BY_ROOM;
|
return self::SELECTION_TYPE_BY_ROOM;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,23 +87,46 @@ class Travel
|
|||||||
public ?Guide $guide = null;
|
public ?Guide $guide = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves additional services filtered by group and availability.
|
* Retrieves additional services filtered by subtype, availability, and optionally by travel date range.
|
||||||
*
|
*
|
||||||
* Filters additional services based on the provided group(s) and optionally
|
* Filters additional services based on the provided subtype(s), availability,
|
||||||
* by availability. Services are sorted alphabetically by label.
|
* and optionally whether their date range overlaps with the travel dates.
|
||||||
|
* Services with null dates are considered always available when date filtering is enabled.
|
||||||
|
* Services are sorted alphabetically by label.
|
||||||
*
|
*
|
||||||
* @param mixed $group The service group(s) to filter by
|
* @param mixed $subTypes The service subtype(s) to filter by
|
||||||
* @param bool $availableOnly Whether to include only available services
|
* @param bool $filterAvailable Whether to include only available services
|
||||||
|
* @param bool $filterByTravelDateRange Whether to filter by travel date range overlap
|
||||||
*
|
*
|
||||||
* @return array<int, Service> The filtered and sorted services array
|
* @return array<int, Service> The filtered and sorted services array
|
||||||
*/
|
*/
|
||||||
public function getAdditionalServicesByGroup(mixed $group, bool $availableOnly = true): array
|
public function getAdditionalServicesBySubTypes(mixed $subTypes, bool $filterAvailable = true, bool $filterByTravelDateRange = false): array
|
||||||
{
|
{
|
||||||
$group = (array) $group;
|
$subTypes = (array) $subTypes;
|
||||||
|
|
||||||
$services = array_filter($this->additionalServices, function (Service $service) use ($group, $availableOnly) {
|
$services = array_filter($this->additionalServices, function (Service $service) use ($subTypes, $filterAvailable, $filterByTravelDateRange) {
|
||||||
return true === in_array($service->subType, $group)
|
// Check subtype
|
||||||
&& (false === $availableOnly || 0 < $service->available || null === $service->available);
|
if (false === in_array($service->subType, $subTypes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check availability
|
||||||
|
if (true === $filterAvailable && null !== $service->available && 0 >= $service->available) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check date range overlap if filtering by travel dates is enabled
|
||||||
|
if (true === $filterByTravelDateRange) {
|
||||||
|
if (null !== $service->dateFrom && null !== $service->dateTo
|
||||||
|
&& null !== $this->dateFrom && null !== $this->dateTo) {
|
||||||
|
// Service is available if its date range overlaps with travel dates:
|
||||||
|
// service.dateFrom <= travel.dateTo AND service.dateTo >= travel.dateFrom
|
||||||
|
return $service->dateFrom <= $this->dateTo && $service->dateTo >= $this->dateFrom;
|
||||||
|
}
|
||||||
|
// Service is available if it has no date constraints or travel has no dates
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
usort($services, function (Service $a, Service $b) {
|
usort($services, function (Service $a, Service $b) {
|
||||||
@@ -120,16 +143,16 @@ class Travel
|
|||||||
* by availability. Services are sorted by subtype.
|
* by availability. Services are sorted by subtype.
|
||||||
*
|
*
|
||||||
* @param string $direction The travel direction to filter by
|
* @param string $direction The travel direction to filter by
|
||||||
* @param bool $availableOnly Whether to include only available services
|
* @param bool $filterAvailable Whether to include only available services
|
||||||
*
|
*
|
||||||
* @return array<int, Service> The filtered and sorted transportation services
|
* @return array<int, Service> The filtered and sorted transportation services
|
||||||
*/
|
*/
|
||||||
public function getTransportationServicesByDirection(string $direction, bool $availableOnly = true): array
|
public function getTransportationServicesByDirection(string $direction, bool $filterAvailable = true): array
|
||||||
{
|
{
|
||||||
$services = array_filter($this->transportationServices, function (Service $service) use ($direction, $availableOnly) {
|
$services = array_filter($this->transportationServices, function (Service $service) use ($direction, $filterAvailable) {
|
||||||
return $direction === $service->direction
|
return $direction === $service->direction
|
||||||
&& $service->price >= 0
|
&& $service->price >= 0
|
||||||
&& (false === $availableOnly || $service->available > 0 || null === $service->available);
|
&& (false === $filterAvailable || $service->available > 0 || null === $service->available);
|
||||||
});
|
});
|
||||||
|
|
||||||
usort($services, function (Service $a, Service $b) {
|
usort($services, function (Service $a, Service $b) {
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ class BookingCreateParticipantType extends AbstractType
|
|||||||
*/
|
*/
|
||||||
private function addDynamicFields(FormInterface $form, BookingDtoInterface $bookingDto, int $participantIndex): void
|
private function addDynamicFields(FormInterface $form, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||||
{
|
{
|
||||||
$dynamicFields = ['assignedRoomId', 'courses']; // List of fields that need dynamic configuration
|
$dynamicFields = ['assignedRoomId', 'courses', 'additionalServices', 'board', 'rentals']; // List of fields that need dynamic configuration
|
||||||
|
|
||||||
foreach ($dynamicFields as $fieldName) {
|
foreach ($dynamicFields as $fieldName) {
|
||||||
if ($this->fieldOptionsProvider->hasFieldOptions($fieldName)) {
|
if ($this->fieldOptionsProvider->hasFieldOptions($fieldName)) {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class BookingEditType extends AbstractType
|
|||||||
{
|
{
|
||||||
// combine selectable services from travel data with additional services
|
// combine selectable services from travel data with additional services
|
||||||
// from booking data
|
// from booking data
|
||||||
$selectableServices = $data->travel->getAdditionalServicesByGroup($subType);
|
$selectableServices = $data->travel->getAdditionalServicesBySubTypes($subType);
|
||||||
$selectableServiceIds = array_map(function (Service $service) {
|
$selectableServiceIds = array_map(function (Service $service) {
|
||||||
return $service->id;
|
return $service->id;
|
||||||
}, $selectableServices);
|
}, $selectableServices);
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ class ParticipantRoomChoiceLoader implements ChoiceLoaderInterface
|
|||||||
--$adjustedOccupancy;
|
--$adjustedOccupancy;
|
||||||
}
|
}
|
||||||
|
|
||||||
$totalAvailableRooms = $roomSelection->quantity;
|
$totalCapacity = $roomSelection->quantity * $roomSelection->capacity;
|
||||||
$remainingCapacity = $totalAvailableRooms - $adjustedOccupancy;
|
$remainingCapacity = $totalCapacity - $adjustedOccupancy;
|
||||||
|
|
||||||
// Include room if it has capacity OR if it's the participant's current assignment
|
// Include room if it has capacity OR if it's the participant's current assignment
|
||||||
if ($remainingCapacity > 0 || $assignedRoomId === $roomSelection->roomId) {
|
if ($remainingCapacity > 0 || $assignedRoomId === $roomSelection->roomId) {
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ class RoomSelectionDto
|
|||||||
|
|
||||||
public int $maxQuantity = 100;
|
public int $maxQuantity = 100;
|
||||||
|
|
||||||
public int $minPax = 0;
|
public int $capacity = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ abstract class AbstractParticipantFieldHandler implements ParticipantFieldHandle
|
|||||||
protected function getParticipant(BookingDtoInterface $bookingDto, int $participantIndex): ?object
|
protected function getParticipant(BookingDtoInterface $bookingDto, int $participantIndex): ?object
|
||||||
{
|
{
|
||||||
$participants = $bookingDto->getParticipants();
|
$participants = $bookingDto->getParticipants();
|
||||||
|
|
||||||
return $participants[$participantIndex] ?? null;
|
return $participants[$participantIndex] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class ApplicantCondition implements FieldConditionInterface
|
|||||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||||
{
|
{
|
||||||
// Default: applicant is the first participant (index 0)
|
// Default: applicant is the first participant (index 0)
|
||||||
return $participantIndex === 0;
|
return 0 === $participantIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDependentFields(): array
|
public function getDependentFields(): array
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Form\Service;
|
namespace App\Form\Service;
|
||||||
|
|
||||||
|
use App\Form\Service\Abstract\AbstractFieldStateProvider;
|
||||||
use App\Form\Service\Condition\ApplicantCondition;
|
use App\Form\Service\Condition\ApplicantCondition;
|
||||||
use App\Form\Service\Condition\CompositeCondition;
|
use App\Form\Service\Condition\CompositeCondition;
|
||||||
use App\Form\Service\Condition\MutabilityCondition;
|
use App\Form\Service\Condition\MutabilityCondition;
|
||||||
use App\Form\Service\Abstract\AbstractFieldStateProvider;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Field state provider for the booking edit workflow.
|
* Field state provider for the booking edit workflow.
|
||||||
|
|||||||
@@ -92,15 +92,56 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
'multiple' => true,
|
'multiple' => true,
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'choices' => array_reduce(
|
'choices' => $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES),
|
||||||
$bookingDto->travel->getAdditionalServicesByGroup(Constants::TOKEN_COURSES),
|
'choice_label' => 'label',
|
||||||
function (array $choices, Service $service) {
|
];
|
||||||
$choices[$service->label] = $service->id;
|
|
||||||
|
|
||||||
return $choices;
|
// Additional services field provider - provides additional services with mandatory pre-selection
|
||||||
|
$this->fieldOptionProviders['additionalServices'] = fn (BookingDtoInterface $bookingDto) => [
|
||||||
|
'label' => 'Zusatzleistungen',
|
||||||
|
'multiple' => true,
|
||||||
|
'expanded' => true,
|
||||||
|
'required' => false,
|
||||||
|
'choices' => $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL),
|
||||||
|
'choice_value' => 'id',
|
||||||
|
'choice_label' => 'label',
|
||||||
|
'choice_attr' => function (?Service $service) {
|
||||||
|
if (null === $service) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$attributes = [];
|
||||||
|
|
||||||
|
// Pre-select and make readonly for mandatory services
|
||||||
|
if (true === $service->mandatory) {
|
||||||
|
$attributes['checked'] = true;
|
||||||
|
$attributes['readonly'] = true;
|
||||||
|
$attributes['class'] = 'text-pink';
|
||||||
|
$attributes['title'] = 'Diese Leistung ist nicht abwählbar';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $attributes;
|
||||||
},
|
},
|
||||||
[]
|
];
|
||||||
),
|
|
||||||
|
// Board field provider - provides available board options from travel data
|
||||||
|
$this->fieldOptionProviders['board'] = fn (BookingDtoInterface $bookingDto) => [
|
||||||
|
'label' => 'Verpflegung',
|
||||||
|
'multiple' => true,
|
||||||
|
'expanded' => true,
|
||||||
|
'required' => false,
|
||||||
|
'choices' => $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD),
|
||||||
|
'choice_label' => 'label',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Rentals field provider - provides available rental options from travel data filtered by date range
|
||||||
|
$this->fieldOptionProviders['rentals'] = fn (BookingDtoInterface $bookingDto) => [
|
||||||
|
'label' => 'Leihmaterial',
|
||||||
|
'multiple' => true,
|
||||||
|
'expanded' => true,
|
||||||
|
'required' => false,
|
||||||
|
'choices' => $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true, true),
|
||||||
|
'choice_label' => 'label',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Future field providers would be added here, for example:
|
// Future field providers would be added here, for example:
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class BookingService
|
|||||||
$selection->roomId = $room->id;
|
$selection->roomId = $room->id;
|
||||||
$selection->roomLabel = $room->label;
|
$selection->roomLabel = $room->label;
|
||||||
$selection->maxQuantity = $room->available;
|
$selection->maxQuantity = $room->available;
|
||||||
$selection->minPax = $room->minPax;
|
$selection->capacity = $room->minPax;
|
||||||
$selection->quantity = $roomsIdsAndQuantities[$room->id] ?? 0;
|
$selection->quantity = $roomsIdsAndQuantities[$room->id] ?? 0;
|
||||||
|
|
||||||
return $selection;
|
return $selection;
|
||||||
|
|||||||
@@ -53,6 +53,15 @@
|
|||||||
{% if participant.courses is defined %}
|
{% if participant.courses is defined %}
|
||||||
{{ form_row(participant.courses) }}
|
{{ form_row(participant.courses) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if participant.additionalServices is defined %}
|
||||||
|
{{ form_row(participant.additionalServices) }}
|
||||||
|
{% endif %}
|
||||||
|
{% if participant.rentals is defined %}
|
||||||
|
{{ form_row(participant.rentals) }}
|
||||||
|
{% endif %}
|
||||||
|
{% if participant.board is defined %}
|
||||||
|
{{ form_row(participant.board) }}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -153,9 +153,9 @@ class TravelDataServiceTest extends TestCase
|
|||||||
$dateId => [
|
$dateId => [
|
||||||
'id' => $dateId,
|
'id' => $dateId,
|
||||||
'hotels' => [
|
'hotels' => [
|
||||||
$hotelId => ['id' => $hotelId, 'name' => 'Test Hotel']
|
$hotelId => ['id' => $hotelId, 'name' => 'Test Hotel'],
|
||||||
]
|
],
|
||||||
]
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->travelLoader
|
$this->travelLoader
|
||||||
@@ -191,8 +191,8 @@ class TravelDataServiceTest extends TestCase
|
|||||||
$mapping = [
|
$mapping = [
|
||||||
$dateId => [
|
$dateId => [
|
||||||
'id' => $dateId,
|
'id' => $dateId,
|
||||||
'hotels' => []
|
'hotels' => [],
|
||||||
]
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->travelLoader
|
$this->travelLoader
|
||||||
@@ -214,9 +214,9 @@ class TravelDataServiceTest extends TestCase
|
|||||||
$dateId => [
|
$dateId => [
|
||||||
'id' => $dateId,
|
'id' => $dateId,
|
||||||
'hotels' => [
|
'hotels' => [
|
||||||
$hotelId => ['id' => $hotelId, 'name' => 'Test Hotel']
|
$hotelId => ['id' => $hotelId, 'name' => 'Test Hotel'],
|
||||||
]
|
],
|
||||||
]
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->travelLoader
|
$this->travelLoader
|
||||||
@@ -256,7 +256,7 @@ class TravelDataServiceTest extends TestCase
|
|||||||
->expects($this->once())
|
->expects($this->once())
|
||||||
->method('get')
|
->method('get')
|
||||||
->with('travel_unified_12345_67890_local')
|
->with('travel_unified_12345_67890_local')
|
||||||
->willReturnCallback(function (string $key, callable $callback) use ($cacheItem, $travel) {
|
->willReturnCallback(function (string $key, callable $callback) use ($cacheItem) {
|
||||||
return $callback($cacheItem);
|
return $callback($cacheItem);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user