From b992e4a5ea87d94b2b4c2ff60b894256ebd37fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Fri, 25 Jul 2025 21:47:00 +0200 Subject: [PATCH] wip: conditional mandatory body dimensions --- .../controllers/form_collection_controller.js | 14 +-- src/Form/BodyDimensionsType.php | 13 ++- src/Form/BookingCreateParticipantType.php | 42 +++++++++ src/Form/Model/ParticipantDto.php | 43 +++++++++ .../Condition/RentalSelectionCondition.php | 89 +++++++++++++++++++ src/Form/Service/CreateFieldStateProvider.php | 16 ++++ 6 files changed, 207 insertions(+), 10 deletions(-) create mode 100644 src/Form/Service/Condition/RentalSelectionCondition.php diff --git a/assets/controllers/form_collection_controller.js b/assets/controllers/form_collection_controller.js index e9e5530..2de5acd 100644 --- a/assets/controllers/form_collection_controller.js +++ b/assets/controllers/form_collection_controller.js @@ -1,23 +1,23 @@ -import { Controller } from '@hotwired/stimulus' +import {Controller} from '@hotwired/stimulus' export default class extends Controller { - static targets = [ 'fields', 'field', 'addButton', 'index' ] + static targets = ['fields', 'field', 'addButton', 'index'] static values = { prototype: String, maxItems: Number, itemsCount: Number, } - connect () { - this.index = this.itemsCountValue = this.fieldTargets.length + connect() { + this.itemsCountValue = this.fieldTargets.length } addItem() { - let prototype = JSON.parse(this.prototypeValue) - const newField = prototype.replace(/__name__/g, this.index) + const index = this.fieldTargets.length + 1 + const prototype = JSON.parse(this.prototypeValue) + const newField = prototype.replace(/__name__/g, index) this.fieldsTarget.insertAdjacentHTML('beforeend', newField) - this.index++ this.itemsCountValue++ } diff --git a/src/Form/BodyDimensionsType.php b/src/Form/BodyDimensionsType.php index cf54a65..ef85ac3 100644 --- a/src/Form/BodyDimensionsType.php +++ b/src/Form/BodyDimensionsType.php @@ -14,7 +14,7 @@ class BodyDimensionsType extends AbstractType $builder ->add('height', ChoiceType::class, [ 'label' => 'Körpergröße', - 'required' => false, + 'required' => $options['height_required'] ?? false, 'expanded' => false, 'multiple' => false, 'placeholder' => 'Keine Angabe', @@ -29,7 +29,7 @@ class BodyDimensionsType extends AbstractType ]) ->add('shoeSize', ChoiceType::class, [ 'label' => 'Schuhgröße', - 'required' => false, + 'required' => $options['shoeSize_required'] ?? false, 'expanded' => false, 'multiple' => false, 'placeholder' => 'Keine Angabe', @@ -37,7 +37,7 @@ class BodyDimensionsType extends AbstractType ]) ->add('weight', ChoiceType::class, [ 'label' => 'Gewicht', - 'required' => false, + 'required' => $options['weight_required'] ?? false, 'expanded' => false, 'multiple' => false, 'placeholder' => 'Keine Angabe', @@ -56,6 +56,13 @@ class BodyDimensionsType extends AbstractType { $resolver->setDefaults([ 'inherit_data' => true, + 'height_required' => false, + 'weight_required' => false, + 'shoeSize_required' => false, ]); + + $resolver->setAllowedTypes('height_required', 'bool'); + $resolver->setAllowedTypes('weight_required', 'bool'); + $resolver->setAllowedTypes('shoeSize_required', 'bool'); } } diff --git a/src/Form/BookingCreateParticipantType.php b/src/Form/BookingCreateParticipantType.php index 5b39deb..86b26e2 100644 --- a/src/Form/BookingCreateParticipantType.php +++ b/src/Form/BookingCreateParticipantType.php @@ -142,7 +142,17 @@ class BookingCreateParticipantType extends AbstractType // Get all fields that have state conditions $allFieldStates = $this->fieldStateProvider->getAllFieldStates($bookingDto, $participantIndex, $formData); + // Handle body dimension fields separately as they are nested in bodyDimensions form + $bodyDimensionFields = ['height', 'weight', 'shoeSize']; + $bodyDimensionStates = []; + 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(); @@ -156,6 +166,38 @@ class BookingCreateParticipantType extends AbstractType $form->add($fieldName, $fieldType::class, $updatedOptions); } } + + // Apply body dimension field states to the nested bodyDimensions form + if (!empty($bodyDimensionStates) && $form->has('bodyDimensions')) { + $this->applyBodyDimensionStates($form, $bodyDimensionStates); + } + } + + /** + * 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> $bodyDimensionStates Field states for body dimension fields + */ + private function applyBodyDimensionStates(FormInterface $form, array $bodyDimensionStates): void + { + $bodyDimensionsField = $form->get('bodyDimensions'); + $currentOptions = $bodyDimensionsField->getConfig()->getOptions(); + + // Convert field states to BodyDimensionsType options + foreach ($bodyDimensionStates as $fieldName => $fieldState) { + if (isset($fieldState['required']) && true === $fieldState['required']) { + $currentOptions[$fieldName.'_required'] = true; + } + } + + // Remove and re-add the bodyDimensions field with updated options + $form->remove('bodyDimensions'); + $form->add('bodyDimensions', BodyDimensionsType::class, $currentOptions); } /** diff --git a/src/Form/Model/ParticipantDto.php b/src/Form/Model/ParticipantDto.php index ce1813e..0bc0f11 100644 --- a/src/Form/Model/ParticipantDto.php +++ b/src/Form/Model/ParticipantDto.php @@ -7,8 +7,10 @@ use App\BusProNet\Model\Pickup; use App\BusProNet\Model\Service; use App\Validator\Constraints as AppAssert; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Context\ExecutionContextInterface; #[AppAssert\Participant(groups: ['booking_edit'])] +#[Assert\Callback('validateBodyDimensionsForRentals', groups: ['booking_create_step_2', 'booking_edit'])] class ParticipantDto { public ?int $index = null; @@ -25,6 +27,7 @@ class ParticipantDto public ?string $title = null; public ?string $gender = null; public ?string $nationality = null; + public ?string $height = null; public ?string $shoeSize = null; public ?string $weight = null; @@ -81,4 +84,44 @@ class ParticipantDto { return 'O' === $this->status; } + + /** + * Validates that body dimension fields are provided when rental services are selected. + * + * This callback validator ensures that height, weight, and shoe size are mandatory + * when the participant has selected any rental services. This is required for + * proper equipment sizing and rental fulfillment. + * + * @param ExecutionContextInterface $context The validation context + */ + public function validateBodyDimensionsForRentals(ExecutionContextInterface $context): void + { + $rentals = $this->rentals ?? []; + + // If no rental services are selected, body dimensions are not required + if (empty($rentals)) { + return; + } + + // Validate height field + if (true === empty($this->height)) { + $context->buildViolation('Deine Körpergröße ist erforderlich wenn Leihmaterial ausgewählt wurde') + ->atPath('height') + ->addViolation(); + } + + // Validate weight field + if (true === empty($this->weight)) { + $context->buildViolation('Dein Gewicht ist erforderlich wenn Leihmaterial ausgewählt wurde') + ->atPath('weight') + ->addViolation(); + } + + // Validate shoe size field + if (true === empty($this->shoeSize)) { + $context->buildViolation('Deine Schuhgröße ist erforderlich wenn Leihmaterial ausgewählt wurde') + ->atPath('shoeSize') + ->addViolation(); + } + } } diff --git a/src/Form/Service/Condition/RentalSelectionCondition.php b/src/Form/Service/Condition/RentalSelectionCondition.php new file mode 100644 index 0000000..b54b836 --- /dev/null +++ b/src/Form/Service/Condition/RentalSelectionCondition.php @@ -0,0 +1,89 @@ + $formData Current form data for condition evaluation + * + * @return bool True if rental services are selected, false otherwise + */ + public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool + { + // First check submitted form data for rental selections + if (isset($formData['participants'][$participantIndex]['rentals'])) { + $selectedRentals = $formData['participants'][$participantIndex]['rentals']; + if (true === is_array($selectedRentals) && false === empty($selectedRentals)) { + return true; + } + } + + // Then check participant DTO data for existing rental selections + $participant = $bookingDto->getParticipant($participantIndex); + if (null !== $participant) { + $rentals = $participant->rentals; + if (false === empty($rentals)) { + // Check if any rental services are actually selected + foreach ($rentals as $rental) { + if ($rental instanceof Service) { + return true; + } + } + } + } + + return false; + } + + /** + * Returns field names that trigger re-evaluation of this condition. + * + * This condition depends on the rentals field, so any changes to rental + * selections should trigger re-evaluation of body dimension field states. + * + * @return string[] Array containing the field names that affect this condition + */ + public function getDependentFields(): array + { + return ['rentals']; + } + + /** + * Returns a human-readable description of this condition. + * + * Provides a clear description of the condition logic for debugging, + * logging, and developer documentation purposes. + * + * @return string A brief description of the condition logic + */ + public function getDescription(): string + { + return 'Body dimensions required when rental services are selected'; + } +} diff --git a/src/Form/Service/CreateFieldStateProvider.php b/src/Form/Service/CreateFieldStateProvider.php index f19746e..42efd5f 100644 --- a/src/Form/Service/CreateFieldStateProvider.php +++ b/src/Form/Service/CreateFieldStateProvider.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Form\Service; use App\Form\Service\Abstract\AbstractFieldStateProvider; +use App\Form\Service\Condition\RentalSelectionCondition; /** * Field state provider for the booking create workflow. @@ -44,6 +45,21 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider */ protected function registerFieldStateConditions(): void { + $rentalCondition = new RentalSelectionCondition(); + + // Make body dimension fields required when rental services are selected + $this->fieldStateConditions['height'] = [ + 'required' => $rentalCondition, + ]; + + $this->fieldStateConditions['weight'] = [ + 'required' => $rentalCondition, + ]; + + $this->fieldStateConditions['shoeSize'] = [ + 'required' => $rentalCondition, + ]; + // Example field state conditions would be registered here // For demonstration purposes, here are some example patterns: