wip: show pricing
This commit is contained in:
@@ -70,6 +70,7 @@ class CreateStep1Controller extends AbstractController
|
||||
'bookingCreateDto' => $bookingCreateDto,
|
||||
'roomSummary' => $summary['selectedRooms'],
|
||||
'participantCount' => $summary['participantCount'],
|
||||
'pricingData' => $summary['pricing'],
|
||||
'form' => $form->createView(),
|
||||
'groupedRooms' => $groupedRooms,
|
||||
'groupedSelectedRooms' => $groupedSelectedRooms,
|
||||
@@ -98,6 +99,7 @@ class CreateStep1Controller extends AbstractController
|
||||
'bookingCreateDto' => $bookingCreateDto,
|
||||
'roomSummary' => $summary['selectedRooms'],
|
||||
'participantCount' => $summary['participantCount'],
|
||||
'pricingData' => $summary['pricing'],
|
||||
'groupedSelectedRooms' => $groupedSelectedRooms,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ class CreateStep2Controller extends AbstractController
|
||||
return $this->redirectToRoute('app_booking_create_step_3');
|
||||
}
|
||||
|
||||
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto);
|
||||
$roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($bookingCreateDto);
|
||||
$availableRooms = $bookingCreateDto->travel->getAvailableRooms();
|
||||
$groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($bookingCreateDto->getSelectedRooms(), $availableRooms);
|
||||
@@ -69,6 +70,7 @@ class CreateStep2Controller extends AbstractController
|
||||
return $this->render('booking/create_step_2.html.twig', [
|
||||
'bookingCreateDto' => $bookingCreateDto,
|
||||
'participantsCount' => $participantsCount,
|
||||
'pricingData' => $summary['pricing'],
|
||||
'assignmentCounts' => $roomAssignmentCounts,
|
||||
'form' => $form->createView(),
|
||||
'groupedSelectedRooms' => $groupedSelectedRooms,
|
||||
@@ -94,6 +96,7 @@ class CreateStep2Controller extends AbstractController
|
||||
$form->handleRequest($request);
|
||||
$this->bookingService->saveBookingCreateDto($request, $bookingCreateDto);
|
||||
|
||||
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto);
|
||||
$roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($bookingCreateDto);
|
||||
$availableRooms = $bookingCreateDto->travel->getAvailableRooms();
|
||||
$groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($bookingCreateDto->getSelectedRooms(), $availableRooms);
|
||||
@@ -107,6 +110,7 @@ class CreateStep2Controller extends AbstractController
|
||||
'form' => $form->createView(),
|
||||
'bookingCreateDto' => $bookingCreateDto,
|
||||
'participantsCount' => $participantsCount,
|
||||
'pricingData' => $summary['pricing'],
|
||||
'assignmentCounts' => $roomAssignmentCounts,
|
||||
'groupedSelectedRooms' => $groupedSelectedRooms,
|
||||
]
|
||||
|
||||
@@ -30,54 +30,18 @@ class BookingCreateParticipantType extends AbstractType
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('firstName', TextType::class, [
|
||||
'label' => 'Vorname',
|
||||
'clean_xss' => true,
|
||||
])
|
||||
->add('lastName', TextType::class, [
|
||||
'label' => 'Nachname',
|
||||
'clean_xss' => true,
|
||||
])
|
||||
->add('dateOfBirth', BirthdayType::class, [
|
||||
'label' => 'Geburtsdatum',
|
||||
'html5' => true,
|
||||
'widget' => 'single_text',
|
||||
'input' => 'datetime_immutable',
|
||||
])
|
||||
->add('gender', ChoiceType::class, [
|
||||
'label' => 'Geschlecht',
|
||||
'required' => false,
|
||||
'placeholder' => 'keine Angabe',
|
||||
'choices' => [
|
||||
'männlich' => 'M',
|
||||
'weiblich' => 'W',
|
||||
'divers' => 'D',
|
||||
],
|
||||
])
|
||||
->add('nationality', CountryType::class, [
|
||||
'label' => 'Nationalität',
|
||||
'property' => 'nationality',
|
||||
'preferred_choices' => ['D', 'A', 'CH'],
|
||||
])
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'E-Mail',
|
||||
'required' => false,
|
||||
'clean_xss' => true,
|
||||
])
|
||||
->add('mobile', TextType::class, [
|
||||
'label' => 'Telefon (mobil)',
|
||||
'required' => false,
|
||||
'clean_xss' => true,
|
||||
])
|
||||
->add('bodyDimensions', BodyDimensionsType::class)
|
||||
->addEventListener(FormEvents::PRE_SET_DATA, [$this, 'onPreSetData'])
|
||||
->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'onPreSubmit']);
|
||||
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
|
||||
$this->onPreSetData($event);
|
||||
})
|
||||
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
|
||||
$this->onPreSubmit($event);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds dynamic fields to the form based on participant data.
|
||||
*/
|
||||
public function onPreSetData(FormEvent $event): void
|
||||
private function onPreSetData(FormEvent $event): void
|
||||
{
|
||||
/** @var ParticipantDto|null $participantData */
|
||||
$participantData = $event->getData();
|
||||
@@ -94,14 +58,17 @@ class BookingCreateParticipantType extends AbstractType
|
||||
return;
|
||||
}
|
||||
|
||||
// Add base fields with states applied
|
||||
$this->addBaseFields($form, $bookingDto, $participantData->index);
|
||||
|
||||
// Add dynamic fields
|
||||
$this->addDynamicFields($form, $bookingDto, $participantData->index);
|
||||
$this->applyFieldStates($form, $bookingDto, $participantData->index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles form pre-submit events to update field states based on submitted data.
|
||||
*/
|
||||
public function onPreSubmit(FormEvent $event): void
|
||||
private function onPreSubmit(FormEvent $event): void
|
||||
{
|
||||
$submittedData = $event->getData();
|
||||
$form = $event->getForm();
|
||||
@@ -123,59 +90,71 @@ class BookingCreateParticipantType extends AbstractType
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply updated field states based on submitted data
|
||||
$this->applyFieldStates($form, $bookingDto, $participantData->index, $submittedData);
|
||||
// Rebuild all fields with updated states based on submitted data
|
||||
$this->rebuildFieldsWithStates($form, $bookingDto, $participantData->index, $submittedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies dynamic field states to all form fields.
|
||||
*
|
||||
* This method checks each form field for state conditions and applies
|
||||
* the appropriate state modifications (readonly, disabled, etc.).
|
||||
* Fields that should be hidden are removed from the form entirely.
|
||||
*
|
||||
* @param FormInterface $form The form to modify
|
||||
* @param BookingDtoInterface $bookingDto The booking data for context (create or edit)
|
||||
* @param int $participantIndex The participant index
|
||||
* @param array<string, mixed> $formData Optional submitted form data for state calculation
|
||||
* Adds base fields to the form with field states applied.
|
||||
*/
|
||||
private function applyFieldStates(FormInterface $form, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): void
|
||||
private function addBaseFields(FormInterface $form, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
{
|
||||
// First, remove fields that should be excluded entirely
|
||||
$this->removeExcludedFields($form, $bookingDto, $participantIndex, $formData);
|
||||
// Get field states for base fields
|
||||
$allFieldStates = $this->fieldStateProvider->getAllFieldStates($bookingDto, $participantIndex);
|
||||
|
||||
// Get all fields that have state conditions (excluding hidden fields)
|
||||
$allFieldStates = $this->fieldStateProvider->getAllFieldStates($bookingDto, $participantIndex, $formData);
|
||||
// Helper to get field state or empty array
|
||||
$getFieldState = fn (string $fieldName) => $allFieldStates[$fieldName] ?? [];
|
||||
|
||||
// Handle body dimension fields separately as they are nested in bodyDimensions form
|
||||
$bodyDimensionFields = ['height', 'weight', 'shoeSize'];
|
||||
$bodyDimensionStates = [];
|
||||
$form
|
||||
->add('firstName', TextType::class, $this->mergeFieldState([
|
||||
'label' => 'Vorname',
|
||||
'clean_xss' => true,
|
||||
], $getFieldState('firstName')))
|
||||
->add('lastName', TextType::class, $this->mergeFieldState([
|
||||
'label' => 'Nachname',
|
||||
'clean_xss' => true,
|
||||
], $getFieldState('lastName')))
|
||||
->add('dateOfBirth', BirthdayType::class, $this->mergeFieldState([
|
||||
'label' => 'Geburtsdatum',
|
||||
'html5' => true,
|
||||
'widget' => 'single_text',
|
||||
'input' => 'datetime_immutable',
|
||||
], $getFieldState('dateOfBirth')))
|
||||
->add('gender', ChoiceType::class, $this->mergeFieldState([
|
||||
'label' => 'Geschlecht',
|
||||
'required' => false,
|
||||
'placeholder' => 'keine Angabe',
|
||||
'choices' => [
|
||||
'männlich' => 'M',
|
||||
'weiblich' => 'W',
|
||||
'divers' => 'D',
|
||||
],
|
||||
], $getFieldState('gender')))
|
||||
->add('nationality', CountryType::class, $this->mergeFieldState([
|
||||
'label' => 'Nationalität',
|
||||
'property' => 'nationality',
|
||||
'preferred_choices' => ['D', 'A', 'CH'],
|
||||
], $getFieldState('nationality')))
|
||||
->add('email', EmailType::class, $this->mergeFieldState([
|
||||
'label' => 'E-Mail',
|
||||
'required' => false,
|
||||
'clean_xss' => true,
|
||||
], $getFieldState('email')))
|
||||
->add('mobile', TextType::class, $this->mergeFieldState([
|
||||
'label' => 'Telefon (mobil)',
|
||||
'required' => false,
|
||||
'clean_xss' => true,
|
||||
], $getFieldState('mobile')));
|
||||
|
||||
foreach ($allFieldStates as $fieldName => $fieldState) {
|
||||
if (in_array($fieldName, $bodyDimensionFields, true)) {
|
||||
// Collect body dimension field states for later processing
|
||||
$bodyDimensionStates[$fieldName] = $fieldState;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($form->has($fieldName)) {
|
||||
$field = $form->get($fieldName);
|
||||
$currentOptions = $field->getConfig()->getOptions();
|
||||
|
||||
// Merge state into current options
|
||||
$updatedOptions = $this->mergeFieldState($currentOptions, $fieldState);
|
||||
|
||||
// Remove and re-add the field with updated options
|
||||
$fieldType = $field->getConfig()->getType()->getInnerType();
|
||||
$form->remove($fieldName);
|
||||
$form->add($fieldName, $fieldType::class, $updatedOptions);
|
||||
// Add body dimensions with state handling
|
||||
$bodyDimensionStates = ['height' => $getFieldState('height'), 'weight' => $getFieldState('weight'), 'shoeSize' => $getFieldState('shoeSize')];
|
||||
$bodyDimensionOptions = [];
|
||||
foreach ($bodyDimensionStates as $fieldName => $fieldState) {
|
||||
if (isset($fieldState['required']) && true === $fieldState['required']) {
|
||||
$bodyDimensionOptions[$fieldName.'_required'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply body dimension field states to the nested bodyDimensions form
|
||||
if (!empty($bodyDimensionStates) && $form->has('bodyDimensions')) {
|
||||
$this->applyBodyDimensionStates($form, $bodyDimensionStates);
|
||||
}
|
||||
$form->add('bodyDimensions', BodyDimensionsType::class, $bodyDimensionOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,30 +188,36 @@ class BookingCreateParticipantType extends AbstractType
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies field states to body dimension fields in the nested bodyDimensions form.
|
||||
*
|
||||
* This method handles the special case of body dimension fields which are embedded
|
||||
* in a nested form type. It converts field state requirements into options that
|
||||
* can be passed to the BodyDimensionsType.
|
||||
*
|
||||
* @param FormInterface $form The parent form containing bodyDimensions
|
||||
* @param array<string, array<string, mixed>> $bodyDimensionStates Field states for body dimension fields
|
||||
* Rebuilds all fields with updated states based on submitted data.
|
||||
*/
|
||||
private function applyBodyDimensionStates(FormInterface $form, array $bodyDimensionStates): void
|
||||
private function rebuildFieldsWithStates(FormInterface $form, BookingDtoInterface $bookingDto, int $participantIndex, array $submittedData): void
|
||||
{
|
||||
$bodyDimensionsField = $form->get('bodyDimensions');
|
||||
$currentOptions = $bodyDimensionsField->getConfig()->getOptions();
|
||||
// First, remove fields that should be excluded entirely
|
||||
$this->removeExcludedFields($form, $bookingDto, $participantIndex, $submittedData);
|
||||
|
||||
// Convert field states to BodyDimensionsType options
|
||||
foreach ($bodyDimensionStates as $fieldName => $fieldState) {
|
||||
if (isset($fieldState['required']) && true === $fieldState['required']) {
|
||||
$currentOptions[$fieldName.'_required'] = true;
|
||||
// Clear the form and rebuild from scratch with updated states
|
||||
|
||||
// Rebuild base fields with updated states
|
||||
$baseFields = ['firstName', 'lastName', 'dateOfBirth', 'gender', 'nationality', 'email', 'mobile'];
|
||||
foreach ($baseFields as $fieldName) {
|
||||
if ($form->has($fieldName)) {
|
||||
$form->remove($fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove and re-add the bodyDimensions field with updated options
|
||||
$form->remove('bodyDimensions');
|
||||
$form->add('bodyDimensions', BodyDimensionsType::class, $currentOptions);
|
||||
// Re-add base fields with updated states
|
||||
$this->addBaseFields($form, $bookingDto, $participantIndex);
|
||||
|
||||
// Rebuild dynamic fields
|
||||
$dynamicFields = ['assignedRoomId', 'remarksRoom', 'courses', 'additionalServices', 'board', 'rentals', 'skiPass'];
|
||||
foreach ($dynamicFields as $fieldName) {
|
||||
if ($form->has($fieldName)) {
|
||||
$form->remove($fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-add dynamic fields
|
||||
$this->addDynamicFields($form, $bookingDto, $participantIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,7 +246,7 @@ class BookingCreateParticipantType extends AbstractType
|
||||
$fieldOptions = $this->fieldOptionsProvider->getFieldOptions($fieldName, $bookingDto, $participantIndex);
|
||||
|
||||
// Skip choice fields without any choices
|
||||
if ($fieldType === ChoiceType::class && false === $this->hasValidFieldOptions($fieldOptions)) {
|
||||
if (ChoiceType::class === $fieldType && false === $this->hasValidFieldOptions($fieldOptions)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class BookingCreateStep2Type extends AbstractType
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private readonly ParticipantFieldHandlerRegistry $participantFieldHandlerRegistry,
|
||||
) {
|
||||
|
||||
@@ -20,9 +20,8 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class BookingEditParticipantType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EditFieldStateProvider $fieldStateProvider,
|
||||
) {
|
||||
public function __construct(private readonly EditFieldStateProvider $fieldStateProvider)
|
||||
{
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
@@ -299,14 +298,24 @@ class BookingEditParticipantType extends AbstractType
|
||||
$personalDataFields = [
|
||||
'firstName', 'lastName', 'dateOfBirth', 'gender', 'nationality', 'email', 'mobile',
|
||||
];
|
||||
// Re-apply field states by rebuilding fields with updated state
|
||||
// Note: We need to store the field types and rebuild them since we cannot modify existing field configs
|
||||
$fieldDefinitions = [
|
||||
'firstName' => [TextType::class, ['label' => 'Vorname', 'clean_xss' => true]],
|
||||
'lastName' => [TextType::class, ['label' => 'Nachname', 'clean_xss' => true]],
|
||||
'dateOfBirth' => [BirthdayType::class, ['label' => 'Geburtsdatum', 'html5' => true, 'widget' => 'single_text', 'input' => 'datetime_immutable']],
|
||||
'gender' => [ChoiceType::class, ['label' => 'Geschlecht', 'required' => false, 'placeholder' => 'keine Angabe', 'choices' => ['männlich' => 'M', 'weiblich' => 'W', 'divers' => 'D']]],
|
||||
'nationality' => [CountryType::class, ['label' => 'Nationalität', 'property' => 'nationality', 'preferred_choices' => ['D', 'A', 'CH']]],
|
||||
'email' => [EmailType::class, ['label' => 'E-Mail', 'required' => false, 'clean_xss' => true]],
|
||||
'mobile' => [TextType::class, ['label' => 'Telefon (mobil)', 'required' => false, 'clean_xss' => true]],
|
||||
];
|
||||
|
||||
foreach ($personalDataFields as $field) {
|
||||
if ($form->has($field)) {
|
||||
$fieldConfig = $form->get($field)->getConfig();
|
||||
$currentOptions = $fieldConfig->getOptions();
|
||||
$updatedOptions = $this->mergeFieldState($currentOptions, $getState($field));
|
||||
$fieldType = $fieldConfig->getType()->getInnerType();
|
||||
if ($form->has($field) && isset($fieldDefinitions[$field])) {
|
||||
[$fieldType, $baseOptions] = $fieldDefinitions[$field];
|
||||
$updatedOptions = $this->mergeFieldState($baseOptions, $getState($field));
|
||||
$form->remove($field);
|
||||
$form->add($field, $fieldType::class, $updatedOptions);
|
||||
$form->add($field, $fieldType, $updatedOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,19 @@ class RoomSelectType extends AbstractType
|
||||
{
|
||||
$builder
|
||||
->add('roomId', HiddenType::class)
|
||||
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
|
||||
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
|
||||
$data = $event->getData();
|
||||
$form = $event->getForm();
|
||||
|
||||
// Build label with pricing
|
||||
$label = 'Anzahl ' . $data->roomLabel;
|
||||
if (null !== $options['room_price']) {
|
||||
$formattedPrice = number_format((float) $options['room_price'], 2, ',', '.');
|
||||
$label .= sprintf(' (€%s pro Nacht)', $formattedPrice);
|
||||
}
|
||||
|
||||
$form->add('quantity', ChoiceType::class, [
|
||||
'label' => 'Anzahl '.$data->roomLabel,
|
||||
'label' => $label,
|
||||
'required' => true,
|
||||
'choices' => ['-' => 0] + array_combine(range(1, $data->maxQuantity), range(1, $data->maxQuantity)),
|
||||
]);
|
||||
@@ -33,6 +40,7 @@ class RoomSelectType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => RoomSelectionDto::class,
|
||||
'room_price' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +39,11 @@ abstract class AbstractFieldOptionsProvider implements FieldOptionsProviderInter
|
||||
* @param string $fieldName The name of the field to configure
|
||||
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
|
||||
* @param int $participantIndex The index of the participant being configured
|
||||
* @param array $options Additional options to customize field behavior
|
||||
*
|
||||
* @return array<string, mixed> Symfony form field options, or empty array if field not supported
|
||||
*/
|
||||
public function getFieldOptions(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex): array
|
||||
public function getFieldOptions(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex, array $options = []): array
|
||||
{
|
||||
// Check if we have a provider for this field
|
||||
if (false === isset($this->fieldOptionProviders[$fieldName])) {
|
||||
@@ -50,7 +51,7 @@ abstract class AbstractFieldOptionsProvider implements FieldOptionsProviderInter
|
||||
}
|
||||
|
||||
// Execute the provider with current context to generate dynamic options
|
||||
return $this->fieldOptionProviders[$fieldName]($bookingDto, $participantIndex);
|
||||
return $this->fieldOptionProviders[$fieldName]($bookingDto, $participantIndex, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,10 +44,11 @@ interface FieldOptionsProviderInterface
|
||||
* @param string $fieldName The name of the field to configure
|
||||
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
|
||||
* @param int $participantIndex The index of the participant being configured
|
||||
* @param array $options Additional options to customize field behavior
|
||||
*
|
||||
* @return array<string, mixed> Symfony form field options, or empty array if field not supported
|
||||
*/
|
||||
public function getFieldOptions(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex): array;
|
||||
public function getFieldOptions(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex, array $options = []): array;
|
||||
|
||||
/**
|
||||
* Checks whether a field has option provider support.
|
||||
|
||||
@@ -137,7 +137,11 @@ class ParticipantAdditionalServicesFieldHandler extends AbstractParticipantField
|
||||
|
||||
foreach ($selectedServices as $selectedService) {
|
||||
if ($this->isServiceValidForParticipant($selectedService, $availableServices, $bookingDto, $participantIndex)) {
|
||||
$validSelections[] = $selectedService;
|
||||
// Convert the selected service ID/data back to the actual Service object
|
||||
$serviceObject = $this->findServiceInAvailableServices($selectedService, $availableServices);
|
||||
if (null !== $serviceObject) {
|
||||
$validSelections[] = $serviceObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,11 @@ class ParticipantBoardFieldHandler extends AbstractParticipantFieldHandler
|
||||
|
||||
foreach ($selectedServices as $selectedService) {
|
||||
if ($this->isServiceValidForParticipant($selectedService, $availableServices, $bookingDto, $participantIndex)) {
|
||||
$validSelections[] = $selectedService;
|
||||
// Convert the selected service ID/data back to the actual Service object
|
||||
$serviceObject = $this->findServiceInAvailableServices($selectedService, $availableServices);
|
||||
if (null !== $serviceObject) {
|
||||
$validSelections[] = $serviceObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,11 @@ class ParticipantCoursesFieldHandler extends AbstractParticipantFieldHandler
|
||||
|
||||
foreach ($selectedServices as $selectedService) {
|
||||
if ($this->isServiceValidForParticipant($selectedService, $availableServices, $bookingDto, $participantIndex)) {
|
||||
$validSelections[] = $selectedService;
|
||||
// Convert the selected service ID/data back to the actual Service object
|
||||
$serviceObject = $this->findServiceInAvailableServices($selectedService, $availableServices);
|
||||
if (null !== $serviceObject) {
|
||||
$validSelections[] = $serviceObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Service\Contract\ParticipantFieldHandlerInterface;
|
||||
|
||||
/**
|
||||
@@ -17,7 +18,7 @@ use App\Form\Service\Contract\ParticipantFieldHandlerInterface;
|
||||
*/
|
||||
class ParticipantFieldHandlerRegistry
|
||||
{
|
||||
/** @var ParticipantFieldHandlerInterface[] Registered handlers indexed by field name */
|
||||
/** @var array<string, ParticipantFieldHandlerInterface> Registered handlers indexed by field name */
|
||||
private array $handlers = [];
|
||||
|
||||
/** @var string[]|null Cached array of handler names sorted by dependency order */
|
||||
@@ -274,11 +275,11 @@ class ParticipantFieldHandlerRegistry
|
||||
* been processed by checking against registered handlers.
|
||||
*
|
||||
* @param array<string, mixed> $participantData The submitted participant data
|
||||
* @param object $participant The cleaned participant DTO
|
||||
* @param ParticipantDto $participant The cleaned participant DTO
|
||||
*
|
||||
* @return array<string, mixed> Updated participant data with synchronized field values
|
||||
*/
|
||||
private function syncParticipantData(array $participantData, object $participant): array
|
||||
private function syncParticipantData(array $participantData, ParticipantDto $participant): array
|
||||
{
|
||||
// Sync fields for all registered handlers
|
||||
foreach ($this->handlers as $fieldName => $handler) {
|
||||
|
||||
@@ -69,7 +69,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
protected function registerFieldOptionProviders(): void
|
||||
{
|
||||
// Room assignment field provider (only available for create workflow)
|
||||
$this->fieldOptionProviders['assignedRoomId'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
|
||||
$this->fieldOptionProviders['assignedRoomId'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Zimmer',
|
||||
'placeholder' => 'Bitte wählen',
|
||||
// Use factory to create context-aware choice loader that:
|
||||
@@ -86,7 +86,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
];
|
||||
|
||||
// Courses field provider - provides age-appropriate courses from travel data
|
||||
$this->fieldOptionProviders['courses'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
|
||||
$this->fieldOptionProviders['courses'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Kurse',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
@@ -97,11 +97,11 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
$participantIndex
|
||||
),
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => 'label',
|
||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
||||
];
|
||||
|
||||
// Additional services field provider - provides age-appropriate additional services with mandatory pre-selection
|
||||
$this->fieldOptionProviders['additionalServices'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
|
||||
$this->fieldOptionProviders['additionalServices'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Zusatzleistungen',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
@@ -112,7 +112,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
$participantIndex
|
||||
),
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => 'label',
|
||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
||||
'choice_attr' => function (?Service $service) {
|
||||
if (null === $service) {
|
||||
return [];
|
||||
@@ -133,7 +133,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
];
|
||||
|
||||
// Board field provider - provides age-appropriate board options from travel data
|
||||
$this->fieldOptionProviders['board'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
|
||||
$this->fieldOptionProviders['board'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Verpflegung',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
@@ -144,11 +144,11 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
$participantIndex
|
||||
),
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => 'label',
|
||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
||||
];
|
||||
|
||||
// Rentals field provider - provides age-appropriate rental options from travel data filtered by date range
|
||||
$this->fieldOptionProviders['rentals'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
|
||||
$this->fieldOptionProviders['rentals'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Leihmaterial',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
@@ -159,11 +159,11 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
$participantIndex
|
||||
),
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => 'label',
|
||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
||||
];
|
||||
|
||||
// Skipass field provider - provides age-appropriate skipass options from travel data filtered by date range
|
||||
$this->fieldOptionProviders['skiPass'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
|
||||
$this->fieldOptionProviders['skiPass'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Skipass',
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
@@ -174,11 +174,11 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
$participantIndex
|
||||
),
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => 'label',
|
||||
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
|
||||
];
|
||||
|
||||
// Room remarks field provider - provides textarea for room-specific remarks (only for 'mbz' rooms)
|
||||
$this->fieldOptionProviders['remarksRoom'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
|
||||
$this->fieldOptionProviders['remarksRoom'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Wünsche oder Anmerkungen zum Zimmer',
|
||||
'required' => false,
|
||||
'clean_xss' => true,
|
||||
@@ -199,6 +199,26 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
// ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats service label with pricing information.
|
||||
*
|
||||
* @param Service|null $service The service to format
|
||||
*
|
||||
* @return string The formatted label
|
||||
*/
|
||||
private function formatServiceLabelWithPrice(?Service $service): string
|
||||
{
|
||||
if (null === $service) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (null === $service->price || 0.0 === $service->price) {
|
||||
return $service->label;
|
||||
}
|
||||
|
||||
return sprintf('%s (€%s)', $service->label, number_format($service->price, 2, ',', '.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters services based on participant's age constraints.
|
||||
*
|
||||
|
||||
@@ -79,7 +79,11 @@ class ParticipantRentalsFieldHandler extends AbstractParticipantFieldHandler
|
||||
|
||||
foreach ($selectedServices as $selectedService) {
|
||||
if ($this->isServiceValidForParticipant($selectedService, $availableServices, $bookingDto, $participantIndex)) {
|
||||
$validSelections[] = $selectedService;
|
||||
// Convert the selected service ID/data back to the actual Service object
|
||||
$serviceObject = $this->findServiceInAvailableServices($selectedService, $availableServices);
|
||||
if (null !== $serviceObject) {
|
||||
$validSelections[] = $serviceObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Room;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
|
||||
/**
|
||||
* Calculates pricing for booking components including rooms and services.
|
||||
*
|
||||
* This service provides comprehensive pricing calculations for the booking system,
|
||||
* handling room pricing based on quantities and service pricing per participant.
|
||||
* It returns structured pricing data for display in forms and summaries.
|
||||
*/
|
||||
class BookingPriceCalculatorService
|
||||
{
|
||||
/**
|
||||
* Calculates comprehensive pricing breakdown for a booking.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking data to calculate pricing for
|
||||
*
|
||||
* @return array{rooms: array, services: array, grandTotal: float} Complete pricing breakdown
|
||||
*/
|
||||
public function getPricingBreakdown(BookingDtoInterface $bookingDto): array
|
||||
{
|
||||
$roomPricing = $this->calculateRoomPricing($bookingDto);
|
||||
$servicePricing = $this->calculateServicePricing($bookingDto);
|
||||
$grandTotal = $this->calculateGrandTotal($bookingDto);
|
||||
|
||||
return [
|
||||
'rooms' => $roomPricing,
|
||||
'services' => $servicePricing,
|
||||
'grandTotal' => $grandTotal,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates pricing for all selected rooms.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking data containing room selections
|
||||
*
|
||||
* @return array Array of room pricing data with labels, quantities, and totals
|
||||
*/
|
||||
public function calculateRoomPricing(BookingDtoInterface $bookingDto): array
|
||||
{
|
||||
$roomPricing = [];
|
||||
|
||||
if (false === $bookingDto instanceof BookingCreateDto) {
|
||||
return $roomPricing;
|
||||
}
|
||||
|
||||
$selectedRooms = $bookingDto->getSelectedRooms();
|
||||
if (true === empty($selectedRooms)) {
|
||||
return $roomPricing;
|
||||
}
|
||||
|
||||
foreach ($selectedRooms as $roomSelection) {
|
||||
$room = $this->getRoomById($bookingDto, $roomSelection->roomId);
|
||||
if (null === $room || null === $room->price) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$totalPrice = $roomSelection->quantity * $room->price;
|
||||
|
||||
$roomPricing[] = [
|
||||
'roomId' => $room->id,
|
||||
'label' => $room->label,
|
||||
'quantity' => $roomSelection->quantity,
|
||||
'unitPrice' => $room->price,
|
||||
'totalPrice' => $totalPrice,
|
||||
];
|
||||
}
|
||||
|
||||
return $roomPricing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates pricing for all selected services across all participants, grouped by subtype.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking data containing participants and their service selections
|
||||
*
|
||||
* @return array Array of service groups with each group containing services of the same subtype
|
||||
*/
|
||||
public function calculateServicePricing(BookingDtoInterface $bookingDto): array
|
||||
{
|
||||
$participants = $bookingDto->getParticipants();
|
||||
|
||||
if (true === empty($participants)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Aggregate service selections across all participants
|
||||
$serviceAggregation = [];
|
||||
|
||||
foreach ($participants as $participant) {
|
||||
$this->aggregateParticipantServices($participant, $serviceAggregation);
|
||||
}
|
||||
|
||||
// Group services by subtype and convert to pricing format
|
||||
return $this->groupServicesBySubtype($serviceAggregation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the grand total for the entire booking.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking data to calculate total for
|
||||
*
|
||||
* @return float The grand total price
|
||||
*/
|
||||
public function calculateGrandTotal(BookingDtoInterface $bookingDto): float
|
||||
{
|
||||
$roomTotal = $this->calculateRoomTotal($bookingDto);
|
||||
$serviceTotal = $this->calculateServiceTotal($bookingDto);
|
||||
|
||||
return $roomTotal + $serviceTotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates total price for all rooms.
|
||||
*/
|
||||
public function calculateRoomTotal(BookingDtoInterface $bookingDto): float
|
||||
{
|
||||
$roomPricing = $this->calculateRoomPricing($bookingDto);
|
||||
|
||||
return array_sum(array_column($roomPricing, 'totalPrice'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates total price for all services.
|
||||
*/
|
||||
public function calculateServiceTotal(BookingDtoInterface $bookingDto): float
|
||||
{
|
||||
$servicePricing = $this->calculateServicePricing($bookingDto);
|
||||
|
||||
return array_sum(array_column($servicePricing, 'groupTotal'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a price value for display with proper German formatting.
|
||||
*
|
||||
* @param float $price The price to format
|
||||
*
|
||||
* @return string Formatted price string (e.g., "123,45")
|
||||
*/
|
||||
public function formatPrice(float $price): string
|
||||
{
|
||||
return number_format($price, 2, ',', '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a price with Euro symbol for display.
|
||||
*
|
||||
* @param float $price The price to format
|
||||
*
|
||||
* @return string Formatted price string with Euro symbol (e.g., "€123,45")
|
||||
*/
|
||||
public function formatPriceWithSymbol(float $price): string
|
||||
{
|
||||
return '€'.$this->formatPrice($price);
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups services by their subtypes for display.
|
||||
*
|
||||
* @param array $serviceAggregation Aggregated service data
|
||||
*
|
||||
* @return array Grouped services by subtype
|
||||
*/
|
||||
private function groupServicesBySubtype(array $serviceAggregation): array
|
||||
{
|
||||
$groupedServices = [];
|
||||
|
||||
foreach ($serviceAggregation as $serviceData) {
|
||||
if ($serviceData['totalPrice'] <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$subType = $serviceData['subType'] ?? 'other';
|
||||
$groupName = $this->getGroupNameForSubtype($subType);
|
||||
|
||||
if (false === isset($groupedServices[$groupName])) {
|
||||
$groupedServices[$groupName] = [
|
||||
'groupName' => $groupName,
|
||||
'services' => [],
|
||||
'groupTotal' => 0.0,
|
||||
];
|
||||
}
|
||||
|
||||
$groupedServices[$groupName]['services'][] = $serviceData;
|
||||
$groupedServices[$groupName]['groupTotal'] += $serviceData['totalPrice'];
|
||||
}
|
||||
|
||||
return array_values($groupedServices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps service subtypes to user-friendly group names.
|
||||
*/
|
||||
private function getGroupNameForSubtype(string $subType): string
|
||||
{
|
||||
$groupMapping = [
|
||||
Constants::TOKEN_COURSES => 'Kurse',
|
||||
Constants::TOKEN_SKI_PASS => 'Skipässe',
|
||||
Constants::TOKEN_ADDITIONAL => 'Zusatzleistungen',
|
||||
Constants::TOKEN_BOARD => 'Verpflegung',
|
||||
];
|
||||
|
||||
// Handle rentals array
|
||||
if (true === in_array($subType, Constants::TOKEN_RENTALS, true)) {
|
||||
return 'Leihmaterial';
|
||||
}
|
||||
|
||||
return $groupMapping[$subType] ?? 'Sonstige Leistungen';
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates service selections from a single participant into the service aggregation array.
|
||||
*/
|
||||
private function aggregateParticipantServices(ParticipantDto $participant, array &$serviceAggregation): void
|
||||
{
|
||||
// Handle single service selections (skiPass)
|
||||
if (null !== $participant->skiPass && null !== $participant->skiPass->price) {
|
||||
$this->addToServiceAggregation($serviceAggregation, $participant->skiPass, 1);
|
||||
}
|
||||
|
||||
// Handle multiple service selections
|
||||
$multipleServiceArrays = [
|
||||
'courses' => $participant->courses,
|
||||
'additionalServices' => $participant->additionalServices,
|
||||
'board' => $participant->board,
|
||||
'rentals' => $participant->rentals,
|
||||
];
|
||||
|
||||
foreach ($multipleServiceArrays as $serviceArray) {
|
||||
if (true === is_array($serviceArray)) {
|
||||
foreach ($serviceArray as $service) {
|
||||
if ($service instanceof Service && null !== $service->price) {
|
||||
$this->addToServiceAggregation($serviceAggregation, $service, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a service to the aggregation array, incrementing count and updating total price.
|
||||
*/
|
||||
private function addToServiceAggregation(array &$serviceAggregation, Service $service, int $quantity): void
|
||||
{
|
||||
$serviceKey = $service->id.'_'.$service->label;
|
||||
|
||||
if (false === isset($serviceAggregation[$serviceKey])) {
|
||||
$serviceAggregation[$serviceKey] = [
|
||||
'serviceId' => $service->id,
|
||||
'label' => $service->label,
|
||||
'unitPrice' => $service->price,
|
||||
'participantCount' => 0,
|
||||
'totalPrice' => 0.0,
|
||||
'subType' => $service->subType,
|
||||
];
|
||||
}
|
||||
|
||||
$serviceAggregation[$serviceKey]['participantCount'] += $quantity;
|
||||
$serviceAggregation[$serviceKey]['totalPrice'] += $service->price * $quantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a room by ID from the booking's travel data.
|
||||
*/
|
||||
private function getRoomById(BookingCreateDto $bookingDto, ?int $roomId): ?Room
|
||||
{
|
||||
if (null === $roomId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($bookingDto->travel->rooms as $room) {
|
||||
if ($room->id === $roomId) {
|
||||
return $room;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ class BookingService
|
||||
|
||||
public function __construct(
|
||||
private readonly TravelDataService $travelDataService,
|
||||
private readonly BookingPriceCalculatorService $priceCalculator,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -156,18 +157,20 @@ class BookingService
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a summary of selected rooms and the resulting participant count for a booking.
|
||||
* Returns a summary of selected rooms, participant count, and pricing information for a booking.
|
||||
*
|
||||
* @return array{selectedRooms: array, participantCount: int}
|
||||
* @return array{selectedRooms: array, participantCount: int, pricing: array}
|
||||
*/
|
||||
public function getRoomSummaryAndParticipantCount(BookingCreateDto $bookingCreateDto): array
|
||||
{
|
||||
$selectedRooms = $bookingCreateDto->getSelectedRooms();
|
||||
$participantCount = $this->getParticipantsCount($selectedRooms, $bookingCreateDto->travel);
|
||||
$pricing = $this->priceCalculator->getPricingBreakdown($bookingCreateDto);
|
||||
|
||||
return [
|
||||
'selectedRooms' => $selectedRooms,
|
||||
'participantCount' => $participantCount,
|
||||
'pricing' => $pricing,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user