From ec1bba7c468c9fe6c0ea0bba16ed980216f0172b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 7 Oct 2025 16:38:55 +0200 Subject: [PATCH] wip: backport booking create flow form handling to edit flow --- .../DataProcessor/BookingDataProcessor.php | 30 +++++++- src/Form/Model/BookingEditDto.php | 4 + .../ParticipantRoomChoiceLoaderFactory.php | 74 ++++++++++++++++++- .../ParticipantFieldOptionsProvider.php | 53 ++++++++----- templates/booking/edit.html.twig | 14 +--- 5 files changed, 143 insertions(+), 32 deletions(-) diff --git a/src/BusProNet/DataProcessor/BookingDataProcessor.php b/src/BusProNet/DataProcessor/BookingDataProcessor.php index 4f4607b..c528859 100644 --- a/src/BusProNet/DataProcessor/BookingDataProcessor.php +++ b/src/BusProNet/DataProcessor/BookingDataProcessor.php @@ -69,6 +69,7 @@ class BookingDataProcessor * Resets all existing participant-to-service mappings to start with a clean slate. * * This ensures that service assignments are rebuilt from scratch based on current form selections. + * Includes resetting room mappings to allow room reassignments during edit. * * @param object $bookingData The booking data object containing services to reset */ @@ -79,6 +80,7 @@ class BookingDataProcessor ...$bookingData->transportationServices, ...$bookingData->pickupsOutbound, ...$bookingData->pickupsInbound, + ...$bookingData->rooms, ]; foreach ($servicesToReset as $service) { @@ -90,7 +92,7 @@ class BookingDataProcessor * Processes all services for a single participant. * * This orchestrator method handles the complete service assignment workflow for one participant, - * including additional services, transportation services, and pickup locations. + * including additional services, transportation services, pickup locations, and room assignments. * * @param object $participant The participant data from the form * @param object $bookingData The booking data object to update @@ -105,6 +107,7 @@ class BookingDataProcessor $this->processAdditionalServices($participant, $bookingData, $travelData); $this->processTransportationServices($participant, $bookingData, $travelData); $this->processPickupLocations($participant, $bookingData); + $this->processRoomAssignment($participant, $bookingData); } /** @@ -190,6 +193,31 @@ class BookingDataProcessor } } + /** + * Processes room assignment for a participant. + * + * Maps the participant to their assigned room. Allows room reassignment during + * booking edits while maintaining the constraint that participants can only be + * assigned to room types that have already been booked. + * + * @param object $participant The participant data from the form + * @param object $bookingData The booking data object to update + */ + private function processRoomAssignment(object $participant, object $bookingData): void + { + if (null === $participant->assignedRoomId) { + return; + } + + // Find the room in the existing booking by ID + foreach ($bookingData->rooms as $room) { + if ($room->id === $participant->assignedRoomId) { + $room->mapping[] = $participant->index; + break; + } + } + } + /** * Removes services and pickups with no participant mappings. * diff --git a/src/Form/Model/BookingEditDto.php b/src/Form/Model/BookingEditDto.php index cc49552..a508c69 100644 --- a/src/Form/Model/BookingEditDto.php +++ b/src/Form/Model/BookingEditDto.php @@ -60,6 +60,10 @@ class BookingEditDto implements BookingDtoInterface $pickup = $booking->getPickupForParticipant($index); $participantData->pickup = $pickup; + // Room assignment - extract from booking room mappings + $room = $booking->getRoomForParticipant($index); + $participantData->assignedRoomId = $room?->id; + $instance->participants[$index] = $participantData; } diff --git a/src/Form/Service/Factory/ParticipantRoomChoiceLoaderFactory.php b/src/Form/Service/Factory/ParticipantRoomChoiceLoaderFactory.php index 6dcb5ef..b18a938 100644 --- a/src/Form/Service/Factory/ParticipantRoomChoiceLoaderFactory.php +++ b/src/Form/Service/Factory/ParticipantRoomChoiceLoaderFactory.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Form\Service\Factory; +use App\BusProNet\Model\Room; use App\Form\ChoiceLoader\ParticipantRoomChoiceLoader; use App\Form\Model\ParticipantDto; use App\Form\Model\RoomSelectionDto; @@ -11,6 +12,9 @@ use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; /** * Factory service for creating ParticipantRoomChoiceLoader instances. + * + * Provides factory methods for both create and edit contexts, converting + * different room data formats into a unified choice loader interface. */ class ParticipantRoomChoiceLoaderFactory { @@ -20,12 +24,16 @@ class ParticipantRoomChoiceLoaderFactory } /** - * Creates a ParticipantRoomChoiceLoader for the given participant and room data. + * Creates a ParticipantRoomChoiceLoader for create context. * - * @param ParticipantDto[] $allParticipants - * @param RoomSelectionDto[] $selectedRooms + * Uses room selections from step 1 (RoomSelectionDto objects) to generate + * available room choices for participants. + * + * @param ParticipantDto[] $allParticipants all participants from the booking + * @param RoomSelectionDto[] $selectedRooms rooms selected in create flow step 1 + * @param int $participantIndex index of the participant needing room choices */ - public function create( + public function createForCreate( array $allParticipants, array $selectedRooms, int $participantIndex, @@ -37,4 +45,62 @@ class ParticipantRoomChoiceLoaderFactory $participantIndex ); } + + /** + * Creates a ParticipantRoomChoiceLoader for edit context. + * + * Uses already-booked rooms from the existing booking to generate available + * room choices. Participants can only be reassigned within the room types + * that have already been booked. + * + * @param ParticipantDto[] $allParticipants all participants from the booking + * @param Room[] $bookedRooms rooms from the existing booking + * @param int $participantIndex index of the participant needing room choices + */ + public function createForEdit( + array $allParticipants, + array $bookedRooms, + int $participantIndex, + ): ParticipantRoomChoiceLoader { + // Convert Booking Room objects to RoomSelectionDto format for the choice loader + $roomSelections = $this->convertBookedRoomsToSelections($bookedRooms); + + return new ParticipantRoomChoiceLoader( + $this->choiceListFactory, + $allParticipants, + $roomSelections, + $participantIndex + ); + } + + /** + * Converts booked Room objects to RoomSelectionDto format. + * + * This adapter method allows reusing the existing ParticipantRoomChoiceLoader + * logic for edit context by converting the Booking->rooms array into the + * same format used in the create flow. + * + * @param Room[] $bookedRooms rooms from existing booking + * + * @return RoomSelectionDto[] converted room selections + */ + private function convertBookedRoomsToSelections(array $bookedRooms): array + { + $selections = []; + + foreach ($bookedRooms as $room) { + $selection = new RoomSelectionDto(); + $selection->roomId = $room->id; + $selection->roomLabel = $room->label; + $selection->roomPrice = $room->price; + // In edit context, totalCount represents how many of this room type were booked + $selection->quantity = $room->totalCount ?? 1; + // maxPax is the capacity per room + $selection->capacity = $room->maxPax ?? 1; + + $selections[] = $selection; + } + + return $selections; + } } diff --git a/src/Form/Service/ParticipantFieldOptionsProvider.php b/src/Form/Service/ParticipantFieldOptionsProvider.php index bd16376..9d35efe 100644 --- a/src/Form/Service/ParticipantFieldOptionsProvider.php +++ b/src/Form/Service/ParticipantFieldOptionsProvider.php @@ -10,6 +10,7 @@ use App\BusProNet\Model\Service; use App\BusProNet\Utility\DirectionMapper; use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingDtoInterface; +use App\Form\Model\BookingEditDto; use App\Form\Service\Abstract\AbstractFieldOptionsProvider; use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory; use App\Service\InsuranceMatchingService; @@ -80,25 +81,44 @@ 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, array $options = []) => [ - 'label' => 'Zimmer', - 'placeholder' => 'Nicht zugeordnet', - // Use factory to create context-aware choice loader that: - // - Shows only available rooms for this participant - // - Excludes rooms already assigned to other participants - // - Respects room capacity and booking constraints - 'choice_loader' => $bookingDto instanceof BookingCreateDto - ? $this->roomChoiceLoaderFactory->create( + // Room assignment field provider (available for both create and edit workflows) + $this->fieldOptionProviders['assignedRoomId'] = function (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) { + $choiceLoader = null; + $disabled = false; + + if ($bookingDto instanceof BookingCreateDto) { + // Create context: use selected rooms from step 1 + $choiceLoader = $this->roomChoiceLoaderFactory->createForCreate( $bookingDto->participants, $bookingDto->getSelectedRooms(), $participantIndex - ) - : null, - // Make field read-only when only one room type is selected - // Room is auto-assigned, no user choice needed - 'disabled' => $bookingDto instanceof BookingCreateDto && 1 === count($bookingDto->getSelectedRooms()), - ]; + ); + // Disable when only one room type selected (auto-assigned) + $disabled = 1 === count($bookingDto->getSelectedRooms()); + } elseif ($bookingDto instanceof BookingEditDto) { + // Edit context: use already-booked rooms from booking + $choiceLoader = $this->roomChoiceLoaderFactory->createForEdit( + $bookingDto->participants, + $bookingDto->booking->rooms, + $participantIndex + ); + // Disable when only one room in booking (no reassignment needed) + $disabled = 1 === count($bookingDto->booking->rooms); + } + + return [ + 'label' => 'Zimmer', + 'placeholder' => 'Nicht zugeordnet', + // Use factory to create context-aware choice loader that: + // - Shows only available rooms for this participant + // - Excludes rooms already assigned to other participants + // - Respects room capacity and booking constraints + 'choice_loader' => $choiceLoader, + // Make field read-only when only one room type is available + // Room is auto-assigned or cannot be changed, no user choice needed + 'disabled' => $disabled, + ]; + }; // Courses field provider - provides age-appropriate courses from travel data $this->fieldOptionProviders['courses'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [ @@ -615,7 +635,6 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider }); } - /** * Generates label for rental insurance checkbox including pricing information. */ diff --git a/templates/booking/edit.html.twig b/templates/booking/edit.html.twig index ea051d6..88dbf0a 100644 --- a/templates/booking/edit.html.twig +++ b/templates/booking/edit.html.twig @@ -233,17 +233,11 @@ {% endif %} - {# Room assignment - read-only display #} + {# Room assignment - editable dropdown #}
-
- - {% set room = bookingData.roomForParticipant(participantData.index) %} - {% if room %} -
{{ room.label }} ({{ room.individualPrice[participantData.index]|format_currency('EUR') }})
- {% else %} -
Keine Unterkunft zugeordnet
- {% endif %} -
+ {% if participant.assignedRoomId is defined %} + {{ form_row(participant.assignedRoomId) }} + {% endif %} {% if participant.remarksRoom is defined %} {{ form_row(participant.remarksRoom) }} {% endif %}