feat: split booking between participants and applicant

addresses #869f2q1vu
This commit is contained in:
fromme committed 2026-10-19 09:18:22 +02:00
1 parent 2d49212b08
commit 1ff293f41f
44 files changed
+2024 -316

No files matched your search

@@ -0,0 +1,63 @@
{# Applicant block, shown only when the applicant does not travel along. #}
{% set isSubmitted = isSubmitted|default(false) %}
{% set applicantErrors = applicantErrors|default([]) %}
{% set isComplete = applicant is not null
and applicant.firstName is not empty
and applicant.lastName is not empty
and applicant.email is not empty
and applicant.mobile is not empty
and applicant.dateOfBirth is not null
and applicant.address is not null %}
{% set showAsError = applicantErrors is not empty or (not isComplete and isSubmitted) %}
<div class="mt-4 border-t border-gray-200 pt-4" id="applicant-card">
<div class="font-bold pb-2">Anmelder:in</div>
{% if applicantErrors is not empty %}
<ul class="mb-2 list-disc pl-5 text-sm text-red-500" {{ qa_attribute('applicant-errors') }}>
{% for message in applicantErrors %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% if is_granted('IS_AUTHENTICATED_FULLY') %}
{# Logged in: the data comes from the account. Editing it here would create a duplicate
BusPro record, which is the same reason participant[0] is static when they travel. #}
<div class="text-sm leading-5" {{ qa_attribute('applicant-static') }}>
{% if applicant is not null and applicant.firstName is not empty %}
<div>{{ applicant.firstName }} {{ applicant.lastName }}</div>
{% if applicant.address is not null %}
<div>{{ applicant.address.street }}</div>
<div>{{ applicant.address.postCode }} {{ applicant.address.city }}</div>
{% endif %}
<div>{{ applicant.email }}</div>
{% if applicant.mobile is not empty %}<div>{{ applicant.mobile }}</div>{% endif %}
{% else %}
<div class="italic text-gray-600">Deine Daten werden aus deinem Konto übernommen.</div>
{% endif %}
</div>
{% else %}
<div class="flex items-start justify-between gap-4">
<div class="{{ html_classes('text-sm leading-5', { 'italic': not isComplete }) }}"
{{ qa_attribute('applicant-summary') }}>
{% if isComplete %}
<div>{{ applicant.firstName }} {{ applicant.lastName }}</div>
<div>{{ applicant.address.postCode }} {{ applicant.address.city }}</div>
<div>{{ applicant.email }}</div>
{% elseif showAsError %}
<div class="font-medium text-red-500" {{ qa_attribute('applicant-invalid') }}>
unvollständige/fehlerhafte Angaben
</div>
{% else %}
<div class="text-gray-600">Bitte gib deine Kontaktdaten an.</div>
{% endif %}
</div>
<a href="{{ path('app_booking_create_applicant') }}"
class="button button--secondary shrink-0"
{{ qa_attribute('btn-edit-applicant') }}>
{{ isComplete ? 'Bearbeiten' : 'Angeben' }}
</a>
</div>
{% endif %}
</div>
@@ -0,0 +1,80 @@
{% extends 'layout_booking.html.twig' %}
{% block content %}
{{ form_start(form, {
'attr': {
'class': 'flex-1 flex flex-col min-h-0',
'novalidate': 'novalidate'
}
}) }}
{# Pagination - mobile only (above form) #}
<div class="lg:hidden">
{% include 'booking/_pagination.html.twig' with { 'current_step': 2 } %}
</div>
<div class="flex-1 flex flex-col lg:grid lg:grid-cols-5 min-h-0 relative">
{# Summary - hidden on mobile, visible on desktop #}
<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 {
'bookingDto': bookingCreateContext.bookingDto,
'summaryData': bookingCreateContext.summaryData
} %}
</div>
{# Form area - full width mobile, 3 cols desktop #}
<div class="flex-1 order-1 lg:col-span-3 bg-white flex flex-col min-h-0">
{# Pagination - desktop only (fixed above scrollable content) #}
<div class="hidden lg:block shrink-0">
{% include 'booking/_pagination.html.twig' with { 'current_step': 2 } %}
</div>
<div id="main-content" class="flex-1 overflow-y-auto scroll-stable 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' %}
<h2 class="pb-2">Anmelder:in</h2>
<p class="pb-4 text-sm leading-5">
Du reist nicht selbst mit. Bitte gib deine Kontaktdaten an &ndash; wir
verwenden sie für die Buchungsbestätigung und für Rückfragen zur Reise.
</p>
{% if form.vars.submitted and not form.vars.valid %}
{% include '_partials/_alert.html.twig' with {
level: 'error',
title: 'Bitte überprüfe deine Angaben',
messages: ['Einige Angaben fehlen noch oder sind fehlerhaft.']
} %}
{% endif %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{{ form_row(form.firstName) }}
{{ form_row(form.lastName) }}
{{ form_row(form.gender) }}
{{ form_row(form.dateOfBirth) }}
{{ form_row(form.nationality) }}
</div>
<div class="pt-4">
{{ form_row(form.address) }}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4">
{{ form_row(form.email) }}
{{ form_row(form.mobile) }}
</div>
</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_create_step_2') }}"
class="button button--secondary">
Zurück
</a>
<button type="submit" class="button button--primary" {{ qa_attribute('btn-submit') }}>
Speichern
</button>
</div>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}
+37 -5
View File
@@ -36,12 +36,44 @@
</h2>
{# Display form-level validation errors #}
{% set applicantErrors = applicantErrors|default([]) %}
{% if form.vars.submitted and not form.vars.valid %}
{% include '_partials/_alert.html.twig' with {
level: 'error',
title: 'Bitte überprüfe die Teilnehmerdaten',
messages: ['Einige Teilnehmer haben noch unvollständige oder fehlerhafte Angaben. Bitte bearbeite die markierten Teilnehmer.']
} %}
{% if applicantErrors is not empty %}
{% include '_partials/_alert.html.twig' with {
level: 'error',
title: 'Bitte überprüfe die Angaben zur anmeldenden Person',
messages: ['Die Angaben zur anmeldenden Person sind noch unvollständig oder fehlerhaft.']
} %}
{% else %}
{% include '_partials/_alert.html.twig' with {
level: 'error',
title: 'Bitte überprüfe die Teilnehmerdaten',
messages: ['Einige Teilnehmer haben noch unvollständige oder fehlerhafte Angaben. Bitte bearbeite die markierten Teilnehmer.']
} %}
{% endif %}
{% endif %}
{% set bookingDto = bookingCreateContext.bookingDto %}
{% if not bookingDto.isInternalAgencyBooking() %}
{# No form_start here: this sits inside the step 2 form and nested
form tags are invalid. htmx posts the inputs enclosed below,
including the CSRF token, to the toggle route. #}
<div class="mb-6 rounded border border-gray-200 bg-white p-4"
hx-post="{{ path('app_booking_create_step_2_applicant_toggle') }}"
hx-trigger="change"
hx-include="this"
{{ qa_attribute('applicant-travels') }}>
{{ form_row(applicantTravelsForm.applicantTravels) }}
{{ form_rest(applicantTravelsForm) }}
{% if not bookingDto.applicantTravels %}
{% include 'booking/create/_applicant_card.html.twig' with {
'applicant': bookingDto.applicant,
'isSubmitted': bookingCreateContext.isSubmitted,
'applicantErrors': applicantErrors
} %}
{% endif %}
</div>
{% endif %}
<div class="divide-y divide-gray-200">
+39 -1
View File
@@ -116,6 +116,44 @@
</div>
{% endif %}
{% if not bookingCreateContext.bookingDto.applicantTravels
and bookingCreateContext.bookingDto.applicant is not null %}
{% set applicant = bookingCreateContext.bookingDto.applicant %}
<h3 class="text-xl font-semibold mb-4">
Anmelder:in
</h3>
<div class="border border-primary-bg mb-4" {{ qa_attribute('summary-applicant') }}>
<div class="p-2 bg-primary-bg uppercase font-semibold">
{{ applicant.firstName }} {{ applicant.lastName }}
<span class="text-sm font-normal normal-case">(reist nicht mit)</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2 p-2 text-sm">
{% if applicant.dateOfBirth %}
<div>
<span class="text-gray-600 font-semibold">Geburtsdatum:</span>
{{ applicant.dateOfBirth|date('d.m.Y') }}
</div>
{% endif %}
{% if applicant.address is not null %}
<div>
<span class="text-gray-600 font-semibold">Adresse:</span>
{{ applicant.address.street }}, {{ applicant.address.postCode }} {{ applicant.address.city }}
</div>
{% endif %}
<div>
<span class="text-gray-600 font-semibold">E-Mail:</span>
{{ applicant.email }}
</div>
{% if applicant.mobile %}
<div>
<span class="text-gray-600 font-semibold">Telefon (mobil):</span>
{{ applicant.mobile }}
</div>
{% endif %}
</div>
</div>
{% endif %}
{# Participants Summary #}
<h3 class="text-xl font-semibold mb-4">
Teilnehmer
@@ -126,7 +164,7 @@
<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 bookingCreateContext.bookingDto.isInternalAgencyBooking() %}<span class="text-sm font-normal">(Anmelder:in)</span>{% endif %}
{% if loop.first and bookingCreateContext.bookingDto.isFirstParticipantApplicant() %}<span class="text-sm font-normal">(Anmelder:in)</span>{% endif %}
</span>
{% if bookingCreateContext.participantPrices is defined and bookingCreateContext.participantPrices[loop.index0] is defined %}
<span>{{ bookingCreateContext.participantPrices[loop.index0]|format_currency('EUR') }}</span>