wip: improved booking summary

This commit is contained in:
Björn Fromme
2026-03-16 11:59:09 +01:00
parent 727ee9b014
commit 65a33922cf
11 changed files with 168 additions and 90 deletions
+2 -3
View File
@@ -1,9 +1,8 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(php -l:*)", "Bash(php -l:*)"
"Bash(php:*)"
], ],
"deny": [] "deny": []
} }
} }
+16
View File
@@ -17,6 +17,9 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
*/ */
class Room class Room
{ {
public const SELECTION_TYPE_BY_PAX = 'by_pax';
public const SELECTION_TYPE_BY_ROOM = 'by_room';
#[Groups(['api:list', 'api:single'])] #[Groups(['api:list', 'api:single'])]
public ?int $id = null; public ?int $id = null;
@@ -72,4 +75,17 @@ class Room
#[Groups(['booking'])] #[Groups(['booking'])]
public array $individualPrice = []; public array $individualPrice = [];
/**
* Determines the selection type of the room based on its label.
*
* @return string Either self::SELECTION_TYPE_BY_PAX or self::SELECTION_TYPE_BY_ROOM
*/
public function getSelectionType(): string
{
if (1 === preg_match('/bett/i', $this->label)) {
return self::SELECTION_TYPE_BY_PAX;
}
return self::SELECTION_TYPE_BY_ROOM;
}
} }
+8 -4
View File
@@ -188,12 +188,16 @@ class Travel
* greater than zero and have an available status. This ensures only * greater than zero and have an available status. This ensures only
* bookable rooms are returned for selection. * bookable rooms are returned for selection.
* *
* @return array<Room> The filtered array of available rooms * @return array<int, Room> The filtered array of available rooms, indexed by room ID
*/ */
public function getAvailableRooms(): array public function getAvailableRooms(): array
{ {
return array_filter($this->rooms, function (Room $room) { $result = [];
return $room->available > 0 && Constants::STATUS_AVAILABLE === $room->status; foreach ($this->rooms as $room) {
}); if ($room->available > 0 && Constants::STATUS_AVAILABLE === $room->status) {
$result[$room->id] = $room;
}
}
return $result;
} }
} }
@@ -23,7 +23,8 @@ class CreateStep1Controller extends AbstractController
public function __construct( public function __construct(
private readonly BookingService $bookingService, private readonly BookingService $bookingService,
) { )
{
} }
/** /**
@@ -51,11 +52,17 @@ class CreateStep1Controller extends AbstractController
return $this->redirectToRoute('app_booking_create_step_2'); return $this->redirectToRoute('app_booking_create_step_2');
} }
$availableRooms = $bookingCreateDto->travel->getAvailableRooms();
$groupedRooms = $this->bookingService->groupRoomsBySelectionType($availableRooms);
$groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($summary['selectedRooms'], $availableRooms);
return $this->render('booking/create_step_1.html.twig', [ return $this->render('booking/create_step_1.html.twig', [
'bookingCreateDto' => $bookingCreateDto, 'bookingCreateDto' => $bookingCreateDto,
'roomSummary' => $summary['selectedRooms'], 'roomSummary' => $summary['selectedRooms'],
'participantCount' => $summary['participantCount'], 'participantCount' => $summary['participantCount'],
'form' => $form->createView(), 'form' => $form->createView(),
'groupedRooms' => $groupedRooms,
'groupedSelectedRooms' => $groupedSelectedRooms,
]); ]);
} }
@@ -76,10 +83,14 @@ class CreateStep1Controller extends AbstractController
$this->bookingService->saveBookingCreateDto($request, $bookingCreateDto); $this->bookingService->saveBookingCreateDto($request, $bookingCreateDto);
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto); $summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto);
$availableRooms = $bookingCreateDto->travel->getAvailableRooms();
$groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($summary['selectedRooms'], $availableRooms);
return $this->render('booking/_room_summary.html.twig', [ return $this->render('booking/_summary.html.twig', [
'bookingCreateDto' => $bookingCreateDto,
'roomSummary' => $summary['selectedRooms'], 'roomSummary' => $summary['selectedRooms'],
'participantCount' => $summary['participantCount'], 'participantCount' => $summary['participantCount'],
'groupedSelectedRooms' => $groupedSelectedRooms,
]); ]);
} }
} }
@@ -64,12 +64,15 @@ class CreateStep2Controller extends AbstractController
} }
$roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($bookingCreateDto); $roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($bookingCreateDto);
$availableRooms = $bookingCreateDto->travel->getAvailableRooms();
$groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($bookingCreateDto->getSelectedRooms(), $availableRooms);
return $this->render('booking/create_step_2.html.twig', [ return $this->render('booking/create_step_2.html.twig', [
'bookingCreateDto' => $bookingCreateDto, 'bookingCreateDto' => $bookingCreateDto,
'participantsCount' => $participantsCount, 'participantsCount' => $participantsCount,
'assignmentCounts' => $roomAssignmentCounts, 'assignmentCounts' => $roomAssignmentCounts,
'form' => $form->createView(), 'form' => $form->createView(),
'groupedSelectedRooms' => $groupedSelectedRooms,
]); ]);
} }
@@ -93,6 +96,8 @@ class CreateStep2Controller extends AbstractController
$this->bookingService->saveBookingCreateDto($request, $bookingCreateDto); $this->bookingService->saveBookingCreateDto($request, $bookingCreateDto);
$roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($bookingCreateDto); $roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($bookingCreateDto);
$availableRooms = $bookingCreateDto->travel->getAvailableRooms();
$groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($bookingCreateDto->getSelectedRooms(), $availableRooms);
// The DTO is now updated with the latest selection. // The DTO is now updated with the latest selection.
// We can now render the blocks with the fresh data. // We can now render the blocks with the fresh data.
@@ -104,6 +109,7 @@ class CreateStep2Controller extends AbstractController
'bookingCreateDto' => $bookingCreateDto, 'bookingCreateDto' => $bookingCreateDto,
'participantsCount' => $participantsCount, 'participantsCount' => $participantsCount,
'assignmentCounts' => $roomAssignmentCounts, 'assignmentCounts' => $roomAssignmentCounts,
'groupedSelectedRooms' => $groupedSelectedRooms,
] ]
); );
} }
-9
View File
@@ -16,13 +16,4 @@ class RoomSelectionDto
public int $maxQuantity = 100; public int $maxQuantity = 100;
public int $minPax = 0; public int $minPax = 0;
public function getType(): string
{
if (1 === preg_match('/bett/i', $this->roomLabel)) {
return 'by_pax';
}
return 'by_room';
}
} }
+47
View File
@@ -137,4 +137,51 @@ class BookingService
'participantCount' => $participantCount, 'participantCount' => $participantCount,
]; ];
} }
/**
* Groups available rooms by selection type ('by_pax' or 'by_room').
*
* @param array<int, Room> $rooms Rooms indexed by room ID
* @return array{by_pax: array<int, Room>, by_room: array<int, Room>}
*/
public function groupRoomsBySelectionType(array $rooms): array
{
$groups = [
'by_pax' => [],
'by_room' => [],
];
foreach ($rooms as $room) {
if (stripos($room->label, 'bett') !== false) {
$groups['by_pax'][$room->id] = $room;
} else {
$groups['by_room'][$room->id] = $room;
}
}
return $groups;
}
/**
* Groups roomSelections by selection type ('by_pax' or 'by_room'), using Room::getSelectionType().
*
* @param array $roomSelections Array of selected RoomSelectionDto
* @param array<int, Room> $roomsById Rooms indexed by room ID
* @return array{by_pax: array, by_room: array}
*/
public function groupRoomSelectionsByType(array $roomSelections, array $roomsById): array
{
$groups = [
'by_pax' => [],
'by_room' => [],
];
foreach ($roomSelections as $roomSelection) {
$room = $roomsById[$roomSelection->roomId] ?? null;
if ($room) {
$type = $room->getSelectionType();
$groups[$type][] = $roomSelection;
}
}
return $groups;
}
} }
-15
View File
@@ -1,15 +0,0 @@
<div id="booking-summary">
<h3>Zusammenfassung</h3>
<h4>Zimmer</h4>
<ul class="list-disc list-inside pb-4">
{% for roomSelection in roomSummary %}
<li>
{{ roomSelection.quantity }} x {{ roomSelection.roomLabel }}
</li>
{% endfor %}
</ul>
<h4>Anzahl Teilnehmer</h4>
<p>
{{ participantCount }}
</p>
</div>
+32
View File
@@ -0,0 +1,32 @@
<h3>Zusammenfassung</h3>
<p>Reise: {{ bookingCreateDto.travel.label }}</p>
<p>Datum: {{ bookingCreateDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo|date('d.m.Y') }}</p>
<p>Hotel: {{ bookingCreateDto.travel.hotel.name }}</p>
{% if groupedSelectedRooms.by_room is not empty %}
<h4>Zimmer</h4>
<ul class="list-disc pl-4 pb-4">
{% for roomSelection in groupedSelectedRooms.by_room %}
<li>
{{ roomSelection.quantity }} x {{ roomSelection.roomLabel }}
{% if assignmentCounts is defined and assignmentCounts[roomSelection.roomId] is defined %}
<span class="block text-sm">{{ assignmentCounts[roomSelection.roomId] }}/{{ roomSelection.quantity }} belegt</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% if groupedSelectedRooms.by_pax is not empty %}
<h4>Betten</h4>
<ul class="list-disc pl-4 pb-4">
{% for roomSelection in groupedSelectedRooms.by_pax %}
<li>
{{ roomSelection.quantity }} x {{ roomSelection.roomLabel }}
{% if assignmentCounts is defined and assignmentCounts[roomSelection.roomId] is defined %}
<span class="block text-sm">{{ assignmentCounts[roomSelection.roomId] }}/{{ roomSelection.quantity }} belegt</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
<h4>Anzahl Teilnehmer</h4>
<p>{{ participantCount }}</p>
+34 -9
View File
@@ -5,28 +5,53 @@
<h1>Neue Buchung</h1> <h1>Neue Buchung</h1>
<div class="grid grid-cols-3 gap-8"> <div class="grid grid-cols-3 gap-8">
<div class="col-span-2"> <div class="col-span-2">
<h2>Zimmerauswahl</h2> <h2 class="pb-4">
Unterkunft
</h2>
{{ form_start(form) }} {{ form_start(form) }}
<div> {% if groupedRooms.by_room is not empty %}
{% for roomSelection in form.roomSelections %} <h3>
{{ form_row(roomSelection, { Zimmer
</h3>
{% for roomId, room in groupedRooms.by_room %}
{{ form_row(form.roomSelections[roomId], {
'attr': { 'attr': {
'hx-post': path('app_booking_create_step_1_room_summary'), 'hx-post': path('app_booking_create_step_1_room_summary'),
'hx-target': '#booking-summary', 'hx-target': '#booking-summary',
'hx-swap': 'outerHTML', 'hx-swap': 'innerHTML',
'hx-trigger': 'change' 'hx-trigger': 'change'
} }
}) }} }) }}
{% endfor %} {% endfor %}
</div> {% endif %}
<div class="flex justify-end"> {% if groupedRooms.by_pax is not empty %}
<h3>
Betten
</h3>
{% for roomId, room in groupedRooms.by_pax %}
{{ form_row(form.roomSelections[roomId], {
'attr': {
'hx-post': path('app_booking_create_step_1_room_summary'),
'hx-target': '#booking-summary',
'hx-swap': 'innerHTML',
'hx-trigger': 'change'
}
}) }}
{% endfor %}
{% endif %}
<div class="flex justify-end pt-4">
<button type="submit" class="button bg-button bg-button--secondary">Weiter</button> <button type="submit" class="button bg-button bg-button--secondary">Weiter</button>
</div> </div>
{{ form_rest(form) }} {{ form_rest(form) }}
{{ form_end(form) }} {{ form_end(form) }}
</div> </div>
<div> <div id="booking-summary">
{% include 'booking/_room_summary.html.twig' %} {% include 'booking/_summary.html.twig' with {
'bookingCreateDto': bookingCreateDto,
'participantCount': participantCount,
'groupedSelectedRooms': groupedSelectedRooms,
'assignmentCounts': []
} %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
+10 -48
View File
@@ -62,53 +62,15 @@
{{ form_rest(form) }} {{ form_rest(form) }}
{{ form_end(form) }} {{ form_end(form) }}
</div> </div>
<div> {% block booking_summary %}
{# This block contains the booking summary #} <div id="booking-summary"{% if htmx_oob_swap is defined and htmx_oob_swap %} hx-swap-oob="true"{% endif %}>
{% block booking_summary %} {% include 'booking/_summary.html.twig' with {
<div id="booking-summary"{% if htmx_oob_swap is defined and htmx_oob_swap %} hx-swap-oob="true"{% endif %}> 'bookingCreateDto': bookingCreateDto,
<h3>Zusammenfassung</h3> 'participantCount': participantsCount,
<h4> 'groupedSelectedRooms': groupedSelectedRooms,
Reise 'assignmentCounts': assignmentCounts
</h4> } %}
<p> </div>
{{ bookingCreateDto.travel.label }} {% endblock %}
</p>
<h4>
Datum
</h4>
<p>
{{ bookingCreateDto.travel.dateFrom | date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo | date('d.m.Y') }}
</p>
<h4>
Unterkunft
</h4>
<p>
{{ bookingCreateDto.travel.hotel.name }}
</p>
{% if participantsCount > 0 %}
<h4>
Anzahl Teilnehmer
</h4>
<p>
{{ participantsCount }}
</p>
{% endif %}
<h4>
Zimmer
</h4>
<ul>
{% for roomSelection in bookingCreateDto.selectedRooms %}
{% set assignedCount = assignmentCounts[roomSelection.roomId] ?? 0 %}
<li>
{{ roomSelection.roomLabel }}
<span class="text-sm pl-2 {% if assignedCount == roomSelection.quantity %}text-green-600{% else %}text-gray-500{% endif %}">
({{ assignedCount }}/{{ roomSelection.quantity }})
</span>
</li>
{% endfor %}
</ul>
</div>
{% endblock %}
</div>
</div> </div>
{% endblock %} {% endblock %}