feat: split booking between participants and applicant
addresses #869f2q1vu
This commit is contained in:
1 parent
2d49212b08
commit
1ff293f41f
44 files changed
+2024
-316
No files matched your search
@@ -161,7 +161,7 @@
|
||||
{{ participantIndex + 1 }}
|
||||
</div>
|
||||
<div class="text-2xl">
|
||||
{{ participantIndex == 0 and not bookingDto.isInternalAgencyBooking() ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1) }}
|
||||
{{ participantIndex == 0 and bookingDto.isFirstParticipantApplicant() ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -506,7 +506,7 @@
|
||||
<div class="pb-4">
|
||||
<fieldset class="border border-primary-bg">
|
||||
<legend class="w-full bg-primary-bg p-2 font-semibold uppercase">
|
||||
Reiseversicherung* <span class="font-normal normal-case text-sm text-gray-600 italic">– wie {{ bookingDto.isInternalAgencyBooking() ? 'Teilnehmer:in 1' : 'Anmelder:in' }}</span>
|
||||
Reiseversicherung* <span class="font-normal normal-case text-sm text-gray-600 italic">– wie {{ bookingDto.isFirstParticipantApplicant() ? 'Anmelder:in' : 'Teilnehmer:in 1' }}</span>
|
||||
</legend>
|
||||
{% if applicantInsurance %}
|
||||
<table class="w-full table-fixed border-collapse">
|
||||
@@ -541,7 +541,7 @@
|
||||
{% else %}
|
||||
<table class="w-full table-fixed border-collapse">
|
||||
<tr>
|
||||
<td class="border border-primary-bg p-2 align-top text-sm text-gray-500 italic">wie {{ bookingDto.isInternalAgencyBooking() ? 'Teilnehmer:in 1' : 'Anmelder:in' }}</td>
|
||||
<td class="border border-primary-bg p-2 align-top text-sm text-gray-500 italic">wie {{ bookingDto.isFirstParticipantApplicant() ? 'Anmelder:in' : 'Teilnehmer:in 1' }}</td>
|
||||
<td class="border border-primary-bg p-2 align-top w-24"></td>
|
||||
<td class="border border-primary-bg p-2 align-top w-12"></td>
|
||||
</tr>
|
||||
|
||||
@@ -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 – 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 %}
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user