feat: implement screendesign
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
{% import _self as macros %}
|
||||
|
||||
{# 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 %}
|
||||
@@ -27,384 +25,519 @@
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{# Specialized macro for checkbox fields (like rental insurance) that need manual fieldset wrapping #}
|
||||
{% macro checkbox_field(form, fieldName, label, options = {}, undefinedLabel = 'Nicht wählbar') %}
|
||||
{% if form[fieldName] is defined %}
|
||||
<fieldset class="mb-1">
|
||||
<legend class="font-semibold mb-1">{{ label }}</legend>
|
||||
{{ form_row(form[fieldName], options) }}
|
||||
</fieldset>
|
||||
{% else %}
|
||||
<fieldset class="mb-1">
|
||||
<legend class="font-semibold mb-1">{{ label }}</legend>
|
||||
{% if undefinedLabel %}
|
||||
<div class="text-sm text-gray-500">{{ undefinedLabel }}</div>
|
||||
{# 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 %}
|
||||
{{ price | format_currency('EUR') }}
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% 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 %}
|
||||
{{ 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 %}
|
||||
{{ 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 %}
|
||||
{% import _self as macros %}
|
||||
<div id="participant-form-view">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="flex-shrink-0"
|
||||
{{ stimulus_controller('gravatar', {
|
||||
url: gravatar_url(form.vars.data.participant.email ?? ''),
|
||||
alt: participantIndex == 0 ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1)
|
||||
}, {
|
||||
image: 'w-16 h-16 rounded-full'
|
||||
}) }}>
|
||||
{{ icon('user', 'w-16 h-16 text-gray-400') }}
|
||||
{% 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>
|
||||
<h2 class="text-2xl">{{ participantIndex == 0 ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1) }}</h2>
|
||||
</div>
|
||||
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'novalidate': 'novalidate',
|
||||
'hx-post': path(submitRouteName, submitRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content',
|
||||
'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
<div id="participant-form" class="space-y-4">
|
||||
{# Personal data section #}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{ 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) }}
|
||||
<div id="participant-form" class="space-y-4">
|
||||
|
||||
{{ 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'
|
||||
}
|
||||
}
|
||||
) }}
|
||||
{% from '_partials/_validation_errors.html.twig' import validation_alert %}
|
||||
{{ validation_alert(form) }}
|
||||
|
||||
{% 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) }}
|
||||
{# Eligibility checks #}
|
||||
{% set participantData = form.vars.data.participant %}
|
||||
{% set hasDateOfBirth = participantData and participantData.dateOfBirth %}
|
||||
{% set isEligible = hasDateOfBirth and is_participant_eligible(bookingDto, participantIndex) %}
|
||||
|
||||
{{ macros.field_or_static(form, 'nationality', 'Nationalität', form.vars.data.participant.nationality|map_nationality, bookingDto, participantIndex) }}
|
||||
</div>
|
||||
{% 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 #}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{ 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 %}
|
||||
{# Address field is in form - render as editable form fields #}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{ 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) %}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{% set assignedRoom = bookingDto.travel.getRoomById(form.vars.data.participant.assignedRoomId) %}
|
||||
{% set roomLabel = assignedRoom ? assignedRoom.label : 'Kein Zimmer zugewiesen' %}
|
||||
{{ 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>
|
||||
{% endif %}
|
||||
|
||||
{# 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 %}
|
||||
<div class="my-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-blue-600 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<p class="text-sm text-blue-800">
|
||||
Leistungen sind erst nach Angabe des Geburtsdatums buchbar
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elseif not isEligible %}
|
||||
<div class="my-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-red-600 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<p class="text-sm text-red-800">
|
||||
Buchung wegen des Alters von Teilnehmer:in {{ participantIndex + 1 }} nicht möglich
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Service selection #}
|
||||
{% if isEligible %}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{ 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'
|
||||
}
|
||||
}) }}
|
||||
|
||||
<div>
|
||||
{{ macros.service_field(form, 'rentals', 'Leihmaterial', {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}, 'Bitte zuerst den Skipass auswählen') }}
|
||||
|
||||
{{ macros.checkbox_field(form, 'rentalInsurance', null, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}, null) }}
|
||||
</div>
|
||||
|
||||
{% if form.bodyDimensions is defined %}
|
||||
<div class="grid gap-y-4">
|
||||
{{ form_row(form.bodyDimensions.height) }}
|
||||
{{ form_row(form.bodyDimensions.shoeSize) }}
|
||||
{{ form_row(form.bodyDimensions.weight) }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div>{# empty slot to maintain grid layout #}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-span-2">
|
||||
{# Insurance display for edit mode (read-only) #}
|
||||
{% if bookingDto.mode == constant('App\\Form\\Model\\BookingDto::MODE_EDIT') and form.vars.data.participant.insurance %}
|
||||
<fieldset class="mb-1">
|
||||
<legend class="font-semibold mb-1">Reiseversicherung</legend>
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ form.vars.data.participant.insurance.label }}
|
||||
{% if form.vars.data.participant.insurance.price and form.vars.data.participant.insurance.price > 0 %}
|
||||
<span class="text-gray-500">(€{{ form.vars.data.participant.insurance.price|number_format(2, ',', '.') }})</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{# Insurance field OR assigned insurance display for dependent participants (create mode) #}
|
||||
{% else %}
|
||||
{% set showBulkInsurance = participantIndex > 0 and bookingDto.participants[0].bulkInsuranceBooking %}
|
||||
|
||||
{% if showBulkInsurance %}
|
||||
<fieldset class="mb-1">
|
||||
<legend class="font-semibold mb-1">Reiseversicherung</legend>
|
||||
<div class="text-sm text-gray-600">
|
||||
{% set applicantInsurance = bookingDto.participants[0].insurance %}
|
||||
{% if applicantInsurance %}
|
||||
{{ applicantInsurance.label }}
|
||||
{% if applicantInsurance.price and applicantInsurance.price > 0 %}
|
||||
<span class="text-gray-500">(€{{ applicantInsurance.price|number_format(2, ',', '.') }})</span>
|
||||
{% endif %}
|
||||
<span class="italic text-gray-500 ml-2">– wie Anmelder</span>
|
||||
{% else %}
|
||||
<span class="italic">wie Anmelder</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
{% else %}
|
||||
<fieldset class="mb-1">
|
||||
<legend class="font-semibold mb-1">Reiseversicherung</legend>
|
||||
|
||||
{# Bulk insurance booking checkbox (applicant only) #}
|
||||
{% if form.bulkInsuranceBooking is defined %}
|
||||
{{ form_row(form.bulkInsuranceBooking) }}
|
||||
{% endif %}
|
||||
|
||||
{% if form.insurance is defined %}
|
||||
{{ form_row(form.insurance, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
},
|
||||
'label': false
|
||||
}) }}
|
||||
{% else %}
|
||||
<div class="text-sm text-gray-500">Nicht wählbar</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{# Transportation Services Section #}
|
||||
<div class="mt-6 border-t pt-4">
|
||||
<h3 class="font-semibold text-lg mb-4">Anreise</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{% if form.transportationOutbound is defined %}
|
||||
{{ form_row(form.transportationOutbound, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'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': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
{% endif %}
|
||||
{% if form.pickup is defined %}
|
||||
<div class="col-span-2 mt-4">
|
||||
{{ form_row(form.pickup, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.parking is defined %}
|
||||
<div class="mt-4">
|
||||
{{ form_row(form.parking, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path(refreshRouteName, refreshRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content', 'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.licensePlate is defined %}
|
||||
<div class="mt-4">
|
||||
{{ form_row(form.licensePlate) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Voucher fields - available for all participants regardless of eligibility #}
|
||||
{% if form.purchaseVoucherCode is defined or form.promoVoucherCode is defined %}
|
||||
<div class="mt-6 border-t pt-4">
|
||||
<h3 class="font-semibold text-lg mb-4">Gutscheine</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{% if form.purchaseVoucherCode is defined %}
|
||||
{{ form_row(form.purchaseVoucherCode) }}
|
||||
{% endif %}
|
||||
{% if form.promoVoucherCode is defined %}
|
||||
{{ form_row(form.promoVoucherCode) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ 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>
|
||||
|
||||
<div class="flex justify-between mt-8">
|
||||
{% if cancelRouteName is defined %}
|
||||
<a href="{{ path(cancelRouteName, cancelRouteParams|default({})) }}"
|
||||
class="button bg-button bg-button--secondary"
|
||||
data-action="click->loading#show"
|
||||
{{ qa_attribute('btn-cancel') }}>
|
||||
Abbrechen
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ path('app_booking_create_step_2') }}"
|
||||
class="button bg-button bg-button--secondary"
|
||||
data-action="click->loading#show"
|
||||
{{ qa_attribute('btn-cancel') }}>
|
||||
Abbrechen
|
||||
</a>
|
||||
{# 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 %}
|
||||
<button type="submit" class="button bg-button bg-button--secondary" {{ qa_attribute('btn-submit') }}>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{# 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' %}
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
{# Service selection #}
|
||||
{% if isEligible %}
|
||||
{{ 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>
|
||||
{% else %}
|
||||
<div class="p-2 text-sm text-gray-500">Bitte zuerst den Skipass auswählen</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">
|
||||
{{ form_row(form.bodyDimensions.height) }}
|
||||
{{ form_row(form.bodyDimensions.shoeSize) }}
|
||||
{{ form_row(form.bodyDimensions.weight) }}
|
||||
</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 %}
|
||||
|
||||
{# 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 %}
|
||||
<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="true">
|
||||
<div id="booking-summary" hx-swap-oob="innerHTML">
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData,
|
||||
|
||||
Reference in New Issue
Block a user