wip: backport booking create flow form handling to edit flow

This commit is contained in:
Björn Fromme
2025-10-07 16:38:55 +02:00
parent b66d453881
commit ec1bba7c46
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.
*
* 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.
*
+4
View File
@@ -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;
}
@@ -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;
}
}
@@ -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.
*/
+4 -10
View File
@@ -233,17 +233,11 @@
</div>
{% endif %}
{# Room assignment - read-only display #}
{# Room assignment - editable dropdown #}
<div class="grid grid-cols-2 gap-4">
<div>
<label class="font-semibold mb-1 block">Zimmer</label>
{% 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 %}
</div>
{% if participant.assignedRoomId is defined %}
{{ form_row(participant.assignedRoomId) }}
{% endif %}
{% if participant.remarksRoom is defined %}
{{ form_row(participant.remarksRoom) }}
{% endif %}