{# Macro to render a form field or static value based on field state #} {% macro field_or_static(form, fieldName, label, staticValue, bookingDto, participantIndex, options = {}) %} {% if form[fieldName] is defined %} {# Field is in form structure - render normally as form input #} {{ form_row(form[fieldName], options) }} {% elseif is_static_text(fieldName, bookingDto, participantIndex) %} {# Field excluded from form - render as static text in fieldset/table style #}
{{ label }}
{{ staticValue ?: '-' }}
{% endif %} {# If neither condition is true, don't render anything (truly hidden) #} {% endmacro %} {# Macro to render a service field with consistent fieldset/table structure #} {% macro service_field(form, fieldName, label, options = {}, undefinedLabel = 'Nicht wählbar') %} {% if form[fieldName] is defined %} {{ form_row(form[fieldName], options) }} {% else %}
{{ label }}
{{ undefinedLabel }}
{% endif %} {% endmacro %} {# Macro to render a service choice table row with optional price and tooltip #} {% macro service_choice_row(child, price, tooltipText, tooltipLabel, widgetAttr = {}, showIncludedLabel = true) %} {% set isReadonly = child.vars.attr.readonly is defined %}
{{ child.vars.label }}
{%- if tooltipText is not null -%}
{%- endif -%} {{ price | format_service_price(showIncludedLabel) }} {% set childTooltip = child.vars.attr['data-tooltip']|default(null) %}
{{ form_widget(child, { 'attr': widgetAttr }) }}
{% endmacro %} {# Macro to render a checkbox table row (for trailing checkboxes like rental insurance, parking, bulk insurance) #} {% macro checkbox_row(checkboxField, label, price, tooltipText, tooltipLabel, widgetAttr = {}, showPrice = true) %} {% set isReadonly = checkboxField.vars.attr.readonly is defined %}
{{ label }}
{%- if tooltipText is not null -%}
{%- endif -%} {{ price | format_service_price(showPrice) }} {% set fieldTooltip = checkboxField.vars.attr['data-tooltip']|default(null) %}
{{ form_widget(checkboxField, { 'attr': widgetAttr }) }}
{% endmacro %} {# Macro to render insurance choice table row with product info links #} {% macro insurance_choice_row(child, choiceData, widgetAttr = {}) %} {% set isReadonly = child.vars.attr.readonly is defined %} {% set isNoInsurance = choiceData.noInsurance|default(false) %} {% set price = choiceData.price|default(null) %} {% set urlsProductInfo = choiceData ? choiceData.getAllUrlsProductInfo() : [] %}
{{ child.vars.label }}
{%- if urlsProductInfo is not empty -%}
{%- endif -%} {{ price | format_service_price(not isNoInsurance) }} {% set childTooltip = child.vars.attr['data-tooltip']|default(null) %} {% if isReadonly or childTooltip is not null %} {{ form_widget(child, { 'attr': widgetAttr }) }} {% else %} {{ form_widget(child, { 'attr': widgetAttr }) }} {% endif %} {% endmacro %} {# Shared by booking create and edit flows; callers should pass one of the flow contexts. #} {% set bookingFlowContext = bookingFlowContext|default(bookingEditContext|default(bookingCreateContext|default(null))) %} {% set bookingDto = bookingDto|default(bookingFlowContext ? bookingFlowContext.bookingDto : null) %} {% set summaryData = summaryData|default(bookingFlowContext ? bookingFlowContext.summaryData : null) %} {% set mutableData = mutableData|default(bookingFlowContext and bookingFlowContext.mutableData is defined ? bookingFlowContext.mutableData : null) %} {# Standalone participant form view (replaces main content area) #} {% block participant_form %} {% form_theme form 'booking/_form_theme.html.twig' %} {% import _self as macros %}
{{ participantIndex + 1 }}
{{ participantIndex == 0 and not bookingDto.isInternalAgencyBooking() ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1) }}
{% from '_partials/_validation_errors.html.twig' import validation_alert %} {{ validation_alert(form) }} {# Eligibility checks #} {% set participantData = form.vars.data.participant %} {% set hasDateOfBirth = participantData and participantData.dateOfBirth %} {% set isEligible = hasDateOfBirth and is_participant_eligible(bookingDto, participantIndex) %} {% if not hasDateOfBirth %} {% include '_partials/_alert.html.twig' with { level: 'info', messages: ['Leistungen sind erst nach Angabe des Geburtsdatums buchbar'] } %} {% elseif not isEligible %} {% include '_partials/_alert.html.twig' with { level: 'error', messages: ['Buchung wegen des Alters von Teilnehmer:in ' ~ (participantIndex + 1) ~ ' nicht möglich'] } %} {% endif %} {# Personal data and address section #} {% set isPersonalDataStatic = is_static_text('firstName', bookingDto, participantIndex) %} {% set isAddressStatic = is_static_text('address', bookingDto, participantIndex) %} {% if isPersonalDataStatic and isAddressStatic %} {# Both personal data and address are static - render in two-column grid with tables #} {% set participant = form.vars.data.participant %} {% set addressSource = (0 == participantIndex and bookingDto.booking) ? bookingDto.booking.applicant : bookingDto.participants[participantIndex] %} {% set genderLabel = participant.gender == 'M' ? 'männlich' : (participant.gender == 'W' ? 'weiblich' : (participant.gender == 'D' ? 'divers' : null)) %}
{# Personal data #}
Persönliche Daten
Name: {{ participant.firstName }} {{ participant.lastName }}
Geburtsdatum: {{ participant.dateOfBirth ? participant.dateOfBirth|date('d.m.Y') : '-' }}
Geschlecht: {{ genderLabel ?: '-' }}
Nationalität: {{ participant.nationality ? participant.nationality|map_nationality : '-' }}
E-Mail: {{ participant.email ?: '-' }}
Telefon: {{ participant.mobile ?: '-' }}
{# Address #}
Adresse
{% if addressSource.address %} {% if addressSource.address.street %}{{ addressSource.address.street }}
{% endif %} {% if addressSource.address.postCode or addressSource.address.city %} {{ addressSource.address.postCode }} {{ addressSource.address.city }}
{% endif %} {% if addressSource.address.country %}{{ addressSource.address.country|map_country }}{% endif %} {% else %} - {% endif %}
{% else %} {# Editable form fields for personal data #}
{{ macros.field_or_static(form, 'firstName', 'Vorname', form.vars.data.participant.firstName, bookingDto, participantIndex) }} {{ macros.field_or_static(form, 'lastName', 'Nachname', form.vars.data.participant.lastName, bookingDto, participantIndex) }} {{ macros.field_or_static( form, 'dateOfBirth', 'Geburtsdatum', form.vars.data.participant.dateOfBirth ? form.vars.data.participant.dateOfBirth|date('d.m.Y') : null, bookingDto, participantIndex, { 'attr': { 'hx-trigger': htmx_change_trigger, 'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})), 'hx-target': '#main-content', 'hx-swap': 'innerHTML' } } ) }} {% set genderLabel = form.vars.data.participant.gender == 'M' ? 'männlich' : (form.vars.data.participant.gender == 'W' ? 'weiblich' : (form.vars.data.participant.gender == 'D' ? 'divers' : null)) %} {{ macros.field_or_static(form, 'gender', 'Geschlecht', genderLabel, bookingDto, participantIndex) }} {{ macros.field_or_static(form, 'nationality', 'Nationalität', form.vars.data.participant.nationality|map_nationality, bookingDto, participantIndex) }} {# Contact information #} {{ macros.field_or_static(form, 'email', 'E-Mail', form.vars.data.participant.email, bookingDto, participantIndex) }} {{ macros.field_or_static(form, 'mobile', 'Telefon (mobil)', form.vars.data.participant.mobile, bookingDto, participantIndex) }}
{# Address #} {% if form.address is defined %}
{# Address field is in form - render as editable form fields #} {{ form_row(form.address.street) }} {{ form_row(form.address.postCode) }} {{ form_row(form.address.city) }} {{ form_row(form.address.country) }}
{% endif %} {% endif %} {# Room assignment - hidden until date of birth is provided, or shown as static text in edit mode #} {% if form.assignedRoomId is defined or is_static_text('assignedRoomId', bookingDto, participantIndex) %}
{% set assignedRoom = bookingDto.travel.getRoomById(form.vars.data.participant.assignedRoomId) %} {% set roomLabel = assignedRoom ? assignedRoom.label : 'Kein Zimmer zugewiesen' %}

Unterkunft

{{ macros.field_or_static(form, 'assignedRoomId', 'Zimmer', roomLabel, bookingDto, participantIndex, { 'attr': { 'hx-trigger': htmx_change_trigger, 'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})), 'hx-target': '#main-content', 'hx-swap': 'innerHTML' } }) }} {% if form.remarksRoom is defined %} {{ form_row(form.remarksRoom) }} {% endif %}
{% endif %} {# Service selection #} {% if isEligible %}

Leistungen

{{ macros.service_field(form, 'skiPass', 'Skipass', { 'attr': { 'hx-trigger': htmx_change_trigger, 'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})), 'hx-target': '#main-content', 'hx-swap': 'innerHTML' } }) }} {{ macros.service_field(form, 'courses', 'Kurse', { 'attr': { 'hx-trigger': htmx_change_trigger, 'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})), 'hx-target': '#main-content', 'hx-swap': 'innerHTML' } }) }} {{ macros.service_field(form, 'additionalServices', 'Zusatzleistungen', { 'attr': { 'hx-trigger': htmx_change_trigger, 'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})), 'hx-target': '#main-content', 'hx-swap': 'innerHTML' } }) }} {# Board and VEG fields combined in one fieldset #} {% if form.board is defined or form.veg is defined %}
Verpflegung {% if form.board is defined %} {{ form_widget(form.board, { 'attr': { 'hx-trigger': htmx_change_trigger, 'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})), 'hx-target': '#main-content', 'hx-swap': 'innerHTML' } }) }} {% endif %} {% if form.veg is defined %}
{{ form_widget(form.veg, { 'attr': { 'hx-trigger': htmx_change_trigger, 'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})), 'hx-target': '#main-content', 'hx-swap': 'innerHTML' } }) }} {{ form_errors(form.veg) }}
{% endif %}
{% if form.board is defined %} {{ form_errors(form.board) }} {% endif %}
Bitte teile uns eventuelle Unverträglichkeiten an {{ customer_service_email }} (inkl. der Buchungsnummer) mit.
{% else %}
Verpflegung
Nicht wählbar
{% endif %} {% set htmxAttr = { 'hx-trigger': htmx_change_trigger, 'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})), 'hx-target': '#main-content', 'hx-swap': 'innerHTML' } %}
Leihmaterial {% if form.rentals is defined and form.rentals.vars.required %}*{% endif %} {% if form.rentals is defined %} {% for child in form.rentals %} {% set choiceData = form.rentals.vars.choices[loop.index0].data %} {{ macros.service_choice_row(child, choiceData.price|default(null), child.vars.attr['data-description']|default(null), 'Details', htmxAttr) }} {% endfor %} {% if form.rentalInsurance is defined %} {% set rentalInsuranceService = bookingDto.travel.getAdditionalServicesBySubTypes(constant('App\\BusProNet\\Constants::TOKEN_RENTAL_INSURANCE'), true)|first %} {{ macros.checkbox_row( form.rentalInsurance, constant('App\\BusProNet\\Constants::SERVICE_LABELS')[constant('App\\BusProNet\\Constants::TOKEN_RENTAL_INSURANCE')], rentalInsuranceService.price|default(null), form.rentalInsurance.vars.help|default(null), 'Info', htmxAttr ) }} {% endif %}
{% elseif form.skiPass is defined and not form.vars.data.participant.skiPass %} {# Skipass field exists but no skipass selected yet #}
Bitte zuerst den Skipass auswählen
{% else %} {# No skipass field or skipass selected but no matching rentals #}
Nicht wählbar
{% endif %}
{% if form.rentals is defined %} {{ form_errors(form.rentals) }} {{ form_help(form.rentals) }} {% if is_travel_start_cutoff_reached(bookingDto) %}
Online nicht mehr buchbar. Bitte direkt über die Hausleitung vor Ort anfragen (vorbehaltlich Verfügbarkeit)
{% endif %} {% endif %}
{% if form.bodyDimensions is defined %}
{{ form_row(form.bodyDimensions.height) }} {{ form_row(form.bodyDimensions.shoeSize) }} {{ form_row(form.bodyDimensions.weight) }}
{{ form.bodyDimensions.vars.help }}
{% endif %} {# Insurance display for edit mode (read-only) - only show if insurance is selected #} {% if bookingDto.mode == constant('App\\Form\\Model\\BookingDto::MODE_EDIT') %} {% if form.vars.data.participant.insurance %} {% set editInsurance = form.vars.data.participant.insurance %}
Reiseversicherung
{{ editInsurance.label }}
{%- if editInsurance.getAllUrlsProductInfo() is not empty -%}
{%- endif -%}
{% if editInsurance.price and editInsurance.price > 0 %} {{ editInsurance.price | format_currency('EUR') }} {% endif %}
{% endif %} {# In edit mode without insurance: don't show the section at all #} {# Insurance field OR assigned insurance display for dependent participants (create mode) #} {% else %} {# Dependents get the applicant's read-only insurance display instead of their own choice when bulk insurance booking is active, or when the applicant selected a family insurance (which always covers the whole family, regardless of the bulk checkbox) #} {% set showApplicantInsurance = participantIndex > 0 and ( bookingDto.participants[0].bulkInsuranceBooking or (bookingDto.participants[0].insurance and bookingDto.participants[0].insurance.familyInsurance) ) %} {% if showApplicantInsurance %} {% set applicantInsurance = bookingDto.participants[0].insurance %}
Reiseversicherung* – wie {{ bookingDto.isInternalAgencyBooking() ? 'Teilnehmer:in 1' : 'Anmelder:in' }} {% if applicantInsurance %}
{{ applicantInsurance.label }}
{%- if applicantInsurance.getAllUrlsProductInfo() is not empty -%}
{%- endif -%}
{% if applicantInsurance.price and applicantInsurance.price > 0 %} {{ applicantInsurance.price | format_currency('EUR') }} {% endif %}
{% else %}
wie {{ bookingDto.isInternalAgencyBooking() ? 'Teilnehmer:in 1' : 'Anmelder:in' }}
{% endif %}
{% else %}
{# The applicant has to choose an insurance before the dependents' dates of birth are known, so a family insurance cannot be offered at that point. Once the constellation qualifies, this standing hint points them back at the option. #} {% if participantIndex == 0 and bookingCreateContext is defined and bookingCreateContext.familyInsuranceUpgradeAvailable %} {% include '_partials/_alert.html.twig' with { level: 'info', messages: ['Für eure Konstellation ist auch eine Familienversicherung verfügbar und kann über die anmeldende Person gebucht werden.'] } %} {% endif %}
Reiseversicherung* {% if form.insurance is defined %} {% if form.bulkInsuranceBooking is defined %} {{ macros.checkbox_row( form.bulkInsuranceBooking, form.bulkInsuranceBooking.vars.label, null, form.bulkInsuranceBooking.vars.attr['data-description']|default(null), 'Info', htmxAttr, false ) }} {% endif %} {% for child in form.insurance %} {% set choiceData = form.insurance.vars.choices[loop.index0].data %} {{ macros.insurance_choice_row(child, choiceData, htmxAttr) }} {% endfor %}
{% else %}
Nicht wählbar
{% endif %}
{% if form.insurance is defined %} {{ form_errors(form.insurance) }} {{ form_help(form.insurance) }}
Eine Versicherung kann nur abgeschlossen werden, wenn Dein Wohnsitz in Deutschland ist.
{% endif %}
{% endif %} {% endif %} {# Transportation Services Section #}

Anreise

{% if form.transportationOutbound is defined %}
{{ form.transportationOutbound.vars.label }} {% if form.transportationOutbound.vars.required %}*{% endif %} {% for child in form.transportationOutbound %} {% set choiceData = form.transportationOutbound.vars.choices[loop.index0].data %} {% set isPkw = choiceData.subType is defined and choiceData.subType in ['PKW', 'CAR'] %} {{ macros.service_choice_row(child, choiceData.price|default(null), child.vars.attr['data-description']|default(null), 'Details', htmxAttr, not isPkw) }} {% endfor %} {% if form.parking is defined %} {% set parkingService = bookingDto.travel.getAdditionalServicesBySubTypes(constant('App\\BusProNet\\Constants::TOKEN_PARKING'))|first %} {{ macros.checkbox_row( form.parking, parkingService.label|default('Parkplatz'), parkingService.price|default(null), form.parking.vars.attr['data-description']|default(null), 'Info', htmxAttr, false ) }} {% endif %}
{{ form_errors(form.transportationOutbound) }} {{ form_help(form.transportationOutbound) }}
{% endif %} {% if form.pickup is defined %} {{ form_row(form.pickup, { 'attr': { 'hx-trigger': htmx_change_trigger, 'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})), 'hx-target': '#main-content', 'hx-swap': 'innerHTML' } }) }} {% endif %}
{% if form.transportationInbound is defined %} {{ form_row(form.transportationInbound, { 'attr': { 'hx-trigger': htmx_change_trigger, 'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})), 'hx-target': '#main-content', 'hx-swap': 'innerHTML' } }) }} {% endif %} {% if form.differentDropOff is defined %}
{{ form_row(form.differentDropOff, { 'attr': { 'hx-trigger': htmx_change_trigger, 'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})), 'hx-target': '#main-content', 'hx-swap': 'innerHTML' } }) }}
{% endif %} {% if form.dropOff is defined %} {{ form_row(form.dropOff, { 'attr': { 'hx-trigger': htmx_change_trigger, 'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})), 'hx-target': '#main-content', 'hx-swap': 'innerHTML' } }) }} {% endif %}
{% if form.licensePlate is defined %} {{ form_row(form.licensePlate) }} {% endif %} {% endif %} {# Voucher fields - available for all participants regardless of eligibility #} {% if form.purchaseVoucherCode is defined or form.promoVoucherCode is defined %}

Gutscheine

{% if form.purchaseVoucherCode is defined %} {{ form_row(form.purchaseVoucherCode) }} {% endif %} {% if form.promoVoucherCode is defined %} {{ form_row(form.promoVoucherCode) }} {% endif %}
{% endif %}
{% endblock %} {# Sidebar summary with conditional OOB swap - only render for HTMX requests #} {% if htmx_oob_swap|default(false) %} {% block booking_summary %}
{% include 'booking/_summary.html.twig' with { 'bookingDto': bookingDto, 'summaryData': summaryData, 'mutableData': mutableData|default(null) } %}
{% endblock %} {% endif %}