From ba0eab9e96e9af383e15d13fe7ddd4dd21c18b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Fri, 24 Oct 2025 17:30:41 +0200 Subject: [PATCH] fix: adapt changes in create mode for edit mode, cleanup --- .../Booking/Edit/IndexController.php | 19 +++- src/Form/BookingParticipantType.php | 2 +- ...AuthenticatedUserPersonalDataCondition.php | 13 ++- src/Form/Service/EditFieldStateProvider.php | 31 +++++- templates/booking/_participant_form.html.twig | 96 ++++++------------- 5 files changed, 82 insertions(+), 79 deletions(-) diff --git a/src/Controller/Booking/Edit/IndexController.php b/src/Controller/Booking/Edit/IndexController.php index 5fd0d73..b7a6632 100644 --- a/src/Controller/Booking/Edit/IndexController.php +++ b/src/Controller/Booking/Edit/IndexController.php @@ -15,6 +15,7 @@ use App\Entity\User; use App\Form\BookingEditType; use App\Form\BookingParticipantType; use App\Form\Model\BookingDto; +use App\Form\Model\ParticipantEditDto; use App\Form\Service\ParticipantFieldHandlerRegistry; use App\Htmx\HxTrait; use App\Security\Crypt; @@ -244,10 +245,15 @@ class IndexController extends AbstractController return $this->redirectToRoute('app_booking_edit', ['id' => $id]); } + // Create wrapper DTO for email uniqueness validation + $wrapper = new ParticipantEditDto( + participant: $participant, + bookingContext: $bookingDto, + ); + // Create form for participant with booking context - $form = $this->createForm(BookingParticipantType::class, $participant, [ + $form = $this->createForm(BookingParticipantType::class, $wrapper, [ 'booking_context' => $bookingDto, - 'edit_mode' => true, 'validation_groups' => ['booking_edit'], ]); @@ -327,10 +333,15 @@ class IndexController extends AbstractController ? $this->travelDataService->getMutabilityData($bookingData->dateId) : null; + // Create wrapper DTO for form + $wrapper = new ParticipantEditDto( + participant: $bookingDto->participants[$index], + bookingContext: $bookingDto, + ); + // Create form with validation disabled - $form = $this->createForm(BookingParticipantType::class, $bookingDto->participants[$index], [ + $form = $this->createForm(BookingParticipantType::class, $wrapper, [ 'booking_context' => $bookingDto, - 'edit_mode' => true, 'validation_groups' => false, ]); diff --git a/src/Form/BookingParticipantType.php b/src/Form/BookingParticipantType.php index 7899805..b904403 100644 --- a/src/Form/BookingParticipantType.php +++ b/src/Form/BookingParticipantType.php @@ -42,7 +42,7 @@ class BookingParticipantType extends AbstractType /** @var BookingDto $bookingContext */ $bookingContext = $options['booking_context']; - // Select field state provider based on edit_mode option + // Select field state provider based on booking mode $this->fieldStateProvider = BookingDto::MODE_EDIT === $bookingContext->getMode() ? $this->editFieldStateProvider : $this->createFieldStateProvider; diff --git a/src/Form/Service/Condition/AuthenticatedUserPersonalDataCondition.php b/src/Form/Service/Condition/AuthenticatedUserPersonalDataCondition.php index 2dfd373..1e1ebc7 100644 --- a/src/Form/Service/Condition/AuthenticatedUserPersonalDataCondition.php +++ b/src/Form/Service/Condition/AuthenticatedUserPersonalDataCondition.php @@ -10,7 +10,7 @@ use App\Form\Service\Contract\FieldConditionInterface; /** * Condition that determines if personal data fields should be hidden for authenticated users. * - * When a participant is linked to an existing BPN account (has addressId and personId), + * When a participant is linked to an existing BPN account (has personId), * their personal data should not be editable during the booking process. Changes to * master personal data should only happen through the dedicated personal data management * interface to prevent: @@ -18,6 +18,10 @@ use App\Form\Service\Contract\FieldConditionInterface; * - Disconnecting bookings from the user's account * - Data inconsistencies between booking and account data * + * Note: In edit mode, the BPN API may not return addressId in booking responses, + * so we rely on personId alone to identify authenticated users. The personId is + * sufficient to link a participant to an existing BPN account. + * * When this condition is satisfied (returns true), the template should: * - Hide the form fields for personal data * - Display the values as static, read-only text @@ -46,9 +50,10 @@ class AuthenticatedUserPersonalDataCondition implements FieldConditionInterface return false; } - // Hide personal data fields if participant has BPN account IDs - // (indicates prepopulation from authenticated user) - return null !== $participant->addressId && null !== $participant->personId; + // Hide personal data fields if participant has BPN person ID + // In edit mode, bookings may not include addressId, so we check personId only + // The personId alone is sufficient to identify a participant linked to a BPN account + return null !== $participant->personId; } /** diff --git a/src/Form/Service/EditFieldStateProvider.php b/src/Form/Service/EditFieldStateProvider.php index ef6f353..7573e95 100644 --- a/src/Form/Service/EditFieldStateProvider.php +++ b/src/Form/Service/EditFieldStateProvider.php @@ -8,6 +8,7 @@ use App\BusProNet\Utility\DirectionMapper; use App\Form\Service\Abstract\AbstractFieldStateProvider; use App\Form\Service\Condition\AdditionalServicesMutabilityCondition; use App\Form\Service\Condition\ApplicantCondition; +use App\Form\Service\Condition\AuthenticatedUserPersonalDataCondition; use App\Form\Service\Condition\CompositeCondition; use App\Form\Service\Condition\DateOfBirthProvidedCondition; use App\Form\Service\Condition\FieldValueCondition; @@ -43,7 +44,12 @@ class EditFieldStateProvider extends AbstractFieldStateProvider $transportationServicesMutabilityCondition = new TransportationServicesMutabilityCondition(); $pickupsMutabilityCondition = new PickupsMutabilityCondition(); + // Authenticated user personal data protection + // Hide personal data fields for participants linked to BPN accounts (prevents duplicate records) + $authenticatedUserCondition = new AuthenticatedUserPersonalDataCondition(); + // Make all personal data fields readonly if participant not mutable + // OR hidden if participant is linked to BPN account (authenticated user) // Note: First participant is now treated as independent from applicant and can be edited $personalDataFields = [ 'firstName', @@ -56,15 +62,38 @@ class EditFieldStateProvider extends AbstractFieldStateProvider ]; foreach ($personalDataFields as $field) { $this->fieldStateConditions[$field] = [ + 'hidden' => $authenticatedUserCondition, 'readonly' => CompositeCondition::not(new MutabilityCondition()), ]; } - // Address fields - readonly if participant not mutable + // Address fields - hidden for authenticated users, readonly if participant not mutable $this->fieldStateConditions['address'] = [ + 'hidden' => $authenticatedUserCondition, 'readonly' => CompositeCondition::not(new MutabilityCondition()), ]; + // Address subfields - must be hidden to prevent creating duplicate BPN records + $this->fieldStateConditions['address.street'] = [ + 'hidden' => $authenticatedUserCondition, + ]; + + $this->fieldStateConditions['address.postCode'] = [ + 'hidden' => $authenticatedUserCondition, + ]; + + $this->fieldStateConditions['address.city'] = [ + 'hidden' => $authenticatedUserCondition, + ]; + + $this->fieldStateConditions['address.country'] = [ + 'hidden' => $authenticatedUserCondition, + ]; + + $this->fieldStateConditions['address.district'] = [ + 'hidden' => $authenticatedUserCondition, + ]; + // Conditional visibility for service fields (same as create flow) $rentalCondition = new RentalSelectionCondition(); $skiPassCondition = new SkiPassSelectionCondition(); diff --git a/templates/booking/_participant_form.html.twig b/templates/booking/_participant_form.html.twig index f3a9da5..20811db 100644 --- a/templates/booking/_participant_form.html.twig +++ b/templates/booking/_participant_form.html.twig @@ -1,5 +1,17 @@ {% import _self as macros %} +{# Macro to render a form field or static value #} +{% macro field_or_static(form, fieldName, label, staticValue, options = {}) %} + {% if form[fieldName] is defined %} + {{ form_row(form[fieldName], options) }} + {% else %} +
+ +
{{ staticValue ?: '-' }}
+
+ {% endif %} +{% endmacro %} + {# Macro to render a field or placeholder with consistent fieldset structure #} {% macro service_field(form, fieldName, label, options = {}, undefinedLabel = 'Nicht wählbar') %} {% if form[fieldName] is defined %} @@ -46,88 +58,34 @@
{# Personal data section #}
- {% if form.firstName is defined %} - {{ form_row(form.firstName) }} - {% else %} -
- -
{{ form.vars.data.participant.firstName }}
-
- {% endif %} + {{ macros.field_or_static(form, 'firstName', 'Vorname', form.vars.data.participant.firstName) }} + {{ macros.field_or_static(form, 'lastName', 'Nachname', form.vars.data.participant.lastName) }} - {% if form.lastName is defined %} - {{ form_row(form.lastName) }} - {% else %} -
- -
{{ form.vars.data.participant.lastName }}
-
- {% endif %} - - {% if form.dateOfBirth is defined %} - {{ form_row(form.dateOfBirth, { + {{ macros.field_or_static( + form, + 'dateOfBirth', + 'Geburtsdatum', + form.vars.data.participant.dateOfBirth ? form.vars.data.participant.dateOfBirth|date('d.m.Y') : null, + { 'attr': { 'hx-trigger': 'change', 'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})), 'hx-target': '#main-content', 'hx-swap': 'innerHTML' } - }) }} - {% else %} -
- -
{{ form.vars.data.participant.dateOfBirth ? form.vars.data.participant.dateOfBirth|date('d.m.Y') : '-' }}
-
- {% endif %} + } + ) }} - {% if form.gender is defined %} - {{ form_row(form.gender) }} - {% else %} -
- -
- {% if form.vars.data.participant.gender == 'M' %} - männlich - {% elseif form.vars.data.participant.gender == 'W' %} - weiblich - {% elseif form.vars.data.participant.gender == 'D' %} - divers - {% else %} - - - {% endif %} -
-
- {% endif %} + {% set genderLabel = form.vars.data.participant.gender == 'M' ? 'männlich' : (form.vars.data.participant.gender == 'W' ? 'weiblich' : (form.vars.data.participant.gender == 'D' ? 'divers' : null)) %} + {{ macros.field_or_static(form, 'gender', 'Geschlecht', genderLabel) }} - {% if form.nationality is defined %} - {{ form_row(form.nationality) }} - {% else %} -
- -
{{ form.vars.data.participant.nationality ?: '-' }}
-
- {% endif %} + {{ macros.field_or_static(form, 'nationality', 'Nationalität', form.vars.data.participant.nationality) }}
{# Contact information #}
- {% if form.email is defined %} - {{ form_row(form.email) }} - {% else %} -
- -
{{ form.vars.data.participant.email }}
-
- {% endif %} - - {% if form.mobile is defined %} - {{ form_row(form.mobile) }} - {% else %} -
- -
{{ form.vars.data.participant.mobile ?: '-' }}
-
- {% endif %} + {{ macros.field_or_static(form, 'email', 'E-Mail', form.vars.data.participant.email) }} + {{ macros.field_or_static(form, 'mobile', 'Telefon (mobil)', form.vars.data.participant.mobile) }}
{# Address #}