wip: conditional mandatory body dimensions

This commit is contained in:
Björn Fromme
2026-03-16 11:59:09 +01:00
parent 0f7d95d31b
commit f36c761c28
6 changed files with 207 additions and 10 deletions
@@ -1,23 +1,23 @@
import { Controller } from '@hotwired/stimulus' import {Controller} from '@hotwired/stimulus'
export default class extends Controller { export default class extends Controller {
static targets = [ 'fields', 'field', 'addButton', 'index' ] static targets = ['fields', 'field', 'addButton', 'index']
static values = { static values = {
prototype: String, prototype: String,
maxItems: Number, maxItems: Number,
itemsCount: Number, itemsCount: Number,
} }
connect () { connect() {
this.index = this.itemsCountValue = this.fieldTargets.length this.itemsCountValue = this.fieldTargets.length
} }
addItem() { addItem() {
let prototype = JSON.parse(this.prototypeValue) const index = this.fieldTargets.length + 1
const newField = prototype.replace(/__name__/g, this.index) const prototype = JSON.parse(this.prototypeValue)
const newField = prototype.replace(/__name__/g, index)
this.fieldsTarget.insertAdjacentHTML('beforeend', newField) this.fieldsTarget.insertAdjacentHTML('beforeend', newField)
this.index++
this.itemsCountValue++ this.itemsCountValue++
} }
+10 -3
View File
@@ -14,7 +14,7 @@ class BodyDimensionsType extends AbstractType
$builder $builder
->add('height', ChoiceType::class, [ ->add('height', ChoiceType::class, [
'label' => 'Körpergröße', 'label' => 'Körpergröße',
'required' => false, 'required' => $options['height_required'] ?? false,
'expanded' => false, 'expanded' => false,
'multiple' => false, 'multiple' => false,
'placeholder' => 'Keine Angabe', 'placeholder' => 'Keine Angabe',
@@ -29,7 +29,7 @@ class BodyDimensionsType extends AbstractType
]) ])
->add('shoeSize', ChoiceType::class, [ ->add('shoeSize', ChoiceType::class, [
'label' => 'Schuhgröße', 'label' => 'Schuhgröße',
'required' => false, 'required' => $options['shoeSize_required'] ?? false,
'expanded' => false, 'expanded' => false,
'multiple' => false, 'multiple' => false,
'placeholder' => 'Keine Angabe', 'placeholder' => 'Keine Angabe',
@@ -37,7 +37,7 @@ class BodyDimensionsType extends AbstractType
]) ])
->add('weight', ChoiceType::class, [ ->add('weight', ChoiceType::class, [
'label' => 'Gewicht', 'label' => 'Gewicht',
'required' => false, 'required' => $options['weight_required'] ?? false,
'expanded' => false, 'expanded' => false,
'multiple' => false, 'multiple' => false,
'placeholder' => 'Keine Angabe', 'placeholder' => 'Keine Angabe',
@@ -56,6 +56,13 @@ class BodyDimensionsType extends AbstractType
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'inherit_data' => true, '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');
} }
} }
+42
View File
@@ -142,7 +142,17 @@ class BookingCreateParticipantType extends AbstractType
// Get all fields that have state conditions // Get all fields that have state conditions
$allFieldStates = $this->fieldStateProvider->getAllFieldStates($bookingDto, $participantIndex, $formData); $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) { 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)) { if ($form->has($fieldName)) {
$field = $form->get($fieldName); $field = $form->get($fieldName);
$currentOptions = $field->getConfig()->getOptions(); $currentOptions = $field->getConfig()->getOptions();
@@ -156,6 +166,38 @@ class BookingCreateParticipantType extends AbstractType
$form->add($fieldName, $fieldType::class, $updatedOptions); $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<string, array<string, mixed>> $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);
} }
/** /**
+43
View File
@@ -7,8 +7,10 @@ use App\BusProNet\Model\Pickup;
use App\BusProNet\Model\Service; use App\BusProNet\Model\Service;
use App\Validator\Constraints as AppAssert; use App\Validator\Constraints as AppAssert;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[AppAssert\Participant(groups: ['booking_edit'])] #[AppAssert\Participant(groups: ['booking_edit'])]
#[Assert\Callback('validateBodyDimensionsForRentals', groups: ['booking_create_step_2', 'booking_edit'])]
class ParticipantDto class ParticipantDto
{ {
public ?int $index = null; public ?int $index = null;
@@ -25,6 +27,7 @@ class ParticipantDto
public ?string $title = null; public ?string $title = null;
public ?string $gender = null; public ?string $gender = null;
public ?string $nationality = null; public ?string $nationality = null;
public ?string $height = null; public ?string $height = null;
public ?string $shoeSize = null; public ?string $shoeSize = null;
public ?string $weight = null; public ?string $weight = null;
@@ -81,4 +84,44 @@ class ParticipantDto
{ {
return 'O' === $this->status; 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();
}
}
} }
@@ -0,0 +1,89 @@
<?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 whether rental services are selected for a participant.
*
* This condition checks if a participant has selected any rental services from the
* available travel services. When rental services are selected, body dimension
* fields (height, weight, shoeSize) should become mandatory for equipment sizing.
*
* The condition examines the participant's rentals array to determine if any
* services with rental subtypes have been selected, triggering mandatory
* body dimension requirements.
*/
class RentalSelectionCondition implements FieldConditionInterface
{
/**
* Evaluates whether the participant has selected any rental services.
*
* Checks if the participant's rentals array contains any Service objects,
* which would indicate rental services have been selected and body
* dimensions should be required for proper equipment sizing.
*
* @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 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';
}
}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Form\Service; namespace App\Form\Service;
use App\Form\Service\Abstract\AbstractFieldStateProvider; use App\Form\Service\Abstract\AbstractFieldStateProvider;
use App\Form\Service\Condition\RentalSelectionCondition;
/** /**
* Field state provider for the booking create workflow. * Field state provider for the booking create workflow.
@@ -44,6 +45,21 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
*/ */
protected function registerFieldStateConditions(): void 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 // Example field state conditions would be registered here
// For demonstration purposes, here are some example patterns: // For demonstration purposes, here are some example patterns: