feat: implement screendesign

This commit is contained in:
Björn Fromme
2025-12-06 15:02:33 +01:00
parent b206c3990b
commit bfb1a04ea0
84 changed files with 2807 additions and 2603 deletions
+10 -10
View File
@@ -1,32 +1,32 @@
{% if modal is not defined %}
{% set modal = false %}
{% endif %}
<div class="{{ html_classes('rounded-md mb-4', { 'py-4': modal == true, 'p-4': modal == false, 'bg-red-50': level == 'error', 'bg-green-50': level == 'success', 'bg-blue-50': level == 'info' }) }}">
<div class="flex">
<div class="shrink-0">
{% if level == 'error' %}
<svg class="size-5 text-red-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<div class="{{ html_classes('rounded-md mb-4', { 'p-4': modal == false, 'bg-red-500': level == 'error', 'bg-green-500': level == 'success', 'bg-yellow-500': level == 'warning', 'bg-primary-light': level == 'info' }) }}">
<div class="flex items-center">
<div class="shrink-0 text-white">
{% if level == 'error' or level == 'warning' %}
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16ZM8.28 7.22a.75.75 0 0 0-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 1 0 1.06 1.06L10 11.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L11.06 10l1.72-1.72a.75.75 0 0 0-1.06-1.06L10 8.94 8.28 7.22Z" clip-rule="evenodd" />
</svg>
{% elseif level == 'success' %}
<svg class="size-5 text-green-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd" />
</svg>
{% else %}
<svg class="size-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-7-4a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM9 9a.75.75 0 0 0 0 1.5h.253a.25.25 0 0 1 .244.304l-.459 2.066A1.75 1.75 0 0 0 10.747 15H11a.75.75 0 0 0 0-1.5h-.253a.25.25 0 0 1-.244-.304l.459-2.066A1.75 1.75 0 0 0 9.253 9H9Z" clip-rule="evenodd" />
</svg>
{% endif %}
</div>
<div class="ml-3">
{% if title is defined %}
<div class="{{ html_classes('text-sm', { 'text-red-700': level == 'error', 'text-green-800': level == 'success', 'text-blue-800': level == 'info', 'mb-2': messages is defined }) }}">
<div class="text-white">
{{ title }}
</div>
{% endif %}
{% if messages is defined %}
<div class="{{ html_classes('text-sm', { 'text-red-700': level == 'error', 'text-green-800': level == 'success', 'text-blue-800': level == 'info' }) }}">
<ul role="list" class="list-disc space-y-1 pl-4">
<div class="text-white">
<ul role="list" class="space-y-1">
{% for message in messages %}
<li>
{{ message|raw }}
+19
View File
@@ -0,0 +1,19 @@
<div class="flex items-center justify-between">
<a href="https://www.ep-reisen.de" title="zur E&amp;P Website">
<img src="{{ asset('build/images/logo.svg') }}" alt="" class="h-10 my-2 w-auto">
</a>
<div class="flex items-center space-x-2">
{% if is_granted('ROLE_USER') %}
<a href="{{ path('app_logout') }}" title="aus MyE&P abmelden">
<svg class="w-10 h-10 text-primary-dark" 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>
</a>
<a href="{{ path('app_account') }}" title="zu meinem Account">
<svg class="w-10 h-10 text-primary-dark" 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"/><circle cx="128" cy="120" r="40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M63.8,199.37a72,72,0,0,1,128.4,0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
</a>
{% else %}
<a href="{{ path('app_login') }}" title="zu meinem Account">
<svg class="w-10 h-10 text-primary-dark" 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"/><circle cx="128" cy="120" r="40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M63.8,199.37a72,72,0,0,1,128.4,0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
</a>
{% endif %}
</div>
</div>
@@ -1,4 +1,39 @@
<div class="invisible fixed top-0 left-0 inset-0 z-10 backdrop-blur-sm flex items-center justify-center transition-all duration-100"
<div class="invisible fixed top-0 left-0 inset-0 z-50 backdrop-blur-sm flex items-center justify-center transition-all duration-100"
{{ stimulus_target('loading', 'indicator') }}>
{% include '_partials/_spinner.html.twig' with { 'class': 'text-primary-light w-32 h-32 lg:w-48 lg:h-48'} %}
<svg class="text-primary-light w-32 h-32 lg:w-48 lg:h-48" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
<g fill="none" fill-rule="evenodd" stroke-width="2">
<circle cx="22" cy="22" r="1">
<animate attributeName="r"
begin="0s" dur="1.8s"
values="1; 20"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="0s" dur="1.8s"
values="1; 0"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="indefinite" />
</circle>
<circle cx="22" cy="22" r="1">
<animate attributeName="r"
begin="-0.9s" dur="1.8s"
values="1; 20"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="-0.9s" dur="1.8s"
values="1; 0"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="indefinite" />
</circle>
</g>
</svg>
</div>
+8 -10
View File
@@ -1,21 +1,19 @@
<div class="fixed inset-0 w-full h-full z-50" {{ stimulus_controller('modal') }} {{ stimulus_action('modal', 'close', 'modal-close@window') }}>
<div class="absolute inset-0 w-full h-full bg-black/80" {{ stimulus_action('modal', 'close', 'click') }}></div>
<div class="absolute inset-0 backdrop-blur-sm" {{ stimulus_action('modal', 'close', 'click') }}></div>
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 px-4 w-full max-w-2xl">
<div class="{{ html_classes('relative py-8 rounded-md', { 'bg-red-50': level == 'error', 'bg-green-50': level == 'success', 'bg-blue-50': level == 'info' }) }}">
<div class="{{ html_classes('flex justify-between pb-4 px-8', { 'text-red-800': level == 'error', 'text-green-800': level == 'success', 'text-blue-800': level == 'info' }) }}">
<div class="text-2xl font-medium">
<div class="{{ html_classes('relative py-4 rounded-md', { 'bg-red-500': level == 'error', 'bg-green-500': level == 'success', 'bg-primary-light': level == 'info' }) }}">
<div class="flex justify-between pb-4 px-4">
<div class="text-2xl font-medium text-white">
{% block title %}{% endblock %}
</div>
<button type="button"
class="inline-block"
class="inline-block bg-gray-200 rounded-md p-1"
{{ stimulus_action('modal', 'close') }}>
{{ icon('close', 'w-6 h-6')}}
<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"/><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>
<div class="max-h-96 overflow-y-auto">
<div class="{{ html_classes('px-8', { 'text-red-700': level == 'error', 'text-green-700': level == 'success', 'text-blue-700': level == 'info' }) }}">
{% block content %}{% endblock %}
</div>
<div class="px-4 max-h-96 overflow-y-auto">
{% block content %}{% endblock %}
</div>
</div>
</div>
-37
View File
@@ -1,37 +0,0 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg class="{{ class }}" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
<g fill="none" fill-rule="evenodd" stroke-width="2">
<circle cx="22" cy="22" r="1">
<animate attributeName="r"
begin="0s" dur="1.8s"
values="1; 20"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="0s" dur="1.8s"
values="1; 0"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="indefinite" />
</circle>
<circle cx="22" cy="22" r="1">
<animate attributeName="r"
begin="-0.9s" dur="1.8s"
values="1; 20"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="-0.9s" dur="1.8s"
values="1; 0"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="indefinite" />
</circle>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,16 @@
{# Validation error alert box #}
{# Parameters:
- form: The form to check for validation errors
- message: Optional custom error message
- showLabels: Whether to show the list of invalid field labels (default: true)
#}
{% macro validation_alert(form, message, showLabels = true) %}
{% if not form.vars.valid %}
{% set errorLabels = showLabels ? collect_invalid_field_labels(form) : [] %}
{% include '_partials/_alert.html.twig' with {
level: 'error',
title: message|default('Du hast nicht alle Pflichtfelder ausgefüllt oder einzelne Angaben sind ungültig:'),
messages: [errorLabels | join(', ')]
} %}
{% endif %}
{% endmacro %}
+33
View File
@@ -0,0 +1,33 @@
{% extends 'layout.html.twig' %}
{% block content %}
<div class="px-4 lg:px-8 py-8 lg:py-16">
<h1 class="text-white uppercase pb-4">
Mein<br>Account
</h1>
</div>
<div class="p-4 ld:p-8">
<nav>
<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>
</nav>
</div>
{% endblock %}
+68
View File
@@ -0,0 +1,68 @@
{% extends 'layout.html.twig' %}
{% block content %}
<div class="px-4 lg:px-8 py-8 lg:py-16">
<h1 class="text-white uppercase pb-4">
Meine<br>Daten
</h1>
</div>
<div class="p-4 ld:p-8">
<div class="divide-y divide-primary-bg/40">
<div class="grid md:grid-cols-2 gap-x-8 gap-y-4 pb-8">
<div class="text-white">
<strong>Name</strong>: {{ personalData.fullName }}
<br>
<strong>Gender</strong>: {{ personalData.gender|map_gender }}
<br>
<strong>Geburtsdatum</strong>: {{ personalData.dateOfBirth|date('d.m.Y') }}
<br>
<strong>Nationalität</strong>: {{ personalData.nationality|map_nationality|default('-') }}
</div>
<div id="newsletter">
<p class="pb-4 text-white">
Du bist aktuell {% if not personalData.communication.newsletter %}<strong>nicht</strong> {% endif%} zum Newsletter
angemeldet.
</p>
<button type="button"
class="relative button button--primary"
hx-post="{{ path('app_personal_data_newsletter') }}"
hx-target="#newsletter"
hx-select="#newsletter"
hx-swap="outerHTML">
{% if personalData.communication.newsletter %}jetzt abmelden{% else %}jetzt anmelden{% endif %}
</button>
</div>
</div>
<div class="pt-8">
<h2 class="text-white uppercase text-xl">
Kontaktdaten
</h2>
{% include '_partials/_flashes.html.twig' %}
{{ form_start(personalDataForm, { 'attr': { 'data-action': 'loading#toggle' } }) }}
<div class="grid md:grid-cols-2 gap-x-8 gap-y-4 pb-4">
<div>
{{ form_row(personalDataForm.street, { 'label_attr': { 'class': 'text-white' } }) }}
{{ form_row(personalDataForm.postCode, { 'label_attr': { 'class': 'text-white' } }) }}
{{ form_row(personalDataForm.city, { 'label_attr': { 'class': 'text-white' } }) }}
{{ form_row(personalDataForm.country, { 'label_attr': { 'class': 'text-white' } }) }}
</div>
<div>
{{ form_row(personalDataForm.email, { 'label_attr': { 'class': 'text-white' } }) }}
{{ form_row(personalDataForm.phone, { 'label_attr': { 'class': 'text-white' } }) }}
{{ form_row(personalDataForm.mobile, { 'label_attr': { 'class': 'text-white' } }) }}
</div>
</div>
<div class="flex justify-between">
<a href="{{ path('app_account') }}" class="button button--secondary">
zurück
</a>
<button type="submit" class="button button--primary">
Speichern
</button>
</div>
{{ form_rest(personalDataForm) }}
{{ form_end(personalDataForm) }}
</div>
</div>
</div>
{% endblock %}
+1
View File
@@ -2,6 +2,7 @@
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}MyE&P.next{% endblock %}</title>
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
-5
View File
@@ -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>
+75
View File
@@ -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 -%}
+30
View File
@@ -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>
+32 -54
View File
@@ -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>
+491 -358
View File
@@ -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,
+218 -171
View File
@@ -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 %}
+80 -82
View File
@@ -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 %}
+10 -28
View File
@@ -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 %}
+98 -57
View File
@@ -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 %}
+68 -62
View File
@@ -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 %}
+81 -57
View File
@@ -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
+49 -30
View File
@@ -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 %}
+93 -94
View File
@@ -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 %}
+46 -18
View File
@@ -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 %}
+100 -87
View File
@@ -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 %}
+22
View File
@@ -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 %}
+13 -13
View File
@@ -25,7 +25,7 @@
{% endmacro %}
{%- block form_row -%}
{%- set row_attr = row_attr|merge({ 'class': (row_attr.class|default('') ~ ' mb-1')|trim }) -%}
{%- 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"}} -%}
@@ -52,9 +52,9 @@
{%- block form_label -%}
{% set class = 'font-semibold' %}
{% if errors|length %}
{% set class = class ~ ' text-red-700' %}
{% set class = class ~ ' text-red-500' %}
{% endif %}
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ class)|trim}) %}
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' ' ~ class)|trim}) %}
{{ parent() }}
{%- endblock form_label -%}
@@ -62,7 +62,7 @@
{%- if errors|length > 0 -%}
<ul class="pb-2">
{%- for error in errors -%}
<li class="text-red-700">{{ error.message }}</li>
<li class="text-red-500">{{ error.message }}</li>
{%- endfor -%}
</ul>
{%- endif -%}
@@ -155,7 +155,7 @@
<div class="flex h-6 items-center">
{{ form_widget(form) }}
</div>
<div class="{{ html_classes('ml-2 leading-6', { 'text-red-700': errors|length }) }}">
<div class="{{ html_classes('ml-2 leading-6', { 'text-red-500': errors|length }) }}">
{{ form.vars.label | raw }}
{{- form_errors(form) -}}
</div>
@@ -178,7 +178,7 @@
<div class="flex h-6 items-center">
{{ form_widget(form) }}
</div>
<div class="{{ html_classes('ml-2 leading-6', { 'text-red-700': errors|length }) }}">
<div class="{{ html_classes('ml-2 leading-6', { 'text-red-500': errors|length }) }}">
{{ form.vars.label | raw }}
{{- form_errors(form) -}}
</div>
@@ -236,7 +236,7 @@
role="button"
class="relative rounded-md border border-gray-300 flex items-start space-x-2 px-3">
<div {{ stimulus_target('multiselect', 'label') }} class="flex-1 min-h-8 py-1.5 text-gray-900 text-sm"></div>
{{ icon('dots', 'w-4 h-4 mt-2') }}
<svg class="w-4 h-4 mt-2" 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="12"/><circle cx="128" cy="60" r="12"/><circle cx="128" cy="196" r="12"/></svg>
</div>
<div {{ stimulus_target('multiselect', 'dropdown')}} class="absolute z-10 top-full -mt-1.5 left-0 inset-x-0 max-h-48 overflow-y-scroll px-3 py-2 bg-white shadow-lg rounded-md rounded-t-none border border-gray-300 border-t-0 hidden">
{% for item in form.children %}
@@ -325,13 +325,13 @@
'data-step-input-target': 'field',
'class': 'hidden',
} }) }}
<div class="flex space-x-2">
<button type="button" class="button button--small bg-button" {{ stimulus_target('step-input', 'buttonDec') }} {{ stimulus_action('step-input', 'decrease') }}>
-
<div class="flex items-center justify-between">
<button type="button" class="button button--small bg-white" {{ stimulus_target('step-input', 'buttonDec') }} {{ stimulus_action('step-input', 'decrease') }}>
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="40" y1="128" x2="216" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
</button>
<div {{ stimulus_target('step-input', 'display') }}></div>
<button type="button" class="button button--small bg-button--secondary" {{ stimulus_target('step-input', 'buttonInc') }} {{ stimulus_action('step-input', 'increase') }}>
+
<div class="px-2" {{ stimulus_target('step-input', 'display') }}></div>
<button type="button" class="button button--small button--primary" {{ stimulus_target('step-input', 'buttonInc') }} {{ stimulus_action('step-input', 'increase') }}>
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="40" y1="128" x2="216" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="128" y1="40" x2="128" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
</button>
</div>
</div>
+5 -8
View File
@@ -1,5 +1,6 @@
<div id="htmx-modal" class="fixed inset-0 w-full h-full z-50" {{ stimulus_controller('modal') }} {{ stimulus_action('modal', 'close', 'modal-close@window') }}>
<div class="absolute inset-0 w-full h-full bg-black/80" {{ stimulus_action('modal', 'close', 'click') }}></div>
<div id="htmx-modal"
class="fixed inset-0 w-full h-full z-50" {{ stimulus_controller('modal') }} {{ stimulus_action('modal', 'close', 'modal-close@window') }}>
<div class="absolute inset-0 w-full h-full backdrop-blur-sm" {{ stimulus_action('modal', 'close', 'click') }}></div>
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 px-4 w-full max-w-2xl">
<div class="relative bg-white py-8 rounded-md">
<div class="flex justify-between pb-4 px-8">
@@ -7,9 +8,9 @@
{% block title %}{% endblock %}
</div>
<button type="button"
class="inline-block"
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10"
{{ stimulus_action('modal', 'close') }}>
{{ icon('close', 'w-6 h-6')}}
<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>
<div class="max-h-96 overflow-y-scroll">
@@ -17,10 +18,6 @@
{% block content %}{% endblock %}
</div>
</div>
<div class="htmx-indicator absolute top-0 left-0 inset-0 bg-white/90 flex items-center justify-center"
id="htmx-modal-indicator">
{% include '_partials/_spinner.html.twig' with { 'class': 'w-8 h-8'} %}
</div>
</div>
</div>
</div>
+14 -19
View File
@@ -1,26 +1,21 @@
{% extends 'base.html.twig' %}
{% block body %}
{# Global toast controller - persists across all HTMX swaps and page navigations #}
<div {{ stimulus_controller('toast') }}></div>
<div class="{{ html_classes('pb-8', { 'pt-24': is_granted('ROLE_USER') }) }}" {{ stimulus_controller('loading', [], { 'hidden': 'invisible' }) }}>
<div class="fixed inset-x-0 top-0 z-20">
<div class="max-w-screen-xl mx-auto flex items-center justify-between bg-primary-dark text-white">
{% if is_granted('ROLE_USER') %}
<nav class="w-full flex items-center relative">
<div class="flex-1 block text-xl px-4">My E&amp;P</div>
<div>
{{ knp_menu_render(knp_menu_get('main')) }}
</div>
</nav>
{% endif %}
<div class="w-screen h-screen {% block background %}bg-outer bg-outer--2{% endblock %}" {{ stimulus_controller('loading', [], { 'hidden': 'invisible' }) }}>
<div class="max-w-screen-xl h-screen ml-auto flex flex-col overflow-hidden bg-inner">
<div class="shrink-0 z-20 py-2 px-4 bg-white shadow-md">
{% block header %}
{% include '_partials/_header.html.twig' %}
{% endblock %}
</div>
<div class="flex-1 flex flex-col min-h-0 relative z-10">
<div class="flex-1 overflow-y-auto">
{% block content %}{% endblock %}
</div>
</div>
{% block footer %}{% endblock %}
</div>
<div class="max-w-screen-xl mx-auto">
{% block content %}{% endblock %}
{% include '_partials/_loading_indicator.html.twig' %}
</div>
<div data-iframe-size></div>
{% include '_partials/_loading_indicator.html.twig' %}
</div>
<div {{ stimulus_controller('toast') }}></div>
{% endblock %}
+18
View File
@@ -0,0 +1,18 @@
{% extends 'base.html.twig' %}
{% block body %}
<div class="w-screen h-screen {% block background %}bg-outer bg-outer--2{% endblock %}" {{ stimulus_controller('loading', [], { 'hidden': 'invisible' }) }}>
<div class="max-w-screen-xl h-screen ml-auto flex flex-col overflow-hidden bg-inner">
<div class="shrink-0 z-20 py-2 px-4 bg-white shadow-md">
{% block header %}
{% include '_partials/_header.html.twig' %}
{% endblock %}
</div>
<div class="flex-1 flex flex-col min-h-0 relative z-10">
{% block content %}{% endblock %}
</div>
</div>
{% include '_partials/_loading_indicator.html.twig' %}
</div>
<div {{ stimulus_controller('toast') }}></div>
{% endblock %}
-97
View File
@@ -1,97 +0,0 @@
{% extends 'layout.html.twig' %}
{% block content %}
<div class="grid md:grid-cols-2 gap-x-16 gap-y-4">
<div>
{% include '_partials/_flashes.html.twig' %}
{{ form_start(personalDataForm, { 'attr': { 'data-action': 'loading#toggle' } }) }}
<div class="pb-4">
<button type="submit" class="button bg-button">
Speichern
</button>
</div>
<h1 class="text-3xl font-semibold pb-2">
Persönliche Daten
</h1>
<div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4">
<div>
<p class="text-lg font-semibold">
Vorname
</p>
{{ personalData.firstName }}
</div>
<div>
<p class="text-lg font-semibold">
Nachname
</p>
{{ personalData.name }}
</div>
<div>
<p class="text-lg font-semibold">
Gender
</p>
{{ personalData.gender|map_gender }}
</div>
<div>
<p class="text-lg font-semibold">
Geburtsdatum
</p>
{{ personalData.dateOfBirth|date('d.m.Y') }}
</div>
<div>
<p class="text-lg font-semibold">
Nationaliät
</p>
{{ personalData.nationality|map_nationality|default('-') }}
</div>
</div>
<h2 class="text-xl font-semibold pb-2">
Anschrift
</h2>
<div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4">
<div class="col-span-2">
{{ form_row(personalDataForm.street) }}
</div>
{{ form_row(personalDataForm.postCode) }}
{{ form_row(personalDataForm.city) }}
{{ form_row(personalDataForm.country) }}
</div>
<h2 class="text-xl font-semibold pb-2">
Kontakt
</h2>
<div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4">
<div class="col-span-2">
{{ form_row(personalDataForm.email) }}
</div>
{{ form_row(personalDataForm.phone) }}
{{ form_row(personalDataForm.mobile) }}
</div>
<button type="submit" class="button bg-button">
Speichern
</button>
{{ form_rest(personalDataForm) }}
{{ form_end(personalDataForm) }}
</div>
<div>
<div id="newsletter">
<h2 class="text-2xl font-semibold pb-4">
Newsletter
</h2>
<p class="pb-4">
Du bist aktuell {% if not personalData.communication.newsletter %}<strong>nicht</strong> {% endif%} zum Newsletter
angemeldet.
</p>
<button type="button"
class="relative button bg-button bg-button--secondary"
hx-post="{{ path('app_personal_data_newsletter') }}"
hx-target="#newsletter"
hx-select="#newsletter"
hx-indicator="#button-indicator"
hx-swap="innerHTML">
{% if personalData.communication.newsletter %}jetzt abmelden{% else %}jetzt anmelden{% endif %}
<span class="htmx-indicator absolute top-0 left-0 inset-0 bg-secondary/80 flex items-center justify-center" id="button-indicator">{% include '_partials/_spinner.html.twig' with { 'class': 'w-6 h-6 text-white' } %}</span>
</button>
</div>
</div>
</div>
{% endblock %}
+24 -22
View File
@@ -1,28 +1,30 @@
{% extends 'layout.html.twig' %}
{% block content %}
<h1 class="mb-4">
Registrierung MyE&amp;P
</h1>
{% include '_partials/_flashes.html.twig' %}
{{ form_start(form, { 'attr': { 'data-action': 'loading#toggle' } }) }}
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 mb-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 px-4 lg:px-8 py-8 lg:py-16">
<div>
{{ form_row(form.firstName) }}
{{ form_row(form.name) }}
{{ form_row(form.email) }}
{{ form_row(form.gender) }}
<h1 class="text-white uppercase pb-4 mb-8 border-b border-primary-bg/40">
Registrierung
<br>
MyE&amp;P
</h1>
{% include '_partials/_flashes.html.twig' %}
<div class="pb-4">
{{ form_start(form, { 'attr': { 'data-action': 'loading#toggle' } }) }}
{{ form_row(form.firstName, { 'label_attr': { 'class': 'text-white' } }) }}
{{ form_row(form.name, { 'label_attr': { 'class': 'text-white' } }) }}
{{ form_row(form.email, { 'label_attr': { 'class': 'text-white' } }) }}
{{ form_row(form.gender, { 'label_attr': { 'class': 'text-white' } }) }}
<button type="submit" class="button button--primary">
Jetzt registrieren
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
</div>
<a href="{{ path('app_login') }}"
class="text-white underline">
Zum Login
</a>
</div>
</div>
<div class="flex flex-col lg:flex-row space-y-2 lg:space-y-0 lg:space-x-2">
<button type="submit" class="button bg-button">
registrieren
</button>
<a href="{{ path('app_login') }}"
class="button bg-button bg-button--secondary">
zum Login
</a>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}
{% endblock %}
+29 -23
View File
@@ -1,27 +1,33 @@
{% extends 'layout.html.twig' %}
{% block content %}
<h1 class="mb-4">
Passwort vergessen
</h1>
{% include '_partials/_flashes.html.twig' %}
{{ form_start(form, { 'attr': { 'data-action': 'loading#toggle' } }) }}
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 mb-4">
{{ form_row(form.email) }}
<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">
Passwort
<br>
zurücksetzen
</h1>
{% include '_partials/_flashes.html.twig' %}
<div class="pb-4">
{{ form_start(form, { 'attr': { 'data-action': 'loading#toggle' } }) }}
{{ form_row(form.email, { 'label_attr': { 'class': 'text-white' } }) }}
<button type="submit" class="button button--primary">
Passwort zurücksetzen
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
</div>
<div class="flex space-x-2">
<a href="{{ path('app_login') }}"
class="text-white underline">
Zum Login
</a>
<a href="{{ path('app_registration') }}"
class="text-white underline">
Jetzt registrieren
</a>
</div>
</div>
</div>
<div class="flex flex-col lg:flex-row space-y-2 lg:space-y-0 lg:space-x-2 mt-4">
<button type="submit" class="button bg-button">
Passwort erneuern
</button>
<a href="{{ path('app_login') }}"
class="button bg-button bg-button--secondary">
zum Login
</a>
<a href="{{ path('app_registration') }}"
class="button bg-button bg-button--secondary">
registrieren
</a>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}
{% endblock %}
+52 -48
View File
@@ -1,73 +1,77 @@
{% extends 'layout.html.twig' %}
{% block content %}
<h1 class="mb-4">
Login MyE&amp;P
</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<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">
Login<br>MyE&amp;P
</h1>
{% include '_partials/_flashes.html.twig' %}
{% 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 lg:flex-row space-y-2 lg:space-y-0 lg:space-x-2 mt-4">
<button type="submit" class="button bg-button">
Login
</button>
<a href="{{ path('app_reset_password') }}"
class="button bg-button bg-button--secondary">
Passwort vergessen?
</a>
<a href="{{ path('app_registration') }}"
class="button bg-button bg-button--secondary">
registrieren
</a>
</div>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
</form>
<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>
<div class="flex flex-col lg:flex-row space-y-2 lg:space-y-0 lg:space-x-2 mt-4">
<button type="submit" class="button button--primary">
Login
</button>
</div>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
</form>
</div>
<div class="flex space-x-2">
<a href="{{ path('app_reset_password') }}"
class="text-white underline">
Passwort vergessen?
</a>
<a href="{{ path('app_registration') }}"
class="text-white underline">
Jetzt registrieren
</a>
</div>
</div>
<div>
<h3 class="mb-2">
<h3 class="mb-2 text-white">
Neu!
</h3>
<p class="mb-4">
<p class="mb-4 text-white">
Melde dich jetzt bei MyEP an und du kannst jederzeit auf deine aktuellen Buchungen zugreifen und
diese abändern. Egal ob du einen Kurs oder eine Skipasserweiterung hinzufügen/entfernen möchtest,
oder das vegetarische Essen auswählen willst. Im neuen Portal ist das nun kostenlos bis 7 Tage vor
Reisebeginn möglich!
</p>
<h3 class="mb-2">
<h3 class="mb-2 text-white">
Noch keine Zugangsdaten?
</h3>
<p class="mb-2">
<p class="mb-2 text-white">
Dann gib deine E-Mail ein, mit der Du gebucht hast und klicke auf "Passwort vergessen"! Und wir
senden dir ein E-Mail mit einem Passwort-Link!
</p>
<p>
<p class="text-white">
Ein LogIn ist nur möglich wenn Eure Daten schon durch eine Buchung oder Anfrage bei uns
angelegt wurden. Wenn Ihr auch Kunde werden möchtet, dann schickt uns Eure Interessen über unser
<a href="https://www.ep-reisen.de/skireisen/unternehmen/kontakt/kontaktformular/" title="Kontaktformular" class="underline">Kontaktformular</a>.