Files
myep/templates/booking/_participant_form.html.twig
T

569 lines
31 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{# 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 but should render as static text #}
<div class="mb-1">
<label class="font-semibold block">{{ label }}</label>
<div class="text-sm text-gray-600">{{ staticValue ?: '-' }}</div>
</div>
{% 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 %}
<div class="pb-4">
<fieldset class="border border-primary-bg">
<legend class="w-full bg-primary-bg p-2 font-semibold uppercase">{{ label }}</legend>
<div class="p-2 text-sm text-gray-500">{{ undefinedLabel }}</div>
</fieldset>
</div>
{% endif %}
{% endmacro %}
{# Macro to render a service choice table row with optional price and tooltip #}
{% macro service_choice_row(child, price, tooltipText, tooltipLabel, widgetAttr = {}) %}
{% set isReadonly = child.vars.attr.readonly is defined %}
<tr>
<td class="border border-primary-bg p-2 align-top">
<div>{{ child.vars.label }}</div>
{%- if tooltipText is not null -%}
<div {{ stimulus_controller('tooltip') }}>
<button type="button" data-tooltip-target="trigger" class="text-sm text-primary-light">
{{ tooltipLabel|default('Details') }}
</button>
<template data-tooltip-target="template">
<div class="text-sm">{{ tooltipText|raw }}</div>
</template>
</div>
{%- endif -%}
</td>
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
{% if price is not null %}
{{ price | format_currency('EUR') }}
{% endif %}
</td>
<td class="border border-primary-bg p-2 align-top w-12 text-center">
{% set childTooltip = child.vars.attr['data-tooltip']|default(null) %}
<div class="{{ html_classes({ 'cursor-not-allowed': isReadonly }) }}"{% if childTooltip is not null %} {{ stimulus_controller('tooltip', { 'content': childTooltip }) }}{% endif %}>
<div class="{{ isReadonly ? 'pointer-events-none' : '' }}">
{{ form_widget(child, { 'attr': widgetAttr }) }}
</div>
</div>
</td>
</tr>
{% 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 = {}) %}
{% set isReadonly = checkboxField.vars.attr.readonly is defined %}
<tr>
<td class="border border-primary-bg p-2 align-top">
<div>{{ label }}</div>
{%- if tooltipText is not null -%}
<div {{ stimulus_controller('tooltip') }}>
<button type="button" data-tooltip-target="trigger" class="text-sm text-primary-light">
{{ tooltipLabel|default('Info') }}
</button>
<template data-tooltip-target="template">
<div class="text-sm">{{ tooltipText|raw }}</div>
</template>
</div>
{%- endif -%}
</td>
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
{% if price is not null %}
{{ price | format_currency('EUR') }}
{% endif %}
</td>
<td class="border border-primary-bg p-2 align-top w-12 text-center">
{% set fieldTooltip = checkboxField.vars.attr['data-tooltip']|default(null) %}
<div class="{{ html_classes({ 'cursor-not-allowed': isReadonly }) }}"{% if fieldTooltip is not null %} {{ stimulus_controller('tooltip', { 'content': fieldTooltip }) }}{% endif %}>
<div class="{{ isReadonly ? 'pointer-events-none' : '' }}">
{{ form_widget(checkboxField, { 'attr': widgetAttr }) }}
</div>
</div>
</td>
</tr>
{% endmacro %}
{# Macro to render insurance choice table row with product info links #}
{% macro insurance_choice_row(child, price, urlsProductInfo, widgetAttr = {}) %}
{% set isReadonly = child.vars.attr.readonly is defined %}
<tr>
<td class="border border-primary-bg p-2 align-top">
<div>{{ child.vars.label }}</div>
{%- if urlsProductInfo is not empty -%}
<div {{ stimulus_controller('tooltip') }}>
<button type="button" data-tooltip-target="trigger" class="text-sm text-primary-light">
Details
</button>
<template data-tooltip-target="template">
<div class="text-sm">
{%- for url in urlsProductInfo -%}
<a href="{{ url }}" target="_blank" rel="noopener noreferrer" class="block">
Produktinformation {{ urlsProductInfo|length > 1 ? loop.index : '' }}
</a>
{%- endfor -%}
</div>
</template>
</div>
{%- endif -%}
</td>
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
{% if price is not null %}
{{ price | format_currency('EUR') }}
{% endif %}
</td>
<td class="border border-primary-bg p-2 align-top w-12 text-center">
{% set childTooltip = child.vars.attr['data-tooltip']|default(null) %}
<div class="{{ html_classes({ 'cursor-not-allowed': isReadonly }) }}"{% if childTooltip is not null %} {{ stimulus_controller('tooltip', { 'content': childTooltip }) }}{% endif %}>
<div class="{{ isReadonly ? 'pointer-events-none' : '' }}">
{{ form_widget(child, { 'attr': widgetAttr }) }}
</div>
</div>
</td>
</tr>
{% endmacro %}
{# Standalone participant form view (replaces main content area) #}
{% block participant_form %}
{% form_theme form 'booking/_form_theme.html.twig' %}
{% import _self as macros %}
<div class="flex items-center gap-4 pb-4 mb-4 border-b border-primary-bg">
<div class="w-12 h-12 inline-flex flex-shrink-0 items-center justify-center rounded-full bg-primary-dark text-white text-2xl font-bold">
{{ participantIndex + 1 }}
</div>
<div class="text-2xl">
{{ participantIndex == 0 ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1) }}
</div>
</div>
<div id="participant-form" class="space-y-4" hx-disinherit="hx-boost">
{% 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 section #}
<div class="grid lg:grid-cols-2 lg:gap-x-8">
{{ 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': 'change',
'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) }}
</div>
{# Address #}
{% if form.address is defined %}
<div class="grid lg:grid-cols-2 lg:gap-x-8">
{# 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) }}
</div>
{% elseif is_static_text('address', bookingDto, participantIndex) %}
{# Address excluded from form but should render as static text #}
{# For applicant (index 0), use booking.applicant.address which has full data from BPN #}
{# For other participants, use participants[index].address (may only have country in BPN response) #}
{% set addressSource = (0 == participantIndex and bookingDto.booking) ? bookingDto.booking.applicant : bookingDto.participants[participantIndex] %}
<div>
<label class="font-semibold mb-1 block">Adresse</label>
{% if addressSource.address %}
<div class="text-sm text-gray-600">
{% if addressSource.address.street %}{{ addressSource.address.street }}<br>{% endif %}
{% if addressSource.address.postCode or addressSource.address.city %}
{{ addressSource.address.postCode }} {{ addressSource.address.city }}<br>
{% endif %}
{% if addressSource.address.country %}{{ addressSource.address.country|map_country }}{% endif %}
</div>
{% else %}
<div class="text-sm text-gray-600">-</div>
{% endif %}
</div>
{% 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' %}
<hr>
<h3 class="font-semibold text-lg mb-4">
Unterkunft
</h3>
<div class="grid lg:grid-cols-2 lg:gap-x-8">
{{ macros.field_or_static(form, 'assignedRoomId', 'Zimmer', roomLabel, bookingDto, participantIndex, {
'attr': {
'hx-trigger': 'change',
'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 %}
</div>
<hr>
{% endif %}
{# Service selection #}
{% if isEligible %}
<h3 class="font-semibold text-lg mb-4">
Leistungen
</h3>
{{ macros.service_field(form, 'skiPass', 'Skipass', {
'attr': {
'hx-trigger': 'change',
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
}
}) }}
{{ macros.service_field(form, 'courses', 'Kurse', {
'attr': {
'hx-trigger': 'change',
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
}
}) }}
{{ macros.service_field(form, 'additionalServices', 'Zusatzleistungen', {
'attr': {
'hx-trigger': 'change',
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
}
}) }}
{{ macros.service_field(form, 'board', 'Verpflegung', {
'attr': {
'hx-trigger': 'change',
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
}
}) }}
{% set htmxAttr = {
'hx-trigger': 'change',
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
'hx-target': '#main-content',
'hx-swap': 'innerHTML'
} %}
<div class="pb-4">
<fieldset class="border border-primary-bg">
<legend class="{{ html_classes('w-full bg-primary-bg p-2 font-semibold uppercase', { 'text-red-500': form.rentals is defined and not form.rentals.vars.valid }) }}">
Leihmaterial
{% if form.rentals is defined and form.rentals.vars.required %}<span>*</span>{% endif %}
</legend>
{% if form.rentals is defined %}
<table class="w-full table-fixed border-collapse">
{% 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, 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 %}
</table>
{% elseif form.skiPass is defined %}
{# Skipass field exists but no rentals yet - user needs to select skipass first #}
<div class="p-2 text-sm text-gray-500">Bitte zuerst den Skipass auswählen</div>
{% else %}
{# No skipass field (e.g., baby participant) - rentals not available #}
<div class="p-2 text-sm text-gray-500">Nicht wählbar</div>
{% endif %}
</fieldset>
{% if form.rentals is defined %}
{{ form_errors(form.rentals) }}
{{ form_help(form.rentals) }}
{% endif %}
</div>
{% if form.bodyDimensions is defined %}
<div class="grid lg:grid-cols-3 lg:gap-x-4 pb-4">
{{ form_row(form.bodyDimensions.height) }}
{{ form_row(form.bodyDimensions.shoeSize) }}
{{ form_row(form.bodyDimensions.weight) }}
{{ form_help(form.bodyDimensions) }}
</div>
{% 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 %}
<div class="pb-4">
<fieldset class="border border-primary-bg">
<legend class="w-full bg-primary-bg p-2 font-semibold uppercase">
Reiseversicherung
</legend>
<table class="w-full table-fixed border-collapse">
<tr>
<td class="border border-primary-bg p-2 align-top">
<div>{{ editInsurance.label }}</div>
{%- if editInsurance.getAllUrlsProductInfo() is not empty -%}
<div {{ stimulus_controller('tooltip') }}>
<button type="button" data-tooltip-target="trigger" class="text-sm text-primary-light">
Details
</button>
<template data-tooltip-target="template">
<div class="text-sm">
{%- for url in editInsurance.getAllUrlsProductInfo() -%}
<a href="{{ url }}" target="_blank" rel="noopener noreferrer" class="block">
Produktinformation {{ editInsurance.getAllUrlsProductInfo()|length > 1 ? loop.index : '' }}
</a>
{%- endfor -%}
</div>
</template>
</div>
{%- endif -%}
</td>
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
{% if editInsurance.price and editInsurance.price > 0 %}
{{ editInsurance.price | format_currency('EUR') }}
{% endif %}
</td>
<td class="border border-primary-bg p-2 align-top w-12"></td>
</tr>
</table>
</fieldset>
</div>
{% 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 %}
{% set showBulkInsurance = participantIndex > 0 and bookingDto.participants[0].bulkInsuranceBooking %}
{% if showBulkInsurance %}
{% set applicantInsurance = bookingDto.participants[0].insurance %}
<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 Anmelder</span>
</legend>
{% if applicantInsurance %}
<table class="w-full table-fixed border-collapse">
<tr>
<td class="border border-primary-bg p-2 align-top">
<div>{{ applicantInsurance.label }}</div>
{%- if applicantInsurance.getAllUrlsProductInfo() is not empty -%}
<div {{ stimulus_controller('tooltip') }}>
<button type="button" data-tooltip-target="trigger" class="text-sm text-primary-light">
Details
</button>
<template data-tooltip-target="template">
<div class="text-sm">
{%- for url in applicantInsurance.getAllUrlsProductInfo() -%}
<a href="{{ url }}" target="_blank" rel="noopener noreferrer" class="block">
Produktinformation {{ applicantInsurance.getAllUrlsProductInfo()|length > 1 ? loop.index : '' }}
</a>
{%- endfor -%}
</div>
</template>
</div>
{%- endif -%}
</td>
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
{% if applicantInsurance.price and applicantInsurance.price > 0 %}
{{ applicantInsurance.price | format_currency('EUR') }}
{% endif %}
</td>
<td class="border border-primary-bg p-2 align-top w-12"></td>
</tr>
</table>
{% 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 Anmelder</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>
</table>
{% endif %}
</fieldset>
</div>
{% else %}
<div class="pb-4">
<fieldset class="border border-primary-bg">
<legend class="{{ html_classes('w-full bg-primary-bg p-2 font-semibold uppercase', { 'text-red-500': form.insurance is defined and not form.insurance.vars.valid }) }}">
Reiseversicherung*
</legend>
{% if form.insurance is defined %}
<table class="w-full table-fixed border-collapse">
{% 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
) }}
{% endif %}
{% for child in form.insurance %}
{% set choiceData = form.insurance.vars.choices[loop.index0].data %}
{{ macros.insurance_choice_row(child, choiceData.price|default(null), choiceData ? choiceData.getAllUrlsProductInfo() : [], htmxAttr) }}
{% endfor %}
</table>
{% else %}
<div class="p-2 text-sm text-gray-500">Nicht wählbar</div>
{% endif %}
</fieldset>
{% if form.insurance is defined %}
{{ form_errors(form.insurance) }}
{{ form_help(form.insurance) }}
{% endif %}
</div>
{% endif %}
{% endif %}
<hr>
{# Transportation Services Section #}
<h3 class="font-semibold text-lg mb-4">
Anreise
</h3>
{% if form.transportationOutbound is defined %}
<div class="pb-4">
<fieldset class="border border-primary-bg">
<legend class="{{ html_classes('w-full bg-primary-bg p-2 font-semibold uppercase', { 'text-red-500': not form.transportationOutbound.vars.valid }) }}">
{{ form.transportationOutbound.vars.label }}
{% if form.transportationOutbound.vars.required %}<span>*</span>{% endif %}
</legend>
<table class="w-full table-fixed border-collapse">
{% for child in form.transportationOutbound %}
{% set choiceData = form.transportationOutbound.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.parking is defined %}
{% set parkingService = bookingDto.travel.getAdditionalServicesBySubTypes(constant('App\\BusProNet\\Constants::TOKEN_PARKING'), true)|first %}
{{ macros.checkbox_row(
form.parking,
'Parkplatz',
parkingService.price|default(null),
null,
null,
htmxAttr
) }}
{% endif %}
</table>
</fieldset>
{{ form_errors(form.transportationOutbound) }}
{{ form_help(form.transportationOutbound) }}
</div>
{% endif %}
{% if form.transportationInbound is defined %}
{{ form_row(form.transportationInbound, {
'attr': {
'hx-trigger': 'change',
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
}
}) }}
{% endif %}
{% if form.pickup is defined %}
{{ form_row(form.pickup, {
'attr': {
'hx-trigger': 'change',
'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 %}
<hr>
<h3 class="font-semibold text-lg mb-4">
Gutscheine
</h3>
<div class="grid lg:grid-cols-2 lg:gap-x-4">
{% if form.purchaseVoucherCode is defined %}
{{ form_row(form.purchaseVoucherCode) }}
{% endif %}
{% if form.promoVoucherCode is defined %}
{{ form_row(form.promoVoucherCode) }}
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}
{# Sidebar summary with conditional OOB swap - only render for HTMX requests #}
{% if htmx_oob_swap|default(false) %}
{% block booking_summary %}
<div id="booking-summary" hx-swap-oob="innerHTML">
{% include 'booking/_summary.html.twig' with {
'bookingCreateDto': bookingDto,
'summaryData': summaryData,
'mutableData': mutableData|default(null)
} %}
</div>
{% endblock %}
{% endif %}