feat: implement screendesign
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
<div class="mt-8 pt-4 border-t border-gray-200 text-center">
|
||||
<a href="{{ path('app_booking_cancel') }}" class="text-sm text-gray-600 hover:text-gray-900 hover:underline">
|
||||
Buchung abbrechen und zur Startseite zurückkehren
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,75 @@
|
||||
{%- block form_row -%}
|
||||
{%- set row_attr = row_attr|merge({ 'class': (row_attr.class|default('') ~ ' mb-4')|trim }) -%}
|
||||
{%- set widget_attr = {} -%}
|
||||
{%- if help is not empty -%}
|
||||
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
|
||||
{%- endif -%}
|
||||
{%- if form.vars.expanded is defined and form.vars.expanded -%}
|
||||
<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.vars.errors | length > 0 }) }}">
|
||||
{{ label }}
|
||||
{%- if required -%}<span>*</span>{%- endif -%}
|
||||
</legend>
|
||||
{{- form_widget(form, widget_attr) -}}
|
||||
</fieldset>
|
||||
{{- form_errors(form) -}}
|
||||
{{- form_help(form) -}}
|
||||
</div>
|
||||
{%- else -%}
|
||||
<div{% with {attr: row_attr} %}{{ block('attributes') }}{% endwith %}>
|
||||
{{- form_label(form) -}}
|
||||
{{- form_widget(form, widget_attr) -}}
|
||||
{{- form_errors(form) -}}
|
||||
{{- form_help(form) -}}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{%- endblock form_row -%}
|
||||
|
||||
{%- block choice_widget_expanded -%}
|
||||
<table class="w-full table-fixed border-collapse">
|
||||
{%- for child in form %}
|
||||
{% set choiceData = form.vars.choices[loop.index0].data %}
|
||||
{% set description = child.vars.attr['data-description']|default(null) %}
|
||||
{% set isReadonly = child.vars.attr.readonly is defined %}
|
||||
{%- set child_attr = {} -%}
|
||||
{%- if attr['hx-trigger'] is defined -%}
|
||||
{%- set child_attr = {
|
||||
'hx-trigger': attr['hx-trigger'],
|
||||
'hx-post': attr['hx-post'],
|
||||
'hx-swap': attr['hx-swap']
|
||||
} -%}
|
||||
{%- endif -%}
|
||||
<tr>
|
||||
<td class="border border-primary-bg p-2 align-top">
|
||||
<div>{{- child.vars.label -}}</div>
|
||||
{%- if description is not null -%}
|
||||
<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">{{ description|raw }}</div>
|
||||
</template>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
</td>
|
||||
{% if choiceData %}
|
||||
<td class="border border-primary-bg p-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{% if choiceData.price %}
|
||||
{{ choiceData.price | format_currency('EUR') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<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': child_attr }) -}}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor -%}
|
||||
</table>
|
||||
{%- endblock choice_widget_expanded -%}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% set current_step = current_step ?? 1 %}
|
||||
{% set step_routes = {
|
||||
1: 'app_booking_create_step_1',
|
||||
2: 'app_booking_create_step_2',
|
||||
3: 'app_booking_create_step_3',
|
||||
4: 'app_booking_create_step_4'
|
||||
} %}
|
||||
<nav class="pagination">
|
||||
{% for step in 1..4 %}
|
||||
<div class="pagination-item">
|
||||
{% if step < current_step %}
|
||||
<a href="{{ path(step_routes[step]) }}"
|
||||
class="pagination-item__border" {{ stimulus_action('loading', 'toggle') }}>
|
||||
<span class="pagination-item__inner bg-primary-bg text-gray-800">
|
||||
{{ step }}
|
||||
</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="pagination-item__border">
|
||||
<span class="{{ html_classes('pagination-item__inner', {
|
||||
'bg-primary-light text-white': step == current_step,
|
||||
'bg-primary-bg text-gray-800': step != current_step
|
||||
}) }}">
|
||||
{{ step }}
|
||||
</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
@@ -4,66 +4,44 @@
|
||||
{% set errorMessages = cardData.errorMessages|default([]) %}
|
||||
{% set mode = mode|default('create') %}
|
||||
|
||||
<div id="participant-card-{{ index }}"
|
||||
class="{{ html_classes('border rounded p-4', { 'border-gray-400 bg-gray-50': isCanceled, 'border-red-700': not isValid }) }}">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0"
|
||||
{{ stimulus_controller('gravatar', {
|
||||
url: gravatar_url(cardData.email),
|
||||
alt: cardData.name
|
||||
}, {
|
||||
image: 'w-12 h-12 rounded-full'
|
||||
}) }}
|
||||
{{ qa_attribute('participant-avatar', index) }}>
|
||||
{{ icon('user', 'w-12 h-12 text-gray-400') }}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-semibold {{ isCanceled ? 'text-gray-600' : '' }}" {{ qa_attribute('participant-name', index) }}>
|
||||
{{ cardData.name }}
|
||||
</h3>
|
||||
{% if isCanceled %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-700 text-white" {{ qa_attribute('participant-canceled', index) }}>
|
||||
storniert
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if not isValid %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-700 text-white" {{ qa_attribute('participant-invalid', index) }}>
|
||||
unvollständige oder fehlerhafte Daten
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="text-sm text-gray-600" {{ qa_attribute('participant-room-name', index) }}>{{ cardData.roomName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-4" id="participant-card-{{ index }}">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="{{ html_classes('w-12 h-12 inline-flex flex-shrink-0 items-center justify-center rounded-full text-white text-2xl font-bold', { 'bg-primary-dark': isValid, 'bg-red-500': not isValid }) }}">
|
||||
{{ participantNumber }}
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="font-medium {{ isCanceled ? 'text-gray-500' : '' }}" {{ qa_attribute('participant-price', index) }}>{{ cardData.price }}</span>
|
||||
<div class="flex-1 leading-5">
|
||||
<div class="font-semibold mb-1">
|
||||
{{ cardData.name }}
|
||||
</div>
|
||||
{% if isCanceled %}
|
||||
<button type="button"
|
||||
class="button bg-button bg-button--secondary opacity-50 cursor-not-allowed"
|
||||
disabled
|
||||
title="Stornierte Teilnehmer können nicht bearbeitet werden">
|
||||
Bearbeiten
|
||||
</button>
|
||||
<div class="text-xs font-medium text-gray-700" {{ qa_attribute('participant-canceled', index) }}>
|
||||
storniert
|
||||
</div>
|
||||
{% elseif not isValid %}
|
||||
<div class="text-xs font-medium text-red-500" {{ qa_attribute('participant-invalid', index) }}>
|
||||
unvollständige/fehlerhafte Daten
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-gray-600" {{ qa_attribute('participant-room-name', index) }}>
|
||||
{{ cardData.roomName }}
|
||||
<br>
|
||||
{{ cardData.price }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex flex-col justify-center">
|
||||
{% if not isCanceled %}
|
||||
{% if mode == 'edit' %}
|
||||
<a href="{{ path('app_booking_edit_participant', {id: bookingId, index: index}) }}"
|
||||
class="button bg-button bg-button--secondary"
|
||||
data-action="click->loading#show"
|
||||
{{ qa_attribute('btn-edit-participant', index) }}>
|
||||
Bearbeiten
|
||||
</a>
|
||||
{% set url = path('app_booking_edit_participant', {id: bookingId, index: index}) %}
|
||||
{% else %}
|
||||
<a href="{{ path('app_booking_create_step_2_participant', {index: index}) }}"
|
||||
class="button bg-button bg-button--secondary"
|
||||
data-action="click->loading#show"
|
||||
{{ qa_attribute('btn-edit-participant', index) }}>
|
||||
Bearbeiten
|
||||
</a>
|
||||
{% set url = path('app_booking_create_step_2_participant', {index: index}) %}
|
||||
{% endif %}
|
||||
<a href="{{ url }}"
|
||||
class="button button--small button--primary"
|
||||
data-action="click->loading#show"
|
||||
{{ qa_attribute('btn-edit-participant', index) }}>
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M92.69,216H48a8,8,0,0,1-8-8V163.31a8,8,0,0,1,2.34-5.65L165.66,34.34a8,8,0,0,1,11.31,0L221.66,79a8,8,0,0,1,0,11.31L98.34,213.66A8,8,0,0,1,92.69,216Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="136" y1="64" x2="192" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="164" y1="92" x2="68" y2="188" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="95.49" y1="215.49" x2="40.51" y2="160.51" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,193 +1,240 @@
|
||||
<div class="booking-summary p-8 bg-gray-50 rounded-lg border sticky top-4">
|
||||
<h3 class="font-bold text-xl mb-6 text-gray-800">Buchungsübersicht</h3>
|
||||
|
||||
{# Inquiry booking notification #}
|
||||
{% if bookingCreateDto is defined and bookingCreateDto.isInquiryBooking() %}
|
||||
<div class="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded text-sm text-yellow-800">
|
||||
<span class="font-semibold">Anfragebuchung:</span> Diese Reise ist ausgebucht. Ihre Buchung wird als Anfrage verarbeitet.
|
||||
<div class="bg-white h-full flex flex-col lg:border-l lg:border-primary-bg">
|
||||
{# Header - always visible #}
|
||||
<div class="shrink-0 px-4 lg:px-8 py-4 lg:flex lg:flex-col lg:justify-center shadow-md relative z-10">
|
||||
<div class="flex items-center justify-between pb-2">
|
||||
<span class="block uppercase font-semibold">Gesamtpreis</span>
|
||||
<span class="font-semibold">{{ summaryData.payableAmount|format_currency('EUR') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Mutability information (edit mode only) #}
|
||||
{% if bookingCreateDto.mode == constant('App\\Form\\Model\\BookingDto::MODE_EDIT') and mutableData %}
|
||||
<div class="mb-6 pb-4 border-b border-gray-200">
|
||||
<h4 class="font-semibold text-lg mb-3 text-gray-800">Aktualisierung möglich bis</h4>
|
||||
<ul class="space-y-1 text-sm text-gray-600">
|
||||
<li class="flex items-start">
|
||||
<span class="mr-2">•</span>
|
||||
<span>
|
||||
<span class="font-medium">Zusatzleistungen:</span>
|
||||
{% if mutableData.items['additional_services'].mutable %}
|
||||
{{ mutableData.items['additional_services'].mutableBefore|date('d.m.Y') }}
|
||||
{% else %}
|
||||
<span class="text-red-600">nicht mehr möglich</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="mr-2">•</span>
|
||||
<span>
|
||||
<span class="font-medium">Beförderung:</span>
|
||||
{% if mutableData.items['transportation'].mutable %}
|
||||
{{ mutableData.items['transportation'].mutableBefore|date('d.m.Y') }}
|
||||
{% else %}
|
||||
<span class="text-red-600">nicht mehr möglich</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="mr-2">•</span>
|
||||
<span>
|
||||
<span class="font-medium">Zustiege:</span>
|
||||
{% if mutableData.items['pickup'].mutable %}
|
||||
{{ mutableData.items['pickup'].mutableBefore|date('d.m.Y') }}
|
||||
{% else %}
|
||||
<span class="text-red-600">nicht mehr möglich</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Travel Information #}
|
||||
<div class="mb-6 pb-4 border-b border-gray-200">
|
||||
{% if summaryData.cmsData.hotel.images is defined %}
|
||||
<img src="{{ summaryData.cmsData.hotel.images.resized.l[0].url }}" alt="{{ summaryData.cmsData.hotel.images.resized.s[0].alt }}" class="w-full rounded mb-4">
|
||||
{% endif %}
|
||||
<div class="space-y-2 text-sm text-gray-600">
|
||||
<div><span class="font-medium">Reise:</span> {{ bookingCreateDto.travel.label }}</div>
|
||||
{% if summaryData.cmsData.region is defined %}
|
||||
<div><span class="font-medium">Gebiet:</span> {{ summaryData.cmsData.region.name }}</div>
|
||||
{% endif %}
|
||||
{% if summaryData.cmsData.country is defined %}
|
||||
<div><span class="font-medium">Land:</span> {{ summaryData.cmsData.country.name }}</div>
|
||||
{% endif %}
|
||||
<div><span class="font-medium">Zeitraum:</span> {{ bookingCreateDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo|date('d.m.Y') }}</div>
|
||||
<div><span class="font-medium">Unterkunft:</span> {{ bookingCreateDto.travel.hotel.name }}{% if summaryData.cmsData.hotel.address is defined %}, {{ summaryData.cmsData.hotel.address | replace({"\n": ', '}) }}{% endif %}</div>
|
||||
<div><span class="font-medium">Anzahl Teilnehmer:</span> {{ summaryData.participantCount }}</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="block uppercase font-semibold">Buchungsübersicht</span>
|
||||
<button type="button"
|
||||
class="lg:hidden button button--small bg-primary-light"
|
||||
data-action="toggle#toggle">
|
||||
<svg class="w-4 h-4 text-white transition-transform"
|
||||
data-toggle-target="icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="208 96 128 176 48 96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Rooms Section #}
|
||||
{% if summaryData.pricingData.rooms is not empty %}
|
||||
<div class="mb-6 pb-4 border-b border-gray-200">
|
||||
<h4 class="font-semibold text-lg mb-3 text-gray-800">Unterkunft</h4>
|
||||
<div class="space-y-2">
|
||||
{% for roomPricing in summaryData.pricingData.rooms %}
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="text-sm text-gray-600">
|
||||
<span class="font-medium">{{ roomPricing.quantity }}x {{ roomPricing.label }}</span>
|
||||
{% if summaryData.assignmentCounts[roomPricing.roomId] is defined %}
|
||||
<span class="block text-xs text-gray-500">{{ summaryData.assignmentCounts[roomPricing.roomId] }} belegt</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-gray-900">
|
||||
€{{ roomPricing.totalPrice|number_format(2, ',', '.') }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
€{{ roomPricing.unitPrice|number_format(2, ',', '.') }} pro Person
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{# Content - toggleable on mobile, always visible on desktop #}
|
||||
<div class="hidden lg:block flex-1 min-h-0 bg-white shadow-lg lg:shadow-none overflow-y-auto px-4 lg:px-8 py-8"
|
||||
data-toggle-target="content">
|
||||
{# Travel Information #}
|
||||
<div class="pb-4">
|
||||
<table>
|
||||
<tr>
|
||||
<th class="text-left align-top border-r-2 border-gray-300 pr-2">
|
||||
Reise
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ bookingCreateDto.travel.label }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-left align-top border-r-2 border-gray-300 pr-2">
|
||||
Zeitraum
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ bookingCreateDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo|date('d.m.Y') }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-left align-top border-r-2 border-gray-300 pr-2">
|
||||
Unterkunft
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ bookingCreateDto.travel.hotel.name }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-left align-top border-r-2 border-gray-300 pr-2">
|
||||
Teilnehmer
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ summaryData.participantCount }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Services Section #}
|
||||
{% if summaryData.pricingData.services is not empty %}
|
||||
<div class="mb-6">
|
||||
<h4 class="font-semibold text-lg mb-3 text-gray-800">Leistungen</h4>
|
||||
{% for serviceGroup in summaryData.pricingData.services %}
|
||||
<div class="mb-4 last:mb-0">
|
||||
<h5 class="font-medium text-sm text-gray-700 mb-2 uppercase tracking-wide">{{ serviceGroup.groupName }}</h5>
|
||||
<div class="space-y-1 ml-4">
|
||||
{% for servicePricing in serviceGroup.services %}
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="text-gray-600">
|
||||
{{ servicePricing.participantCount }}x {{ servicePricing.label }}
|
||||
</span>
|
||||
<span class="font-medium text-gray-900">
|
||||
€{{ servicePricing.totalPrice|number_format(2, ',', '.') }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% include 'booking/_summary_hotel.html.twig' %}
|
||||
|
||||
{# Inquiry booking notification #}
|
||||
{% if bookingCreateDto is defined and bookingCreateDto.isInquiryBooking() %}
|
||||
<div>
|
||||
Deine Buchung wird als Anfrage verarbeitet.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Mutability information (edit mode only) #}
|
||||
{% if bookingCreateDto.mode == constant('App\\Form\\Model\\BookingDto::MODE_EDIT') and mutableData %}
|
||||
<div class="border border-primary-bg mb-4">
|
||||
<div class="p-2 bg-primary-bg uppercase font-semibold">
|
||||
Aktualisierung möglich bis
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<table class="w-full table-fixed">
|
||||
<tr>
|
||||
<td class="p-2 align-top">
|
||||
Zusatzleistungen
|
||||
</td>
|
||||
<td class="p-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{% if mutableData.items['additional_services'].mutable %}
|
||||
{{ mutableData.items['additional_services'].mutableBefore|date('d.m.Y') }}
|
||||
{% else %}
|
||||
nicht mehr möglich
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 align-top">
|
||||
Beförderung
|
||||
</td>
|
||||
<td class="p-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{% if mutableData.items['transportation'].mutable %}
|
||||
{{ mutableData.items['transportation'].mutableBefore|date('d.m.Y') }}
|
||||
{% else %}
|
||||
nicht mehr möglich
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 align-top">
|
||||
Zustiege
|
||||
</td>
|
||||
<td class="p-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{% if mutableData.items['pickup'].mutable %}
|
||||
{{ mutableData.items['pickup'].mutableBefore|date('d.m.Y') }}
|
||||
{% else %}
|
||||
nicht mehr möglich
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Surcharges Section (Edit mode only) #}
|
||||
{% if summaryData.pricingData.surcharges is defined and summaryData.pricingData.surcharges is not empty %}
|
||||
<div class="mb-6 pb-4 border-b border-gray-200">
|
||||
<h4 class="font-semibold text-lg mb-3 text-gray-800">Zuschläge</h4>
|
||||
<div class="space-y-1">
|
||||
{% for surchargePricing in summaryData.pricingData.surcharges %}
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="text-gray-600">
|
||||
{{ surchargePricing.participantCount }}x {{ surchargePricing.label }}
|
||||
</span>
|
||||
<span class="font-medium text-gray-900">
|
||||
€{{ surchargePricing.totalPrice|number_format(2, ',', '.') }}
|
||||
</span>
|
||||
{# Rooms Section #}
|
||||
{% if summaryData.pricingData.rooms is not empty %}
|
||||
<div class="border border-primary-bg mb-4">
|
||||
<div class="p-2 bg-primary-bg uppercase font-semibold">
|
||||
Unterkunft
|
||||
</div>
|
||||
<table class="w-full table-fixed">
|
||||
{% for roomPricing in summaryData.pricingData.rooms %}
|
||||
<tr>
|
||||
<td class="p-2 align-top">
|
||||
<div>{{ roomPricing.quantity }}x {{ roomPricing.label }}</div>
|
||||
{% if summaryData.assignmentCounts[roomPricing.roomId] is defined %}
|
||||
<div class="text-sm text-gray-600">{{ summaryData.assignmentCounts[roomPricing.roomId] }}x Erwachsener</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="p-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{{ roomPricing.totalPrice | format_currency('EUR') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Services Section #}
|
||||
{% if summaryData.pricingData.services is not empty %}
|
||||
<div class="border border-primary-bg mb-4">
|
||||
<div class="p-2 bg-primary-bg uppercase font-semibold">
|
||||
Leistungen
|
||||
</div>
|
||||
{% for serviceGroup in summaryData.pricingData.services %}
|
||||
<div class="p-2 text-primary-dark/70 uppercase font-semibold">
|
||||
{{ serviceGroup.groupName }}
|
||||
</div>
|
||||
<table class="w-full table-fixed">
|
||||
{% for servicePricing in serviceGroup.services %}
|
||||
<tr>
|
||||
<td class="px-2 pb-2 align-top">
|
||||
{{ servicePricing.participantCount }}x {{ servicePricing.label }}
|
||||
</td>
|
||||
<td class="px-2 pb-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{{ servicePricing.totalPrice | format_currency('EUR') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# Total Section #}
|
||||
{% if summaryData.pricingData.grandTotal is defined and summaryData.pricingData.grandTotal > 0 %}
|
||||
<div class="pt-4 border-t-2 border-gray-300">
|
||||
{# Surcharges Section (Edit mode only) #}
|
||||
{% if summaryData.pricingData.surcharges is defined and summaryData.pricingData.surcharges is not empty %}
|
||||
<div class="border border-primary-bg mb-4">
|
||||
<div class="p-2 bg-primary-bg uppercase font-semibold">
|
||||
Zu-/Abschläge
|
||||
</div>
|
||||
<table class="w-full table-fixed">
|
||||
{% for surchargePricing in summaryData.pricingData.surcharges %}
|
||||
<tr>
|
||||
<td class="px-2 pb-2 align-top">
|
||||
{{ surchargePricing.participantCount }}x {{ surchargePricing.label }}
|
||||
</td>
|
||||
<td class="px-2 pb-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{{ surchargePricing.totalPrice|format_currency('EUR') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Total Section #}
|
||||
{% if summaryData.totalPrice > 0 %}
|
||||
{# Show subtotal and voucher discounts when vouchers are applied #}
|
||||
{% set acceptedVouchers = bookingCreateDto.getAcceptedVouchers() %}
|
||||
{% if acceptedVouchers and acceptedVouchers.hasVouchers() %}
|
||||
{# Subtotal before vouchers #}
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-gray-600">Gesamtpreis:</span>
|
||||
<span class="text-gray-900">
|
||||
€{{ summaryData.pricingData.grandTotal|number_format(2, ',', '.') }}
|
||||
</span>
|
||||
<div class="border border-primary-bg mb-4">
|
||||
<div class="p-2 bg-primary-bg uppercase font-semibold">
|
||||
Kostenübersicht
|
||||
</div>
|
||||
<table class="w-full table-fixed">
|
||||
{# Subtotal before vouchers #}
|
||||
<tr>
|
||||
<td class="px-2 pb-2 align-top">
|
||||
Gesamtpreis
|
||||
</td>
|
||||
<td class="px-2 pb-2 align-top w-24 text-right whitespace-nowrap">
|
||||
{{ summaryData.totalPrice|format_currency('EUR') }}
|
||||
</td>
|
||||
</tr>
|
||||
{# Voucher discounts breakdown #}
|
||||
{% for voucher in acceptedVouchers.vouchers %}
|
||||
<tr>
|
||||
<td class="px-2 pb-2 align-top">
|
||||
{% if voucher.promotional %}
|
||||
Aktionsgutschein ({{ voucher.code }})
|
||||
{% elseif voucher.goodwill %}
|
||||
Kulanzgutschein ({{ voucher.code }})
|
||||
{% elseif voucher.purchase %}
|
||||
Kaufgutschein ({{ voucher.code }})
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-2 pb-2 align-top w-24 text-right whitespace-nowrap">
|
||||
- {{ voucher.amount|format_currency('EUR') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{# Voucher discounts breakdown #}
|
||||
<div class="mb-3 space-y-1">
|
||||
{% for voucher in acceptedVouchers.vouchers %}
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="text-gray-600">
|
||||
{% if voucher.promotional %}
|
||||
Aktionsgutschein ({{ voucher.code }})
|
||||
{% elseif voucher.goodwill %}
|
||||
Kulanzgutschein ({{ voucher.code }})
|
||||
{% elseif voucher.purchase %}
|
||||
Kaufgutschein ({{ voucher.code }})
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="font-medium text-green-600">
|
||||
-€{{ voucher.amount|number_format(2, ',', '.') }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Amount to pay after vouchers #}
|
||||
<div class="flex justify-between items-center pt-2 border-t border-gray-200">
|
||||
<span class="font-bold text-lg text-gray-800">Zu zahlen:</span>
|
||||
<span class="font-bold text-xl text-gray-900" {{ qa_attribute('summary-total') }}>
|
||||
€{{ (summaryData.pricingData.grandTotal - acceptedVouchers.totalDiscount)|number_format(2, ',', '.') }}
|
||||
</span>
|
||||
<div class="flex items-center justify-between px-2 pb-2">
|
||||
<span class="block uppercase font-semibold">Zu zahlen</span>
|
||||
<span class="font-semibold">{{ summaryData.payableAmount|format_currency('EUR') }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-bold text-lg text-gray-800">Gesamtpreis:</span>
|
||||
<span class="font-bold text-xl text-gray-900" {{ qa_attribute('summary-total') }}>
|
||||
€{{ summaryData.pricingData.grandTotal|number_format(2, ',', '.') }}
|
||||
</span>
|
||||
<div class="flex items-center justify-between pb-2">
|
||||
<span class="block uppercase font-semibold">Gesamtpreis</span>
|
||||
<span class="font-semibold">{{ summaryData.payableAmount|format_currency('EUR') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{% if summaryData.cmsData.hotel.images is defined %}
|
||||
<div class="grid grid-cols-3 gap-x-4 py-4 border-t border-primary-bg">
|
||||
<img src="{{ summaryData.cmsData.hotel.images.resized.l[0].url }}"
|
||||
alt="{{ summaryData.cmsData.hotel.images.resized.l[0].alt }}"
|
||||
class="block w-full h-auto">
|
||||
<div class="col-span-2">
|
||||
<span class="block font-semibold uppercase">{{ summaryData.cmsData.hotel.name }}</span>
|
||||
{% if summaryData.cmsData.hotel.address is defined %}
|
||||
{{ summaryData.cmsData.hotel.address | nl2br }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,98 +1,96 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
<div class="px-4 lg:px-8 py-8 lg:py-16">
|
||||
|
||||
<h1 class="mb-4">
|
||||
Neue Buchung
|
||||
</h1>
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-6">
|
||||
<h2 class="text-blue-900 font-semibold text-lg mb-3">
|
||||
Optional: Anmelden für schnelleres Buchen
|
||||
</h2>
|
||||
<p class="text-blue-800 mb-2">
|
||||
Melde dich an, um deine persönlichen Daten automatisch in die Buchung zu übernehmen.
|
||||
Dies ist <strong>vollständig optional</strong> – du kannst auch ohne Anmeldung als Gast fortfahren.
|
||||
</p>
|
||||
</div>
|
||||
<h1 class="text-white uppercase pb-4 mb-8 border-b border-primary-bg/40">
|
||||
Neue<br>Buchung
|
||||
</h1>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{# Login Form #}
|
||||
<div>
|
||||
<h3 class="mb-4 font-semibold text-lg">
|
||||
Mit bestehendem Account anmelden
|
||||
</h3>
|
||||
|
||||
{% if error %}
|
||||
{% include '_partials/_alert.html.twig' with { 'level': 'error', messages: [ error.messageKey|trans(error.messageData, 'security') ] } %}
|
||||
{% endif %}
|
||||
|
||||
<form action="{{ path('app_login') }}" method="post" {{ stimulus_action('loading', 'toggle') }}>
|
||||
<div class="mb-4">
|
||||
<label for="username" class="mb-1 font-semibold">
|
||||
E-Mail:
|
||||
</label>
|
||||
<input type="email"
|
||||
id="username"
|
||||
name="_username"
|
||||
value="{{ last_username }}"
|
||||
class="form-field"
|
||||
required
|
||||
autocomplete="username">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="mb-1 font-semibold">
|
||||
Passwort:
|
||||
</label>
|
||||
<input type="password"
|
||||
id="password"
|
||||
name="_password"
|
||||
class="form-field"
|
||||
required
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-2">
|
||||
<button type="submit" class="button bg-button">
|
||||
Anmelden und fortfahren
|
||||
</button>
|
||||
<a href="{{ path('app_reset_password') }}"
|
||||
class="text-sm text-center text-blue-600 hover:underline">
|
||||
Passwort vergessen?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
</form>
|
||||
<div class="text-white uppercase pb-4 ld:pb-8">
|
||||
{{ travel_title }}
|
||||
<br>
|
||||
{{ travel_date_from | date('d.m.Y') }} - {{ travel_date_to | date('d.m.Y') }}
|
||||
</div>
|
||||
|
||||
{# Continue as Guest #}
|
||||
<div>
|
||||
<h3 class="mb-4 font-semibold text-lg">
|
||||
Als Gast fortfahren
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<a href="{{ path('app_booking_create_step_1') }}" class="button button--secondary mb-4">
|
||||
Als Gast fortfahren
|
||||
</a>
|
||||
|
||||
<p class="mb-4 text-gray-700">
|
||||
Du kannst auch ohne Anmeldung buchen. Deine persönlichen Daten gibst du dann im nächsten Schritt ein.
|
||||
</p>
|
||||
|
||||
<a href="{{ path('app_booking_create_step_1') }}" class="button bg-button bg-button--secondary w-full block text-center">
|
||||
Als Gast fortfahren
|
||||
</a>
|
||||
|
||||
<div class="mt-6 p-4 bg-gray-50 border border-gray-200 rounded">
|
||||
<h4 class="font-semibold mb-2 text-sm">
|
||||
<h2 class="text-base text-white">
|
||||
Noch kein Account?
|
||||
</h4>
|
||||
<p class="text-sm text-gray-700">
|
||||
</h2>
|
||||
<p class="text-white">
|
||||
Nach Abschluss deiner Buchung wird automatisch ein Account für dich erstellt.
|
||||
Du erhältst dann Zugangsdaten per E-Mail und kannst deine Buchungen jederzeit verwalten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-base text-white">
|
||||
Mit bestehendem Account anmelden
|
||||
</h2>
|
||||
|
||||
{% include 'booking/_cancel_link.html.twig' %}
|
||||
{% if error %}
|
||||
{% include '_partials/_alert.html.twig' with { 'level': 'error', messages: [ error.messageKey|trans(error.messageData, 'security') ] } %}
|
||||
{% endif %}
|
||||
|
||||
<div class="pb-4">
|
||||
<form action="{{ path('app_login') }}" method="post" {{ stimulus_action('loading', 'toggle') }}>
|
||||
<div class="mb-4">
|
||||
<label for="username" class="mb-1 font-semibold text-white">
|
||||
E-Mail:
|
||||
</label>
|
||||
<input type="email"
|
||||
id="username"
|
||||
name="_username"
|
||||
value="{{ last_username }}"
|
||||
class="form-field"
|
||||
required
|
||||
autocomplete="username">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="mb-1 font-semibold text-white">
|
||||
Passwort:
|
||||
</label>
|
||||
<input type="password"
|
||||
id="password"
|
||||
name="_password"
|
||||
class="form-field"
|
||||
required
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button button--primary">
|
||||
Anmelden und fortfahren
|
||||
</button>
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<a href="{{ path('app_reset_password') }}"
|
||||
class="text-sm underline text-white">
|
||||
Passwort vergessen?
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4">
|
||||
<button type="button"
|
||||
hx-get="{{ path('app_booking_cancel') }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10">
|
||||
<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,34 +5,16 @@
|
||||
{% block body %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-lg mx-auto">
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-6">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<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" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
Booking Error
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
{% for flash_message in app.flashes('error') %}
|
||||
<p>{{ flash_message }}</p>
|
||||
{% else %}
|
||||
<p>Es ist ein Fehler beim Starten des Buchungsvorgangs aufgetreten.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="flex space-x-2">
|
||||
<a href="https://www.ep-reisen.de" class="button bg-button bg-button--secondary">
|
||||
Zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% set errorMessages = app.flashes('error')|default(['Es ist ein Fehler beim Starten des Buchungsvorgangs aufgetreten.']) %}
|
||||
{% include '_partials/_alert.html.twig' with {
|
||||
level: 'error',
|
||||
title: 'Booking Error',
|
||||
messages: errorMessages
|
||||
} %}
|
||||
|
||||
<a href="https://www.ep-reisen.de" class="button button--secondary">
|
||||
Zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,64 +1,105 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
{% extends 'layout_booking.html.twig' %}
|
||||
|
||||
{% macro stepFormField(form) %}
|
||||
<div class="grid grid-cols-3 pb-2">
|
||||
<div class="col-span-2 pr-2">
|
||||
<div class="font-semibold">
|
||||
{{ form.vars.label_room }}
|
||||
</div>
|
||||
<div class="text-gray-500">
|
||||
{{ form.vars.label_price }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{{ form_row(form, {
|
||||
'attr': {
|
||||
'hx-post': path('app_booking_create_step_1_refresh'),
|
||||
'hx-swap': 'none',
|
||||
'hx-trigger': 'change delay:300ms',
|
||||
}
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
<h1>Neue Buchung</h1>
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
<div id="form-wrapper" class="col-span-2">
|
||||
<h2 class="pb-4">
|
||||
Unterkunft
|
||||
</h2>
|
||||
{{ form_start(form) }}
|
||||
{% do form.roomSelections.setRendered %}
|
||||
{# This block contains the room selection form fields #}
|
||||
{% block room_selection_form %}
|
||||
<div id="room-selection-form"{% if htmx_oob_swap is defined and htmx_oob_swap %} hx-swap-oob="true"{% endif %}>
|
||||
{{ form_errors(form) }}
|
||||
{% if groupedRooms.by_room is not empty %}
|
||||
<h3>
|
||||
Zimmer
|
||||
</h3>
|
||||
{% for roomId, room in groupedRooms.by_room %}
|
||||
{{ form_row(form.roomSelections[roomId], {
|
||||
'attr': {
|
||||
'hx-post': path('app_booking_create_step_1_refresh'),
|
||||
'hx-swap': 'none',
|
||||
'hx-trigger': 'change delay:300ms',
|
||||
}
|
||||
}) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if groupedRooms.by_pax is not empty %}
|
||||
<h3>
|
||||
Betten
|
||||
</h3>
|
||||
{% for roomId, room in groupedRooms.by_pax %}
|
||||
{{ form_row(form.roomSelections[roomId], {
|
||||
'attr': {
|
||||
'hx-post': path('app_booking_create_step_1_refresh'),
|
||||
'hx-swap': 'none',
|
||||
'hx-trigger': 'change delay:300ms'
|
||||
}
|
||||
}) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'class': 'flex-1 flex flex-col min-h-0',
|
||||
'hx-post': path('app_booking_create_step_1'),
|
||||
'hx-target': '#form-wrapper',
|
||||
'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
{# Pagination - mobile only (above summary) #}
|
||||
<div class="lg:hidden">
|
||||
{% include 'booking/_pagination.html.twig' with { 'current_step': 1 } %}
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col lg:grid lg:grid-cols-5 min-h-0 relative">
|
||||
{% block booking_summary %}
|
||||
<div id="booking-summary"
|
||||
class="order-1 lg:order-2 lg:flex-1 lg:min-h-0 lg:col-span-2"
|
||||
{{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }}
|
||||
{% if htmx_oob_swap is defined and htmx_oob_swap %}hx-swap-oob="true"{% endif %}>
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingCreateDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block form %}
|
||||
<div id="form-wrapper"
|
||||
class="flex-1 order-2 lg:order-1 lg:col-span-3 bg-primary-bg flex flex-col min-h-0">
|
||||
{# Flash messages - must be inside form-wrapper for HTMX swap to display them #}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
{# Pagination - desktop only (fixed above scrollable content) #}
|
||||
<div class="hidden lg:block shrink-0">
|
||||
{% include 'booking/_pagination.html.twig' with { 'current_step': 1 } %}
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
|
||||
<h2 class="pb-4">
|
||||
Unterkunft
|
||||
</h2>
|
||||
{% do form.roomSelections.setRendered %}
|
||||
{% block room_selection_form %}
|
||||
<div id="room-selection-form"{% if htmx_oob_swap is defined and htmx_oob_swap %} hx-swap-oob="true"{% endif %}>
|
||||
{{ form_errors(form) }}
|
||||
{% if groupedRooms.by_room is not empty %}
|
||||
<h3>
|
||||
Zimmer
|
||||
</h3>
|
||||
{% for roomId, room in groupedRooms.by_room %}
|
||||
{{ _self.stepFormField(form.roomSelections[roomId]) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if groupedRooms.by_pax is not empty %}
|
||||
<h3>
|
||||
Betten
|
||||
</h3>
|
||||
{% for roomId, room in groupedRooms.by_pax %}
|
||||
{{ _self.stepFormField(form.roomSelections[roomId]) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
<div class="flex justify-end pt-4">
|
||||
<button type="submit" class="button bg-button bg-button--secondary" {{ qa_attribute('btn-submit')}} {{ stimulus_action('loading', 'toggle') }}>Weiter</button>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{% block booking_summary %}
|
||||
<div id="booking-summary"{% if htmx_oob_swap is defined and htmx_oob_swap %} hx-swap-oob="true"{% endif %}>
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingCreateDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
|
||||
<div class="flex justify-between" hx-disinherit="*">
|
||||
<button type="button"
|
||||
hx-get="{{ path('app_booking_cancel') }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10">
|
||||
<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
<button type="submit" class="button button--primary" {{ qa_attribute('btn-submit')}} {{ stimulus_action('loading', 'toggle') }}>
|
||||
Weiter zu Schritt 2
|
||||
</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% include 'booking/_cancel_link.html.twig' %}
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,74 +1,80 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
{% extends 'layout_booking.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
<h1>Neue Buchung</h1>
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'class': 'flex-1 flex flex-col min-h-0',
|
||||
'hx-post': path('app_booking_create_step_2'),
|
||||
'hx-target': '#main-content',
|
||||
'hx-swap': 'innerHTML scroll:top'
|
||||
}
|
||||
}) }}
|
||||
{# Pagination - mobile only (above summary) #}
|
||||
<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">
|
||||
{% block booking_summary %}
|
||||
<div id="booking-summary"
|
||||
class="order-1 lg:order-2 lg:flex-1 lg:min-h-0 lg:col-span-2"
|
||||
{{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }}
|
||||
{% if htmx_oob_swap|default(false) %} hx-swap-oob="true"{% endif %}>
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block participant_cards %}
|
||||
<div id="main-content"
|
||||
class="flex-1 order-2 lg:order-1 lg:col-span-3 bg-primary-bg 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 class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
|
||||
<h2 class="pb-4">
|
||||
Teilnehmer
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
{# Main content area - cards grid #}
|
||||
{% block participant_cards %}
|
||||
<div id="main-content" class="col-span-2">
|
||||
{{ form_start(form, {
|
||||
'method': 'POST',
|
||||
'action': path('app_booking_create_step_2'),
|
||||
'attr': {
|
||||
'hx-post': path('app_booking_create_step_2'),
|
||||
'hx-target': '#main-content',
|
||||
'hx-swap': 'innerHTML'
|
||||
}
|
||||
}) }}
|
||||
{# Display form-level validation errors #}
|
||||
{% 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.']
|
||||
} %}
|
||||
{% endif %}
|
||||
|
||||
<h2>Teilnehmer</h2>
|
||||
|
||||
{# Display form-level validation errors #}
|
||||
{% if form.vars.submitted and not form.vars.valid %}
|
||||
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg" {{ qa_attribute('alert-form-errors') }}>
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-red-600 mr-2 mt-0.5 flex-shrink-0" 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>
|
||||
<div>
|
||||
<p class="font-semibold text-red-800 mb-1">Bitte überprüfe die Teilnehmerdaten</p>
|
||||
<p class="text-sm text-red-700">Einige Teilnehmer haben noch unvollständige oder fehlerhafte Angaben. Bitte bearbeite die markierten Teilnehmer.</p>
|
||||
<div class="divide-y divide-gray-200">
|
||||
{% for cardData in cardsData %}
|
||||
{% include 'booking/_participant_card.html.twig' with {
|
||||
'cardData': cardData,
|
||||
'index': loop.index0,
|
||||
'participantNumber': loop.index,
|
||||
'mode': 'create'
|
||||
} %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="participant-cards-grid" class="space-y-4">
|
||||
{% for cardData in cardsData %}
|
||||
{% include 'booking/_participant_card.html.twig' with {
|
||||
'cardData': cardData,
|
||||
'index': loop.index0,
|
||||
'mode': 'create'
|
||||
} %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-8">
|
||||
<a href="{{ path('app_booking_create_step_1') }}" class="button bg-button bg-button--secondary" {{ qa_attribute('btn-back') }} {{ stimulus_action('loading', 'toggle') }}>
|
||||
Zurück
|
||||
</a>
|
||||
<button type="submit" class="button bg-button bg-button--secondary" {{ qa_attribute('btn-submit') }} {{ stimulus_action('loading', 'toggle') }}>
|
||||
Weiter
|
||||
{% endblock %}
|
||||
</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" hx-disinherit="*">
|
||||
<button type="button"
|
||||
hx-get="{{ path('app_booking_cancel') }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10">
|
||||
<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
<button type="submit" class="button button--primary" {{ qa_attribute('btn-submit') }} {{ stimulus_action('loading', 'toggle') }}>
|
||||
Weiter zu Schritt 3
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{# Sidebar summary #}
|
||||
{% block booking_summary %}
|
||||
<div id="booking-summary"{% if htmx_oob_swap|default(false) %} hx-swap-oob="true"{% endif %}>
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% include 'booking/_cancel_link.html.twig' %}
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,23 +1,54 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
{% extends 'layout_booking.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
<h1>Neue Buchung</h1>
|
||||
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
{# Main content area - participant form #}
|
||||
<div id="main-content" class="col-span-2">
|
||||
{% include 'booking/_participant_form.html.twig' %}
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'class': 'flex-1 flex flex-col min-h-0',
|
||||
'novalidate': 'novalidate',
|
||||
'hx-post': path(submitRouteName, submitRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content',
|
||||
'hx-swap': 'innerHTML scroll:top'
|
||||
}
|
||||
}) }}
|
||||
{# 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 {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
</div>
|
||||
|
||||
{# Sidebar summary #}
|
||||
<div id="booking-summary">
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
{# 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 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' %}
|
||||
{% include 'booking/_participant_form.html.twig' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'booking/_cancel_link.html.twig' %}
|
||||
<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="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10"
|
||||
{{ stimulus_action('loading', 'toggle') }}>
|
||||
<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</a>
|
||||
<button type="submit" class="button button--primary" {{ qa_attribute('btn-submit') }} {{ stimulus_action('loading', 'toggle') }}>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,68 +1,92 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
{% extends 'layout_booking.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Neue Buchung</h1>
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'class': 'flex-1 flex flex-col min-h-0',
|
||||
'novalidate': 'novalidate',
|
||||
'hx-post': path('app_booking_create_step_3'),
|
||||
'hx-target': '#form-wrapper',
|
||||
'hx-select': '#form-wrapper',
|
||||
'hx-swap': 'outerHTML'
|
||||
}
|
||||
}) }}
|
||||
{# Pagination - mobile only (above summary) #}
|
||||
<div class="lg:hidden">
|
||||
{% include 'booking/_pagination.html.twig' with { 'current_step': 3 } %}
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col lg:grid lg:grid-cols-3 min-h-0 relative">
|
||||
{% block booking_summary %}
|
||||
<div id="booking-summary"
|
||||
class="order-1 lg:order-2 lg:flex-1 lg:min-h-0"
|
||||
{{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }}
|
||||
{% if htmx_oob_swap is defined and htmx_oob_swap %}hx-swap-oob="true"{% endif %}>
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingCreateDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block form %}
|
||||
<div id="form-wrapper"
|
||||
class="flex-1 order-2 lg:order-1 lg:col-span-2 bg-white flex flex-col min-h-0">
|
||||
{# Flash messages - must be inside form-wrapper for HTMX swap to display them #}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
{# Pagination - desktop only (fixed above scrollable content) #}
|
||||
<div class="hidden lg:block shrink-0">
|
||||
{% include 'booking/_pagination.html.twig' with { 'current_step': 3 } %}
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
|
||||
<h2 class="pb-4">
|
||||
Zahlungsart
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
<div id="form-wrapper" class="col-span-2">
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
{# Payment method selection with HTMX #}
|
||||
<div id="form-payment"
|
||||
hx-post="{{ path('app_booking_create_step_3_refresh') }}"
|
||||
hx-trigger="change"
|
||||
hx-target="#form-payment"
|
||||
hx-select="#form-payment"
|
||||
hx-swap="outerHTML">
|
||||
{{ form_row(form.paymentMethod) }}
|
||||
|
||||
<h2 class="mb-6">Zahlungsart</h2>
|
||||
{# Bank account section - conditionally rendered #}
|
||||
{% if form.bankAccount is defined %}
|
||||
<h3 class="font-semibold text-lg mb-4">
|
||||
Bankverbindung
|
||||
</h3>
|
||||
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'novalidate': 'novalidate',
|
||||
'hx-post': path('app_booking_create_step_3'),
|
||||
'hx-target': '#form-wrapper',
|
||||
'hx-select': '#form-wrapper',
|
||||
'hx-swap': 'outerHTML'
|
||||
}
|
||||
}) }}
|
||||
<div class="space-y-4">
|
||||
{{ form_row(form.bankAccount.iban) }}
|
||||
{{ form_row(form.bankAccount.accountHolder) }}
|
||||
{{ form_row(form.bankAccount.bankName) }}
|
||||
|
||||
{# Payment method selection with HTMX #}
|
||||
<div id="form-payment"
|
||||
hx-post="{{ path('app_booking_create_step_3_refresh') }}"
|
||||
hx-trigger="change"
|
||||
hx-target="#form-payment"
|
||||
hx-select="#form-payment"
|
||||
hx-swap="outerHTML">
|
||||
{{ form_row(form.paymentMethod) }}
|
||||
|
||||
{# Bank account section - conditionally rendered #}
|
||||
{% if form.bankAccount is defined %}
|
||||
<div class="border border-gray-300 rounded-lg p-4 bg-gray-50 mt-4">
|
||||
<h3 class="font-semibold text-lg mb-4">Bankverbindung</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
{{ form_row(form.bankAccount.iban) }}
|
||||
{{ form_row(form.bankAccount.accountHolder) }}
|
||||
{{ form_row(form.bankAccount.bankName) }}
|
||||
|
||||
<div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded">
|
||||
{{ form_row(form.bankAccount.sepaMandateAccepted, {
|
||||
'label_attr': {'class': 'text-sm'}
|
||||
}) }}
|
||||
</div>
|
||||
<div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded">
|
||||
{{ form_row(form.bankAccount.sepaMandateAccepted, {
|
||||
'label_attr': {'class': 'text-sm'}
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between pt-6">
|
||||
<a href="{{ path('app_booking_create_step_2') }}" class="button bg-button bg-button--secondary" {{ stimulus_action('loading', 'toggle') }}>Zurück</a>
|
||||
<button type="submit" class="button bg-button bg-button--secondary" {{ stimulus_action('loading', 'toggle') }}>Weiter</button>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div id="booking-summary">
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingCreateDto,
|
||||
'summaryData': summaryData
|
||||
} %}
|
||||
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
|
||||
<div class="flex justify-between" hx-disinherit="*">
|
||||
<button type="button"
|
||||
hx-get="{{ path('app_booking_cancel') }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10">
|
||||
<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
<button type="submit" class="button button--primary" {{ qa_attribute('btn-submit') }} {{ stimulus_action('loading', 'toggle') }}>
|
||||
Weiter zu Schritt 4
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'booking/_cancel_link.html.twig' %}
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,37 +3,56 @@
|
||||
{% block title %}Buchung erfolgreich{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-2xl mx-auto text-center py-12">
|
||||
<h1 class="text-3xl font-bold mb-4">Buchung erfolgreich abgeschlossen</h1>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 px-4 lg:px-8 py-8 lg:py-16">
|
||||
<div>
|
||||
<h1 class="text-white uppercase pb-4 mb-8 border-b border-primary-bg/40">
|
||||
Vielen Dank
|
||||
<br>
|
||||
für deine Buchung
|
||||
</h1>
|
||||
|
||||
<p class="text-xl mb-8">
|
||||
Deine Buchungsnummer lautet <strong class="font-mono">{{ bookingNumber }}</strong>
|
||||
</p>
|
||||
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8">
|
||||
<p class="text-gray-700">
|
||||
Du erhältst in Kürze eine Bestätigungs-E-Mail mit allen Details zu Deiner Buchung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% if app.user %}
|
||||
{# Authenticated user - show account-related actions #}
|
||||
<div class="space-y-4">
|
||||
<a href="{{ path('app_personal_data') }}" class="button bg-button bg-button--primary inline-block">
|
||||
Zu meinen persönlichen Daten
|
||||
</a>
|
||||
<a href="{{ path('app_bookings') }}" class="button bg-button bg-button--secondary inline-block ml-2">
|
||||
Meine Buchungen anzeigen
|
||||
</a>
|
||||
<a href="{{ path('app_logout') }}" class="button bg-button bg-button--outline inline-block ml-2">
|
||||
Abmelden
|
||||
</a>
|
||||
<div class="text-xl text-white mb-8">
|
||||
Deine Buchungsnummer lautet
|
||||
<br>
|
||||
<strong class="font-mono">{{ bookingNumber }}</strong>
|
||||
</div>
|
||||
{% else %}
|
||||
{# Guest user - show simple homepage link #}
|
||||
<a href="https://www.ep-reisen.de" class="button bg-button bg-button--primary">
|
||||
Zurück zur Startseite
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="pb-8">
|
||||
{% include '_partials/_alert.html.twig' with {
|
||||
level: 'info',
|
||||
messages: ['Du erhältst in Kürze eine Bestätigungs-E-Mail mit allen Details.']
|
||||
} %}
|
||||
</div>
|
||||
|
||||
{% if app.user %}
|
||||
{# Authenticated user - show account-related actions #}
|
||||
<ul class="divide-y divide-primary-bg/40">
|
||||
<li class="py-4">
|
||||
<a href="{{ path('app_bookings') }}" class="flex items-center space-x-2 group" {{ stimulus_action('loading', 'toggle') }}>
|
||||
<svg class="w-8 h-8 text-white group-hover:text-primary-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="128" y1="128" x2="216" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="128" y1="64" x2="216" y2="64" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="128" y1="192" x2="216" y2="192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="40 64 56 80 88 48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="40 128 56 144 88 112" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="40 192 56 208 88 176" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
<span class="text-xl uppercase text-white group-hover:text-primary-light">Meine Buchungen</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="py-4">
|
||||
<a href="{{ path('app_personal_data') }}" class="flex items-center space-x-2 group" {{ stimulus_action('loading', 'toggle') }}>
|
||||
<svg class="w-8 h-8 text-white group-hover:text-primary-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M160,224c3.67-13.8,16.6-24,32-24s28.33,10.2,32,24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="192" cy="176" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M120,208H40a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H93.33a8,8,0,0,1,4.8,1.6l27.74,20.8a8,8,0,0,0,4.8,1.6H216a8,8,0,0,1,8,8v32" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
<span class="text-xl uppercase text-white group-hover:text-primary-light">Meine Daten</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="py-4">
|
||||
<a href="{{ path('app_logout') }}" class="flex items-center space-x-2 group">
|
||||
<svg class="w-8 h-8 text-white group-hover:text-primary-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="112 40 48 40 48 216 112 216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="112" y1="128" x2="224" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="184 88 224 128 184 168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
<span class="text-xl uppercase text-white group-hover:text-primary-light">abmelden</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
{# Guest user - show simple homepage link #}
|
||||
<a href="https://www.ep-reisen.de" class="button button--primary">
|
||||
Zurück zur Startseite
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,109 +1,108 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
{% extends 'layout_booking.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
|
||||
{# Header with reload button #}
|
||||
<div class="flex justify-between items-center pb-8">
|
||||
<h1 class="text-3xl font-semibold">Buchung bearbeiten</h1>
|
||||
|
||||
<button type="button"
|
||||
hx-post="{{ path('app_booking_edit_reload', {id: bookingData.id}) }}"
|
||||
hx-confirm="Alle nicht gespeicherten Änderungen gehen verloren. Fortfahren?"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
class="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 rounded transition-colors">
|
||||
Änderungen verwerfen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Grid layout with 2/3 cards + 1/3 summary #}
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
{# Main content area - cards grid #}
|
||||
<div id="main-content" class="col-span-2">
|
||||
{% block participant_cards %}
|
||||
{{ form_start(form) }}
|
||||
<div>
|
||||
<h2>Teilnehmer</h2>
|
||||
|
||||
{% if isDirty %}
|
||||
<div class="mb-6 p-4 bg-yellow-50 border border-yellow-400 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-6 h-6 text-yellow-600 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-semibold text-yellow-800">Ungespeicherte Änderungen</h3>
|
||||
<p class="text-yellow-700 text-sm mt-1">
|
||||
Du hast Änderungen an der Buchung vorgenommen.
|
||||
Bitte denke daran, abschließend den 'Buchung aktualisieren' Button zu klicken.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Display validation errors #}
|
||||
{% if hasValidationErrors|default(false) %}
|
||||
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-red-600 mr-2 mt-0.5 flex-shrink-0" 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>
|
||||
<div>
|
||||
<p class="font-semibold text-red-800 mb-1">Bitte überprüfe die Teilnehmerdaten</p>
|
||||
<p class="text-sm text-red-700">Einige Teilnehmer haben noch unvollständige oder fehlerhafte Angaben. Bitte bearbeite die markierten Teilnehmer.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="participant-cards-grid" class="space-y-4">
|
||||
{% for participant in bookingDto.participants %}
|
||||
{% set isCanceled = (bookingData.participantsStatus[loop.index0] ?? null) == 'S' %}
|
||||
{% include 'booking/_participant_card.html.twig' with {
|
||||
'cardData': cardsData[loop.index0],
|
||||
'index': loop.index0,
|
||||
'mode': 'edit',
|
||||
'isCanceled': isCanceled,
|
||||
'bookingId': bookingData.id
|
||||
} %}
|
||||
{% endfor %}
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'class': 'flex-1 flex flex-col min-h-0'
|
||||
}
|
||||
}) }}
|
||||
<div class="flex-1 flex flex-col lg:grid lg:grid-cols-5 min-h-0 relative">
|
||||
{# Sidebar summary #}
|
||||
{% block booking_summary %}
|
||||
<div id="booking-summary"
|
||||
class="order-1 lg:order-2 lg:flex-1 lg:min-h-0 lg:col-span-2"
|
||||
{{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }}
|
||||
{% if htmx_oob_swap is defined and htmx_oob_swap %}hx-swap-oob="true"{% endif %}>
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData,
|
||||
'mutableData': mutableData|default(null)
|
||||
} %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<div class="flex justify-between mt-8">
|
||||
<button type="button"
|
||||
hx-post="{{ path('app_booking_edit_cancel', {id: bookingData.id}) }}"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
class="button bg-button bg-button--secondary">
|
||||
Zurück
|
||||
</button>
|
||||
{# Main content area #}
|
||||
<div id="main-content"
|
||||
class="flex-1 order-2 lg:order-1 lg:col-span-3 bg-primary-bg flex flex-col min-h-0">
|
||||
{# Header - desktop only #}
|
||||
<div class="hidden lg:flex shrink-0 px-4 lg:px-8 h-16 items-center justify-between shadow-md relative z-10 bg-white">
|
||||
<h1 class="text-xl font-semibold uppercase">
|
||||
Buchung bearbeiten
|
||||
</h1>
|
||||
{% if isDirty %}
|
||||
<button type="submit"
|
||||
<button type="button"
|
||||
hx-post="{{ path('app_booking_edit_reload', {id: bookingData.id}) }}"
|
||||
hx-confirm="Alle nicht gespeicherten Änderungen gehen verloren. Fortfahren?"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
{% if hasValidationErrors|default(false) %}
|
||||
disabled
|
||||
title="Bitte behebe zuerst alle Validierungsfehler"
|
||||
{% endif %}
|
||||
class="button bg-button bg-button--secondary {{ hasValidationErrors|default(false) ? 'opacity-50 cursor-not-allowed' : '' }}">
|
||||
Buchung aktualisieren
|
||||
class="button button--small button--secondary">
|
||||
Änderungen verwerfen
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Scrollable content #}
|
||||
<div class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
|
||||
{% block participant_cards %}
|
||||
<h2 class="pb-4">Teilnehmer</h2>
|
||||
|
||||
{% if isDirty %}
|
||||
{% include '_partials/_alert.html.twig' with {
|
||||
level: 'warning',
|
||||
title: 'Ungespeicherte Änderungen',
|
||||
messages: ['Du hast Änderungen an der Buchung vorgenommen. Bitte denke daran, abschließend den \'Buchung aktualisieren\' Button zu klicken.']
|
||||
} %}
|
||||
{% endif %}
|
||||
|
||||
{# Display validation errors #}
|
||||
{% if hasValidationErrors|default(false) %}
|
||||
{% 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 %}
|
||||
|
||||
<div id="participant-cards-grid" class="space-y-4">
|
||||
{% for participant in bookingDto.participants %}
|
||||
{% set isCanceled = (bookingData.participantsStatus[loop.index0] ?? null) == 'S' %}
|
||||
{% include 'booking/_participant_card.html.twig' with {
|
||||
'cardData': cardsData[loop.index0],
|
||||
'index': loop.index0,
|
||||
'participantNumber': loop.index,
|
||||
'mode': 'edit',
|
||||
'isCanceled': isCanceled,
|
||||
'bookingId': bookingData.id
|
||||
} %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{# Sidebar summary #}
|
||||
{% block booking_summary %}
|
||||
<div id="booking-summary"{% if htmx_oob_swap is defined and htmx_oob_swap %} hx-swap-oob="true"{% endif %}>
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData,
|
||||
'mutableData': mutableData|default(null)
|
||||
} %}
|
||||
{# Fixed footer #}
|
||||
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
|
||||
<div class="flex justify-between">
|
||||
<button type="button"
|
||||
hx-post="{{ path('app_booking_edit_cancel', {id: bookingData.id}) }}"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10">
|
||||
<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</button>
|
||||
{% if isDirty %}
|
||||
<button type="submit"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
{% if hasValidationErrors|default(false) %}
|
||||
disabled
|
||||
title="Bitte behebe zuerst alle Validierungsfehler"
|
||||
{% endif %}
|
||||
class="button button--primary {{ hasValidationErrors|default(false) ? 'opacity-50 cursor-not-allowed' : '' }}">
|
||||
Buchung aktualisieren
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,24 +1,52 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
{% extends 'layout_booking.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
<h1>Buchung bearbeiten</h1>
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'class': 'flex-1 flex flex-col min-h-0',
|
||||
'novalidate': 'novalidate',
|
||||
'hx-post': path(submitRouteName, submitRouteParams|default({index: participantIndex})),
|
||||
'hx-target': '#main-content',
|
||||
'hx-swap': 'innerHTML scroll:top'
|
||||
}
|
||||
}) }}
|
||||
<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 {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData,
|
||||
'mutableData': mutableData|default(null)
|
||||
} %}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
{# Main content area - participant form #}
|
||||
<div id="main-content" class="col-span-2">
|
||||
{% include 'booking/_participant_form.html.twig' %}
|
||||
{# 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">
|
||||
{# Header - desktop only #}
|
||||
<div class="hidden lg:flex shrink-0 px-4 lg:px-8 h-16 items-center shadow-md relative z-10 bg-white">
|
||||
<h1 class="text-xl font-semibold uppercase">Buchung bearbeiten</h1>
|
||||
</div>
|
||||
<div id="main-content" class="flex-1 overflow-y-auto 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' %}
|
||||
{% include 'booking/_participant_form.html.twig' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Sidebar summary #}
|
||||
<div id="booking-summary">
|
||||
{% include 'booking/_summary.html.twig' with {
|
||||
'bookingCreateDto': bookingDto,
|
||||
'summaryData': summaryData,
|
||||
'mutableData': mutableData|default(null)
|
||||
} %}
|
||||
<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_edit', {id: bookingDto.booking.id}) }}"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
{{ qa_attribute('btn-cancel') }}>
|
||||
<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</a>
|
||||
<button type="submit" class="button button--primary" {{ qa_attribute('btn-submit') }} {{ stimulus_action('loading', 'toggle') }}>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'booking/_cancel_link.html.twig' %}
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,92 +1,105 @@
|
||||
{% extends 'layout.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-2xl font-semibold pb-4">
|
||||
Meine Buchungen
|
||||
</h1>
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
<table class="w-full mb-4 responsive">
|
||||
<thead>
|
||||
<tr class="bg-zinc-200">
|
||||
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-left text-sm md:text-base" scope="col">
|
||||
Reise
|
||||
</th>
|
||||
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-sm md:text-base" scope="col">
|
||||
Reisedatum
|
||||
</th>
|
||||
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-sm md:text-base" scope="col">
|
||||
Buchungsnummer
|
||||
</th>
|
||||
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-sm md:text-base" scope="col">
|
||||
Reisepreis
|
||||
</th>
|
||||
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-sm md:text-base" scope="col">
|
||||
offen
|
||||
</th>
|
||||
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-sm md:text-base" scope="col">
|
||||
Status
|
||||
</th>
|
||||
<th scope="col">
|
||||
<span class="sr-only">Aktionen</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for booking in bookings %}
|
||||
<tr class="odd:bg-zinc-50 hover:bg-zinc-100 odd:hover:bg-zinc-100">
|
||||
<td class="border-r border-zinc-200 px-2 py-1 md:p-2"
|
||||
data-label="Reise">
|
||||
{{ booking.travelName }}
|
||||
</td>
|
||||
<td class="border-r border-zinc-200 px-2 py-1 md:p-2"
|
||||
data-label="Buchungsdatum">
|
||||
{% if booking.travelData %}
|
||||
{{ booking.travelData.dateFrom|date('d.m.Y') }} - {{ booking.travelData.dateTo|date('d.m.Y') }}
|
||||
{% else %}
|
||||
{{ booking.travelDate|date('d.m.Y') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="border-r border-zinc-200 px-2 py-1 md:p-2"
|
||||
data-label="Buchungsnummer">
|
||||
{{ booking.bookingNumber }}
|
||||
</td>
|
||||
<td class="border-r border-zinc-200 px-2 py-1 md:p-2"
|
||||
data-label="Reisepreis">
|
||||
{{ booking.price|format_currency('EUR') }}
|
||||
</td>
|
||||
<td class="border-r border-zinc-200 px-2 py-1 md:p-2"
|
||||
data-label="offen">
|
||||
{{ booking.balance ? booking.balance|format_currency('EUR') : '-' }}
|
||||
</td>
|
||||
<td class="border-r border-zinc-200 px-2 py-1 md:p-2"
|
||||
data-label="Status">
|
||||
{{ booking.status|map_status }}
|
||||
</td>
|
||||
<td class="px-2 py-1 md:p-2">
|
||||
<div class="flex md:flex-col space-x-2 md:space-x-0 md:space-y-1">
|
||||
{% if booking.editable %}
|
||||
<a href="{{ path('app_booking_edit', { 'id': booking.id }) }}"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
class="button bg-button bg-button--secondary button--small">
|
||||
bearbeiten
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ path('app_booking_invoice', { 'id': booking.id }) }}"
|
||||
class="button bg-button button--small"
|
||||
target="_blank">
|
||||
Rechnung
|
||||
</a>
|
||||
{% if booking.travelInfoUrl is not null %}
|
||||
<a href="{{ booking.travelInfoUrl }}"
|
||||
class="button bg-button bg-pink button--small"
|
||||
target="_blank">
|
||||
Reiseinfos
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="flex-1 flex flex-col min-h-0">
|
||||
{# Header #}
|
||||
<div class="shrink-0 px-4 lg:px-8 py-8 lg:py-16">
|
||||
<h1 class="text-white uppercase pb-4">
|
||||
Meine<br>Buchungen
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{# Scrollable content area #}
|
||||
<div class="flex-1 overflow-y-auto p-4 lg:p-8 bg-primary-bg">
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
|
||||
<div class="space-y-8">
|
||||
{% for booking in bookings %}
|
||||
<div class="grid md:grid-cols-2 gap-4 p-2 bg-white rounded-md">
|
||||
<div class="flex items-start space-x-4">
|
||||
<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">
|
||||
{{ loop.index }}
|
||||
</div>
|
||||
<div class="text-xl font-semibold">
|
||||
{{ booking.travelName }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<table class="mb-4">
|
||||
<tr>
|
||||
<th class="text-left border-r-2 border-gray-300 pr-2">
|
||||
Reisedatum
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{% if booking.travelData %}
|
||||
{{ booking.travelData.dateFrom|date('d.m.Y') }} - {{ booking.travelData.dateTo|date('d.m.Y') }}
|
||||
{% else %}
|
||||
{{ booking.travelDate|date('d.m.Y') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-left border-r-2 border-gray-300 pr-2">
|
||||
Buchungsnr.
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ booking.bookingNumber }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-left border-r-2 border-gray-300">
|
||||
Reisepreis
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ booking.price|format_currency('EUR') }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-left border-r-2 border-gray-300">
|
||||
offen
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ booking.balance ? booking.balance|format_currency('EUR') : '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-left border-r-2 border-gray-300">
|
||||
Status
|
||||
</th>
|
||||
<td class="pl-2">
|
||||
{{ booking.status|map_status }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="flex space-x-2">
|
||||
{% if booking.editable %}
|
||||
<a href="{{ path('app_booking_edit', { 'id': booking.id }) }}"
|
||||
{{ stimulus_action('loading', 'toggle') }}
|
||||
class="button button--primary button--small"
|
||||
title="Buchung bearbeiten">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M92.69,216H48a8,8,0,0,1-8-8V163.31a8,8,0,0,1,2.34-5.65L165.66,34.34a8,8,0,0,1,11.31,0L221.66,79a8,8,0,0,1,0,11.31L98.34,213.66A8,8,0,0,1,92.69,216Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="136" y1="64" x2="192" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="164" y1="92" x2="68" y2="188" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="95.49" y1="215.49" x2="40.51" y2="160.51" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ path('app_booking_invoice', { 'id': booking.id }) }}"
|
||||
class="button button--secondary button--small"
|
||||
title="Rechnung herunterladen"
|
||||
target="_blank">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M64,56H40A16,16,0,0,0,24,72h0A16,16,0,0,0,40,88H56a16,16,0,0,1,16,16h0a16,16,0,0,1-16,16H28" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="48" y1="48" x2="48" y2="56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="48" y1="120" x2="48" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M96,56H224V192a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V152" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="104" y1="104" x2="224" y2="104" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="80" y1="152" x2="224" y2="152" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="168" y1="104" x2="168" y2="200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</a>
|
||||
{% if booking.travelInfoUrl is not null %}
|
||||
<a href="{{ booking.travelInfoUrl }}"
|
||||
class="button button--secondary button--small"
|
||||
title="zu den Reiseinfos"
|
||||
target="_blank">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="128" cy="128" r="96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M120,120a8,8,0,0,1,8,8v40a8,8,0,0,0,8,8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="124" cy="84" r="12"/></svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{% extends 'htmx_modal.html.twig' %}
|
||||
|
||||
{% block title %}Sicher?{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pb-8">
|
||||
Willst du den Buchungsvorgang wirklich abbrechen?
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<button type="button"
|
||||
hx-post="{{ path('app_booking_cancel') }}"
|
||||
hx-target="body"
|
||||
class="button button--small button--primary">
|
||||
Ja
|
||||
</button>
|
||||
<button type="button"
|
||||
{{ stimulus_action('modal', 'close') }}
|
||||
class="button button--small button--secondary">
|
||||
Nein
|
||||
</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user