wip: service descriptions and car license plate field

This commit is contained in:
Björn Fromme
2026-03-16 11:59:10 +01:00
parent b84997d058
commit 84f4481ef0
12 changed files with 623 additions and 5 deletions
+3 -1
View File
@@ -180,6 +180,7 @@ class BookingCreateParticipantType extends AbstractType
'pickupOutbound',
'pickupInbound',
'parking',
'licensePlate',
];
foreach ($dynamicFields as $fieldName) {
@@ -211,7 +212,7 @@ class BookingCreateParticipantType extends AbstractType
$this->addBaseFields($form, $bookingDto, $participantIndex);
// Rebuild dynamic fields
$dynamicFields = ['assignedRoomId', 'remarksRoom', 'courses', 'additionalServices', 'board', 'rentals', 'rentalInsurance', 'skiPass', 'transportationOutbound', 'transportationInbound', 'pickupOutbound', 'pickupInbound', 'parking'];
$dynamicFields = ['assignedRoomId', 'remarksRoom', 'courses', 'additionalServices', 'board', 'rentals', 'rentalInsurance', 'skiPass', 'transportationOutbound', 'transportationInbound', 'pickupOutbound', 'pickupInbound', 'parking', 'licensePlate'];
foreach ($dynamicFields as $fieldName) {
if ($form->has($fieldName)) {
$form->remove($fieldName);
@@ -241,6 +242,7 @@ class BookingCreateParticipantType extends AbstractType
'pickupOutbound' => ChoiceType::class,
'pickupInbound' => ChoiceType::class,
'parking' => CheckboxType::class,
'licensePlate' => TextType::class,
];
foreach ($dynamicFields as $fieldName => $fieldType) {
+4 -3
View File
@@ -7,7 +7,6 @@ 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'])]
class ParticipantDto
@@ -51,7 +50,7 @@ class ParticipantDto
public array $board = [];
public array $rentals = [];
public ?Service $rentalInsurance = null;
// Rental insurance checkbox state (boolean: true if rental insurance requested)
public bool $rentalInsuranceSelected = false;
@@ -69,6 +68,9 @@ class ParticipantDto
// Parking service object for pricing calculation (new, contains actual service with pricing)
public ?Service $parkingService = null;
// License plate for participants with parking (optional, visible only when parking is selected)
public ?string $licensePlate = null;
// Deprecated properties for backward compatibility - will be removed in future version
public ?Service $transportationServiceTo = null;
public ?Service $transportationServiceFro = null;
@@ -105,5 +107,4 @@ class ParticipantDto
{
return 'O' === $this->status;
}
}
@@ -118,6 +118,11 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
'hidden' => CompositeCondition::not($rentalCondition),
];
// Show license plate only when parking is selected (hidden by default)
$this->fieldStateConditions['licensePlate'] = [
'hidden' => FieldValueCondition::equals('parking', false),
];
// Example field state conditions would be registered here
// For demonstration purposes, here are some example patterns:
@@ -108,6 +108,20 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
),
'choice_value' => 'id',
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
'choice_attr' => function (?Service $service) {
if (null === $service) {
return [];
}
$attributes = [];
// Add service description as data attribute for frontend use
if (null !== $service->description && '' !== trim($service->description)) {
$attributes['data-description'] = $service->description;
}
return $attributes;
},
];
// Additional services field provider - provides age-appropriate additional services with mandatory pre-selection
@@ -141,6 +155,11 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
$attributes['title'] = 'Diese Leistung ist nicht abwählbar';
}
// Add service description as data attribute for frontend use
if (null !== $service->description && '' !== trim($service->description)) {
$attributes['data-description'] = $service->description;
}
return $attributes;
},
];
@@ -181,6 +200,20 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
),
'choice_value' => 'id',
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
'choice_attr' => function (?Service $service) {
if (null === $service) {
return [];
}
$attributes = [];
// Add service description as data attribute for frontend use
if (null !== $service->description && '' !== trim($service->description)) {
$attributes['data-description'] = $service->description;
}
return $attributes;
},
];
// Rental insurance field provider - provides rental insurance options when rental services are selected
@@ -188,6 +221,19 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
'label' => $this->getRentalInsuranceCheckboxLabel($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTAL_INSURANCE, true, true)),
'required' => false,
'property_path' => 'rentalInsuranceSelected',
'help' => $this->getRentalInsuranceDescription($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTAL_INSURANCE, true, true)),
];
// License plate field provider - provides text input for vehicle license plate when parking is selected
$this->fieldOptionProviders['licensePlate'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
'label' => 'Kennzeichen',
'required' => false,
'attr' => [
'placeholder' => 'z.B. AB-CD 123',
'maxlength' => 20,
],
'help' => 'Bitte gib das Kennzeichen deines Fahrzeugs an. Du kannst es aber auch später nachreichen.',
'clean_xss' => true,
];
// Skipass field provider - provides age-appropriate skipass options from travel data filtered by date range
@@ -207,6 +253,20 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
),
'choice_value' => 'id',
'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service),
'choice_attr' => function (?Service $service) {
if (null === $service) {
return [];
}
$attributes = [];
// Add service description as data attribute for frontend use
if (null !== $service->description && '' !== trim($service->description)) {
$attributes['data-description'] = $service->description;
}
return $attributes;
},
];
// Room remarks field provider - provides textarea for room-specific remarks (only for 'mbz' rooms)
@@ -494,6 +554,20 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
return 'Leihmaterial-Versicherung';
}
$rentalInsuranceService = reset($rentalInsuranceServices); // Get the first (and only) rental insurance service
return $this->formatServiceLabelWithPrice($rentalInsuranceService);
}
/**
* Gets the rental insurance description for help text.
*/
private function getRentalInsuranceDescription(array $rentalInsuranceServices): ?string
{
if (empty($rentalInsuranceServices)) {
return null;
}
$rentalInsuranceService = reset($rentalInsuranceServices); // Get the first (and only) rental insurance service
return $rentalInsuranceService->description;
}
}
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace App\Form\Service;
use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
/**
* Handles processing of the licensePlate field for booking participants.
*
* This handler manages license plate input for participants who have selected
* parking services. The license plate field is only visible when parking is
* selected, creating a dependency chain where license plate depends on parking.
*
* The field accepts any string value without format validation, allowing for
* international license plate formats and variations.
*
* Dependencies: parking (for field visibility)
*/
class ParticipantLicensePlateFieldHandler extends AbstractParticipantFieldHandler
{
/**
* Returns the form field name this handler processes.
*
* @return string The field name 'licensePlate'
*/
public function getFieldName(): string
{
return 'licensePlate';
}
/**
* Returns the field dependencies for proper processing order.
*
* This handler depends on parking because license plate is only relevant
* when parking services are selected.
*
* @return string[] Array containing 'parking' dependency
*/
public function getDependencies(): array
{
return ['parking'];
}
/**
* Determines if this handler should process the field based on submitted data.
*
* For license plate fields, we always need to process to handle cases where
* the license plate should be cleared when parking is deselected. This ensures
* the participant DTO is updated correctly when parking availability changes.
*
* @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 license plate fields
*/
public function shouldProcess(array $submittedData, int $participantIndex): bool
{
return true; // Always process to handle deselection cases
}
/**
* Processes the licensePlate field for a specific participant.
*
* This method extracts the license plate value from submitted form data
* and updates the participant DTO with the value. If parking is not selected,
* the license plate is automatically cleared to maintain data consistency.
*
* @param array<string, mixed> $submittedData The submitted participant form data
* @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit)
* @param int $participantIndex The index of the participant being processed
*/
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
{
// Safely get the participant object, returning early if not found
$participant = $this->getParticipant($bookingDto, $participantIndex);
if (null === $participant) {
return;
}
// Check if parking is selected - license plate only relevant with parking
if (false === $participant->parking) {
// If parking is not selected, clear license plate data
$participant->licensePlate = null;
return;
}
// Extract license plate value from submitted data
$licensePlateValue = $this->getFieldValue($submittedData, $this->getFieldName());
// Store the license plate value (null if empty/not provided)
$participant->licensePlate = $this->sanitizeLicensePlateValue($licensePlateValue);
}
/**
* Sanitizes the license plate value.
*
* Handles basic cleanup of the license plate input by trimming whitespace
* and converting empty strings to null for consistent data handling.
*
* @param mixed $value The raw license plate value from form submission
*
* @return string|null The sanitized license plate value, or null if empty
*/
private function sanitizeLicensePlateValue(mixed $value): ?string
{
if (null === $value || false === is_string($value)) {
return null;
}
$trimmedValue = trim($value);
return '' === $trimmedValue ? null : $trimmedValue;
}
}
@@ -87,11 +87,12 @@ class ParticipantRentalInsuranceFieldHandler extends AbstractParticipantFieldHan
// Check if rental insurance should be visible based on rental selections
$hasRentals = false === empty($participant->rentals);
if (false === $hasRentals) {
// If no rentals are selected, clear rental insurance data
$participant->rentalInsuranceSelected = false;
$participant->rentalInsurance = null;
return;
}