wip: backport booking create flow form handling to edit flow

This commit is contained in:
Björn Fromme
2026-03-16 11:59:10 +01:00
parent e330626b03
commit d70a27f2fd
5 changed files with 143 additions and 32 deletions
@@ -69,6 +69,7 @@ class BookingDataProcessor
* Resets all existing participant-to-service mappings to start with a clean slate. * 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. * 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 * @param object $bookingData The booking data object containing services to reset
*/ */
@@ -79,6 +80,7 @@ class BookingDataProcessor
...$bookingData->transportationServices, ...$bookingData->transportationServices,
...$bookingData->pickupsOutbound, ...$bookingData->pickupsOutbound,
...$bookingData->pickupsInbound, ...$bookingData->pickupsInbound,
...$bookingData->rooms,
]; ];
foreach ($servicesToReset as $service) { foreach ($servicesToReset as $service) {
@@ -90,7 +92,7 @@ class BookingDataProcessor
* Processes all services for a single participant. * Processes all services for a single participant.
* *
* This orchestrator method handles the complete service assignment workflow for one 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 $participant The participant data from the form
* @param object $bookingData The booking data object to update * @param object $bookingData The booking data object to update
@@ -105,6 +107,7 @@ class BookingDataProcessor
$this->processAdditionalServices($participant, $bookingData, $travelData); $this->processAdditionalServices($participant, $bookingData, $travelData);
$this->processTransportationServices($participant, $bookingData, $travelData); $this->processTransportationServices($participant, $bookingData, $travelData);
$this->processPickupLocations($participant, $bookingData); $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. * Removes services and pickups with no participant mappings.
* *
+4
View File
@@ -60,6 +60,10 @@ class BookingEditDto implements BookingDtoInterface
$pickup = $booking->getPickupForParticipant($index); $pickup = $booking->getPickupForParticipant($index);
$participantData->pickup = $pickup; $participantData->pickup = $pickup;
// Room assignment - extract from booking room mappings
$room = $booking->getRoomForParticipant($index);
$participantData->assignedRoomId = $room?->id;
$instance->participants[$index] = $participantData; $instance->participants[$index] = $participantData;
} }
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Form\Service\Factory; namespace App\Form\Service\Factory;
use App\BusProNet\Model\Room;
use App\Form\ChoiceLoader\ParticipantRoomChoiceLoader; use App\Form\ChoiceLoader\ParticipantRoomChoiceLoader;
use App\Form\Model\ParticipantDto; use App\Form\Model\ParticipantDto;
use App\Form\Model\RoomSelectionDto; use App\Form\Model\RoomSelectionDto;
@@ -11,6 +12,9 @@ use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
/** /**
* Factory service for creating ParticipantRoomChoiceLoader instances. * 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 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 * Uses room selections from step 1 (RoomSelectionDto objects) to generate
* @param RoomSelectionDto[] $selectedRooms * 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 $allParticipants,
array $selectedRooms, array $selectedRooms,
int $participantIndex, int $participantIndex,
@@ -37,4 +45,62 @@ class ParticipantRoomChoiceLoaderFactory
$participantIndex $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;
}
} }
@@ -10,6 +10,7 @@ use App\BusProNet\Model\Service;
use App\BusProNet\Utility\DirectionMapper; use App\BusProNet\Utility\DirectionMapper;
use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
use App\Form\Model\BookingEditDto;
use App\Form\Service\Abstract\AbstractFieldOptionsProvider; use App\Form\Service\Abstract\AbstractFieldOptionsProvider;
use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory; use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory;
use App\Service\InsuranceMatchingService; use App\Service\InsuranceMatchingService;
@@ -80,25 +81,44 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
*/ */
protected function registerFieldOptionProviders(): void protected function registerFieldOptionProviders(): void
{ {
// Room assignment field provider (only available for create workflow) // Room assignment field provider (available for both create and edit workflows)
$this->fieldOptionProviders['assignedRoomId'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [ $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
);
// 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', 'label' => 'Zimmer',
'placeholder' => 'Nicht zugeordnet', 'placeholder' => 'Nicht zugeordnet',
// Use factory to create context-aware choice loader that: // Use factory to create context-aware choice loader that:
// - Shows only available rooms for this participant // - Shows only available rooms for this participant
// - Excludes rooms already assigned to other participants // - Excludes rooms already assigned to other participants
// - Respects room capacity and booking constraints // - Respects room capacity and booking constraints
'choice_loader' => $bookingDto instanceof BookingCreateDto 'choice_loader' => $choiceLoader,
? $this->roomChoiceLoaderFactory->create( // Make field read-only when only one room type is available
$bookingDto->participants, // Room is auto-assigned or cannot be changed, no user choice needed
$bookingDto->getSelectedRooms(), 'disabled' => $disabled,
$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()),
]; ];
};
// Courses field provider - provides age-appropriate courses from travel data // Courses field provider - provides age-appropriate courses from travel data
$this->fieldOptionProviders['courses'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [ $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. * Generates label for rental insurance checkbox including pricing information.
*/ */
+3 -9
View File
@@ -233,17 +233,11 @@
</div> </div>
{% endif %} {% endif %}
{# Room assignment - read-only display #} {# Room assignment - editable dropdown #}
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> {% if participant.assignedRoomId is defined %}
<label class="font-semibold mb-1 block">Zimmer</label> {{ form_row(participant.assignedRoomId) }}
{% set room = bookingData.roomForParticipant(participantData.index) %}
{% if room %}
<div class="text-sm">{{ room.label }} ({{ room.individualPrice[participantData.index]|format_currency('EUR') }})</div>
{% else %}
<div class="text-sm text-gray-500">Keine Unterkunft zugeordnet</div>
{% endif %} {% endif %}
</div>
{% if participant.remarksRoom is defined %} {% if participant.remarksRoom is defined %}
{{ form_row(participant.remarksRoom) }} {{ form_row(participant.remarksRoom) }}
{% endif %} {% endif %}