feat: centralize create/edit flow contexts, extract edit submit service

This commit is contained in:
Björn Fromme
2026-04-12 10:17:04 +02:00
parent f903f17ebd
commit 69705052b3
28 changed files with 1342 additions and 423 deletions
@@ -140,6 +140,12 @@
</tr>
{% endmacro %}
{# Shared by booking create and edit flows; callers should pass one of the flow contexts. #}
{% set bookingFlowContext = bookingEditContext|default(bookingCreateContext|default(null)) %}
{% set bookingDto = bookingFlowContext.bookingDto %}
{% set summaryData = bookingFlowContext.summaryData %}
{% set mutableData = bookingFlowContext.mutableData|default(null) %}
{# Standalone participant form view (replaces main content area) #}
{% block participant_form %}
{% form_theme form 'booking/_form_theme.html.twig' %}
@@ -684,7 +690,7 @@
{% block booking_summary %}
<div id="booking-summary" hx-swap-oob="innerHTML">
{% include 'booking/_summary.html.twig' with {
'bookingCreateDto': bookingDto,
'bookingDto': bookingDto,
'summaryData': summaryData,
'mutableData': mutableData|default(null)
} %}
+7 -7
View File
@@ -29,7 +29,7 @@
Reise
</th>
<td class="pl-2">
{{ bookingCreateDto.travel.label }}
{{ bookingDto.travel.label }}
</td>
</tr>
<tr>
@@ -37,7 +37,7 @@
Zeitraum
</th>
<td class="pl-2">
{{ bookingCreateDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo|date('d.m.Y') }}
{{ bookingDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingDto.travel.dateTo|date('d.m.Y') }}
</td>
</tr>
<tr>
@@ -45,7 +45,7 @@
Unterkunft
</th>
<td class="pl-2">
{{ bookingCreateDto.travel.hotel.name }}
{{ bookingDto.travel.hotel.name }}
</td>
</tr>
<tr>
@@ -56,13 +56,13 @@
{{ summaryData.participantCount }}
</td>
</tr>
{% if bookingCreateDto.bookingStatus != 'F' %}
{% if bookingDto.bookingStatus != 'F' %}
<tr>
<th class="text-left align-top border-r-2 border-gray-300 pr-2">
Status
</th>
<td class="pl-2">
{{ bookingCreateDto.bookingStatus|map_status }}
{{ bookingDto.bookingStatus|map_status }}
</td>
</tr>
{% endif %}
@@ -72,7 +72,7 @@
{% include 'booking/_summary_hotel.html.twig' %}
{# Mutability information (edit mode only) #}
{% if bookingCreateDto.mode == constant('App\\Form\\Model\\BookingDto::MODE_EDIT') and mutableData %}
{% if bookingDto.mode == constant('App\\Form\\Model\\BookingDto::MODE_EDIT') and mutableData %}
<div class="border border-primary-bg mb-4">
<div class="p-2 bg-primary-bg uppercase font-semibold">
Aktualisierung möglich bis
@@ -201,7 +201,7 @@
{# Total Section #}
{% if summaryData.totalPrice > 0 %}
{# Show subtotal and voucher discounts when vouchers are applied #}
{% set acceptedVouchers = bookingCreateDto.getAcceptedVouchers() %}
{% set acceptedVouchers = bookingDto.getAcceptedVouchers() %}
{% if acceptedVouchers and acceptedVouchers.hasVouchers() %}
<div class="border border-primary-bg mb-4">
<div class="p-2 bg-primary-bg uppercase font-semibold">
+6 -6
View File
@@ -43,8 +43,8 @@
{{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }}
{% if htmx_oob_swap is defined and htmx_oob_swap %}hx-swap-oob="true"{% endif %}>
{% include 'booking/_summary.html.twig' with {
'bookingCreateDto': bookingCreateDto,
'summaryData': summaryData
'bookingDto': bookingCreateContext.bookingDto,
'summaryData': bookingCreateContext.summaryData
} %}
</div>
{% endblock %}
@@ -65,19 +65,19 @@
{% block room_selection_form %}
<div id="room-selection-form"{% if htmx_oob_swap is defined and htmx_oob_swap %} hx-swap-oob="true"{% endif %}>
{{ form_errors(form) }}
{% if groupedRooms.by_room is not empty %}
{% if bookingCreateContext.groupedRooms.by_room is not empty %}
<h3>
Zimmer
</h3>
{% for roomId, room in groupedRooms.by_room %}
{% for roomId, room in bookingCreateContext.groupedRooms.by_room %}
{{ _self.stepFormField(form.roomSelections[roomId]) }}
{% endfor %}
{% endif %}
{% if groupedRooms.by_pax is not empty %}
{% if bookingCreateContext.groupedRooms.by_pax is not empty %}
<h3>
Betten
</h3>
{% for roomId, room in groupedRooms.by_pax %}
{% for roomId, room in bookingCreateContext.groupedRooms.by_pax %}
{{ _self.stepFormField(form.roomSelections[roomId]) }}
{% endfor %}
{% endif %}
+4 -4
View File
@@ -18,8 +18,8 @@
{{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }}
{% if htmx_oob_swap|default(false) %} hx-swap-oob="true"{% endif %}>
{% include 'booking/_summary.html.twig' with {
'bookingCreateDto': bookingDto,
'summaryData': summaryData
'bookingDto': bookingCreateContext.bookingDto,
'summaryData': bookingCreateContext.summaryData
} %}
</div>
{% endblock %}
@@ -45,13 +45,13 @@
{% endif %}
<div class="divide-y divide-gray-200">
{% for cardData in cardsData %}
{% for cardData in bookingCreateContext.cardsData %}
{% include 'booking/_participant_card.html.twig' with {
'cardData': cardData,
'index': loop.index0,
'participantNumber': loop.index,
'mode': 'create',
'isSubmitted': isSubmitted
'isSubmitted': bookingCreateContext.isSubmitted
} %}
{% endfor %}
</div>
@@ -16,8 +16,8 @@
<div id="booking-summary"
class="hidden lg:block lg:order-2 lg:flex-1 lg:min-h-0 lg:col-span-2">
{% include 'booking/_summary.html.twig' with {
'bookingCreateDto': bookingDto,
'summaryData': summaryData
'bookingDto': bookingCreateContext.bookingDto,
'summaryData': bookingCreateContext.summaryData
} %}
</div>
+2 -2
View File
@@ -18,8 +18,8 @@
{{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }}
{% if htmx_oob_swap is defined and htmx_oob_swap %}hx-swap-oob="true"{% endif %}>
{% include 'booking/_summary.html.twig' with {
'bookingCreateDto': bookingCreateDto,
'summaryData': summaryData
'bookingDto': bookingCreateContext.bookingDto,
'summaryData': bookingCreateContext.summaryData
} %}
</div>
{% endblock %}
+30 -30
View File
@@ -20,7 +20,7 @@
<div class="shrink-0 px-4 lg:px-8 py-4 lg:flex lg:flex-col lg:justify-center shadow-md relative z-10">
<div class="flex items-center justify-between pb-2">
<span class="block uppercase font-semibold">Gesamtpreis</span>
<span class="font-semibold">{{ summaryData.payableAmount|format_currency('EUR') }}</span>
<span class="font-semibold">{{ bookingCreateContext.summaryData.payableAmount|format_currency('EUR') }}</span>
</div>
<div class="flex items-center justify-between">
<span class="block uppercase font-semibold">Buchungsübersicht</span>
@@ -35,7 +35,7 @@
Reise
</th>
<td class="pl-2">
{{ bookingCreateDto.travel.label }}
{{ bookingCreateContext.bookingDto.travel.label }}
</td>
</tr>
<tr>
@@ -43,7 +43,7 @@
Zeitraum
</th>
<td class="pl-2">
{{ bookingCreateDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo|date('d.m.Y') }}
{{ bookingCreateContext.bookingDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateContext.bookingDto.travel.dateTo|date('d.m.Y') }}
</td>
</tr>
<tr>
@@ -51,7 +51,7 @@
Unterkunft
</th>
<td class="pl-2">
{{ bookingCreateDto.travel.hotel.name }}
{{ bookingCreateContext.bookingDto.travel.hotel.name }}
</td>
</tr>
<tr>
@@ -59,7 +59,7 @@
Teilnehmer
</th>
<td class="pl-2">
{{ summaryData.participantCount }}
{{ bookingCreateContext.summaryData.participantCount }}
</td>
</tr>
</table>
@@ -94,7 +94,7 @@
Reise
</th>
<td class="pl-2">
{{ bookingCreateDto.travel.label }}
{{ bookingCreateContext.bookingDto.travel.label }}
</td>
</tr>
<tr>
@@ -102,7 +102,7 @@
Zeitraum
</th>
<td class="pl-2">
{{ bookingCreateDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo|date('d.m.Y') }}
{{ bookingCreateContext.bookingDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateContext.bookingDto.travel.dateTo|date('d.m.Y') }}
</td>
</tr>
<tr>
@@ -110,7 +110,7 @@
Unterkunft
</th>
<td class="pl-2">
{{ bookingCreateDto.travel.hotel.name }}
{{ bookingCreateContext.bookingDto.travel.hotel.name }}
</td>
</tr>
<tr>
@@ -118,7 +118,7 @@
Teilnehmer
</th>
<td class="pl-2">
{{ summaryData.participantCount }}
{{ bookingCreateContext.summaryData.participantCount }}
</td>
</tr>
</table>
@@ -132,18 +132,18 @@
</h3>
{# Rooms Section #}
{% if summaryData.pricingData.rooms is not empty %}
{% if bookingCreateContext.summaryData.pricingData.rooms is not empty %}
<div class="border border-primary-bg mb-4">
<div class="p-2 bg-primary-bg uppercase font-semibold">
Unterkunft
</div>
<table class="w-full table-fixed">
{% for roomPricing in summaryData.pricingData.rooms %}
{% for roomPricing in bookingCreateContext.summaryData.pricingData.rooms %}
<tr>
<td class="p-2 align-top">
<div>{{ roomPricing.quantity }}x {{ roomPricing.label }}</div>
{% if summaryData.assignmentCounts[roomPricing.roomId] is defined %}
<div class="text-sm text-gray-600">{{ summaryData.assignmentCounts[roomPricing.roomId] }} Person(en) belegt</div>
{% if bookingCreateContext.summaryData.assignmentCounts[roomPricing.roomId] is defined %}
<div class="text-sm text-gray-600">{{ bookingCreateContext.summaryData.assignmentCounts[roomPricing.roomId] }} Person(en) belegt</div>
{% endif %}
</td>
<td class="p-2 align-top w-24 text-right whitespace-nowrap">
@@ -156,12 +156,12 @@
{% endif %}
{# Services Section #}
{% if summaryData.pricingData.services is not empty %}
{% if bookingCreateContext.summaryData.pricingData.services is not empty %}
<div class="border border-primary-bg mb-4">
<div class="p-2 bg-primary-bg uppercase font-semibold">
Leistungen
</div>
{% for serviceGroup in summaryData.pricingData.services %}
{% for serviceGroup in bookingCreateContext.summaryData.pricingData.services %}
<div class="p-2 text-primary-dark/70 uppercase font-semibold">
{{ serviceGroup.groupName }}
</div>
@@ -186,15 +186,15 @@
Teilnehmer
</h3>
{% for participant in bookingCreateDto.participants %}
{% for participant in bookingCreateContext.bookingDto.participants %}
<div class="border border-primary-bg mb-4">
<div class="p-2 bg-primary-bg uppercase font-semibold flex justify-between items-center">
<span>
{{ participant.firstName }} {{ participant.lastName }}
{% if loop.first and not bookingCreateDto.isInternalAgencyBooking() %}<span class="text-sm font-normal">(Anmelder:in)</span>{% endif %}
{% if loop.first and not bookingCreateContext.bookingDto.isInternalAgencyBooking() %}<span class="text-sm font-normal">(Anmelder:in)</span>{% endif %}
</span>
{% if participantPrices is defined and participantPrices[loop.index0] is defined %}
<span>{{ participantPrices[loop.index0]|format_currency('EUR') }}</span>
{% if bookingCreateContext.participantPrices is defined and bookingCreateContext.participantPrices[loop.index0] is defined %}
<span>{{ bookingCreateContext.participantPrices[loop.index0]|format_currency('EUR') }}</span>
{% endif %}
</div>
{# Personal Data - responsive grid #}
@@ -242,7 +242,7 @@
</div>
{# Unterkunft Section #}
{% set assignedRoom = bookingCreateDto.travel.getRoomById(participant.assignedRoomId) %}
{% set assignedRoom = bookingCreateContext.bookingDto.travel.getRoomById(participant.assignedRoomId) %}
{% if assignedRoom or participant.remarksRoom %}
<div class="px-2 py-1 text-primary-dark/70 text-sm uppercase font-semibold">
Unterkunft
@@ -462,8 +462,8 @@
{% endfor %}
{# Grand Total #}
{% if summaryData.pricingData.grandTotal is defined and summaryData.pricingData.grandTotal > 0 %}
{% set acceptedVouchers = bookingCreateDto.getAcceptedVouchers() %}
{% if bookingCreateContext.summaryData.pricingData.grandTotal is defined and bookingCreateContext.summaryData.pricingData.grandTotal > 0 %}
{% set acceptedVouchers = bookingCreateContext.bookingDto.getAcceptedVouchers() %}
{% if acceptedVouchers and acceptedVouchers.hasVouchers() %}
<div class="border border-primary-bg mb-4">
<div class="p-2 bg-primary-bg uppercase font-semibold">
@@ -475,7 +475,7 @@
Gesamtpreis
</td>
<td class="px-2 pb-2 align-top w-24 text-right whitespace-nowrap">
{{ summaryData.pricingData.grandTotal|format_currency('EUR') }}
{{ bookingCreateContext.summaryData.pricingData.grandTotal|format_currency('EUR') }}
</td>
</tr>
{% for voucher in acceptedVouchers.vouchers %}
@@ -498,12 +498,12 @@
</div>
<div class="flex items-center justify-between px-2 pb-4">
<span class="block uppercase font-semibold">Zu zahlen</span>
<span class="font-semibold">{{ summaryData.payableAmount|format_currency('EUR') }}</span>
<span class="font-semibold">{{ bookingCreateContext.summaryData.payableAmount|format_currency('EUR') }}</span>
</div>
{% else %}
<div class="flex items-center justify-between pb-4">
<span class="block uppercase font-semibold">Gesamtpreis</span>
<span class="font-semibold">{{ summaryData.pricingData.grandTotal|format_currency('EUR') }}</span>
<span class="font-semibold">{{ bookingCreateContext.summaryData.pricingData.grandTotal|format_currency('EUR') }}</span>
</div>
{% endif %}
{% endif %}
@@ -516,24 +516,24 @@
<table class="w-full table-fixed text-sm">
<tr>
<td class="p-2 align-top">
{% if bookingCreateDto.paymentMethod == constant('App\\BusProNet\\Constants::PAYMENT_METHOD_DEBIT') %}
{% if bookingCreateContext.bookingDto.paymentMethod == constant('App\\BusProNet\\Constants::PAYMENT_METHOD_DEBIT') %}
Lastschrift (Einzugsermächtigungsverfahren)
{% else %}
Überweisung
{% endif %}
</td>
</tr>
{% if bookingCreateDto.paymentMethod == constant('App\\BusProNet\\Constants::PAYMENT_METHOD_DEBIT') and bookingCreateDto.bankAccount %}
{% if bookingCreateContext.bookingDto.paymentMethod == constant('App\\BusProNet\\Constants::PAYMENT_METHOD_DEBIT') and bookingCreateContext.bookingDto.bankAccount %}
<tr>
<td class="p-2 align-top">
<span class="text-gray-600 font-semibold">IBAN:</span>
<span class="font-mono">{{ bookingCreateDto.bankAccount.iban }}</span>
<span class="font-mono">{{ bookingCreateContext.bookingDto.bankAccount.iban }}</span>
</td>
</tr>
<tr>
<td class="p-2 align-top">
<span class="text-gray-600 font-semibold">Kontoinhaber:</span>
{{ bookingCreateDto.bankAccount.accountHolder }}
{{ bookingCreateContext.bookingDto.bankAccount.accountHolder }}
</td>
</tr>
{% endif %}
@@ -572,7 +572,7 @@
Zurück
</a>
<button type="submit" class="button button--primary" {{ qa_attribute('btn-submit') }}>
{% if bookingCreateDto.bookingStatus == 'A' %}
{% if bookingCreateContext.bookingDto.bookingStatus == 'A' %}
Anfragen
{% else %}
Verbindlich buchen
+16 -16
View File
@@ -15,9 +15,9 @@
{{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }}
{% if htmx_oob_swap is defined and htmx_oob_swap %}hx-swap-oob="true"{% endif %}>
{% include 'booking/_summary.html.twig' with {
'bookingCreateDto': bookingDto,
'summaryData': summaryData,
'mutableData': mutableData|default(null)
'bookingDto': bookingEditContext.bookingDto,
'summaryData': bookingEditContext.summaryData,
'mutableData': bookingEditContext.mutableData|default(null)
} %}
</div>
{% endblock %}
@@ -30,9 +30,9 @@
<h1 class="text-xl font-semibold uppercase">
Buchung bearbeiten
</h1>
{% if isDirty %}
{% if bookingEditContext.isDirty %}
<button type="button"
hx-get="{{ path('app_booking_edit_reload', {id: bookingData.id}) }}"
hx-get="{{ path('app_booking_edit_reload', {id: bookingEditContext.bookingData.id}) }}"
hx-target="body"
hx-swap="beforeend"
class="button button--small button--secondary">
@@ -46,7 +46,7 @@
{% block participant_cards %}
<h2 class="pb-4">Teilnehmer:innen</h2>
{% if isDirty %}
{% if bookingEditContext.isDirty %}
{% include '_partials/_alert.html.twig' with {
level: 'warning',
title: 'Ungespeicherte Änderungen',
@@ -55,7 +55,7 @@
{% endif %}
{# Display validation errors #}
{% if hasValidationErrors|default(false) %}
{% if bookingEditContext.hasValidationErrors %}
{% include '_partials/_alert.html.twig' with {
level: 'error',
title: 'Bitte überprüfe die Teilnehmerdaten',
@@ -64,16 +64,16 @@
{% endif %}
<div id="participant-cards-grid" class="space-y-4">
{% for participant in bookingDto.participants %}
{% set isCanceled = (bookingData.participantsStatus[loop.index0] ?? null) == 'S' %}
{% for participant in bookingEditContext.bookingDto.participants %}
{% set isCanceled = (bookingEditContext.bookingData.participantsStatus[loop.index0] ?? null) == 'S' %}
{% include 'booking/_participant_card.html.twig' with {
'cardData': cardsData[loop.index0],
'cardData': bookingEditContext.cardsData[loop.index0],
'index': loop.index0,
'participantNumber': loop.index,
'mode': 'edit',
'bookingId': bookingData.id,
'bookingId': bookingEditContext.bookingData.id,
'isCanceled': isCanceled,
'isSubmitted': isSubmitted,
'isSubmitted': bookingEditContext.isSubmitted,
} %}
{% endfor %}
</div>
@@ -85,17 +85,17 @@
{# Fixed footer #}
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
<div class="flex justify-between" hx-disinherit="*">
<a href="{{ isDirty ? path('app_booking_edit_cancel', {id: bookingData.id}) : path('app_bookings') }}"
<a href="{{ bookingEditContext.isDirty ? path('app_booking_edit_cancel', {id: bookingEditContext.bookingData.id}) : path('app_bookings') }}"
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-8 h-8 md:w-10 md:h-10">
<svg class="w-6 h-6 md:w-8 md:h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
</a>
{% if isDirty %}
{% if bookingEditContext.isDirty %}
<button type="submit"
{% if hasValidationErrors|default(false) %}
{% if bookingEditContext.hasValidationErrors %}
disabled
title="Bitte prüfe zuerst deine Eingaben"
{% endif %}
class="button button--primary {{ hasValidationErrors|default(false) ? 'opacity-50 cursor-not-allowed' : '' }}">
class="button button--primary {{ bookingEditContext.hasValidationErrors ? 'opacity-50 cursor-not-allowed' : '' }}">
Buchung aktualisieren
</button>
{% endif %}
+12 -5
View File
@@ -12,9 +12,9 @@
<div id="booking-summary"
class="hidden lg:block lg:order-2 lg:flex-1 lg:min-h-0 lg:col-span-2">
{% include 'booking/_summary.html.twig' with {
'bookingCreateDto': bookingDto,
'summaryData': summaryData,
'mutableData': mutableData|default(null)
'bookingDto': bookingEditContext.bookingDto,
'summaryData': bookingEditContext.summaryData,
'mutableData': bookingEditContext.mutableData|default(null)
} %}
</div>
@@ -27,13 +27,20 @@
<div id="main-content" class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
{# Flash messages - must be inside main-content for HTMX swap to display them #}
{% include '_partials/_flashes.html.twig' %}
{% include 'booking/_participant_form.html.twig' %}
{% include 'booking/_participant_form.html.twig' with {
'bookingEditContext': bookingEditContext,
'participantIndex': participantIndex,
'refreshRouteName': refreshRouteName,
'refreshRouteParams': refreshRouteParams|default({}),
'cancelRouteName': cancelRouteName|default('app_booking_edit'),
'cancelRouteParams': cancelRouteParams|default({id: bookingEditContext.bookingData.id})
} %}
</div>
</div>
</div>
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
<div class="flex justify-between">
<a href="{{ path('app_booking_edit', {id: bookingDto.booking.id}) }}"
<a href="{{ path('app_booking_edit', {id: bookingEditContext.bookingData.id}) }}"
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-8 h-8 md:w-10 md:h-10"
{{ qa_attribute('btn-cancel') }}>
<svg class="w-6 h-6 md:w-8 md:h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>