wip: group inquiries and bookings

This commit is contained in:
Björn Fromme committed 2026-07-14 11:29:43 +02:00
1 parent 6a6051e92b
commit 505bda46cf
115 files changed
+8107 -424

No files matched your search

+188 -185
View File
@@ -8,7 +8,6 @@
</div>
{{ form_row(form.calendarCode) }}
{{ form_row(form.cmsCode) }}
{{ form_row(form.minPaxSummer) }}
{{ form_row(form.maxAdolescentAge) }}
{{ form_row(form.currency) }}
</div>
@@ -54,118 +53,128 @@
<hr class="my-8">
<h2 class="text-xl font-bold">
Preise
</h2>
<div class="data-table-wrapper mb-4">
<div class="data-table-wrapper__inner text-sm">
<table class="data-table">
<thead>
<tr>
<th>
von
</th>
<th>
bis
</th>
<th>
Typ
</th>
<th>
Saison
</th>
<th>
Pax bis
</th>
<th>
Mindestmiete pro Nacht
</th>
<th>
weitere Personen pro Nacht
</th>
<th>
Unterbelegung
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for price in accommodation.accommodationPrices %}
<tr>
<td>
{{ price.dateFrom | date('d.m.Y') }}
</td>
<td>
{{ price.dateTo | date('d.m.Y') }}
</td>
<td>
{% if price.type is not null %}
{% if price.type.value == 'discount' %}
{{ icon('discount', 'w-5 h-5') }}
{% elseif price.type.value == 'override' %}
{{ icon('stack', 'w-5 h-5') }}
{% endif %}
{% endif %}
</td>
<td>
{{ price.season.token | trans }}
</td>
<td>
{{ price.includedPax }}
</td>
<td>
{{ (price.pricePerNight / 100) | format_currency(accommodation.currency) }}
</td>
<td>
{{ (price.priceAdditionalPerson / 100) | format_currency(accommodation.currency) }}
</td>
<td>
{% if price.acceptUndersubscription %}
{{ icon('check', 'w-4 h-4') }}
{% endif %}
</td>
<td class="text-right">
<twig:dropdown>
<twig:dropdown:link url="{{ path('app_admin_accommodationprice_edit', { 'id': price.id }) }}">
bearbeiten
</twig:dropdown:link>
<twig:dropdown:link url="{{ path('app_admin_accommodationprice_duplicate', { 'id': price.id }) }}">
duplizieren
</twig:dropdown:link>
<twig:dropdown:hxbutton url="{{ path('app_admin_accommodationprice_delete', { 'id': price.id }) }}" warning>
löschen
</twig:dropdown:hxbutton>
</twig:dropdown>
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="text-center">
Keine Daten
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="flex justify-end">
<a href="{{ path('app_admin_accommodationprice_create', { 'id': accommodation.id }) }}" class="button button--primary button--small">
neu
</a>
</div>
<hr class="my-8">
<h2 class="text-xl font-bold">
Verpflegungsleistungen
</h2>
<div class="data-table-wrapper mb-4">
<section id="prices">
<h2 class="text-xl font-bold">
Preise
</h2>
<div class="data-table-wrapper mb-4">
<div class="data-table-wrapper__inner text-sm">
<table class="data-table">
<thead>
<tr>
<th>
von
</th>
<th>
bis
</th>
<th>
Typ
</th>
<th>
Saison
</th>
<th>
Pax bis
</th>
<th>
Mindestmiete pro Nacht
</th>
<th>
weitere Personen pro Nacht
</th>
<th>
Unterbel.
</th>
<th>
Kurzzeit
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for price in accommodation.accommodationPrices %}
<tr>
<td>
{{ price.dateFrom | date('d.m.Y') }}
</td>
<td>
{{ price.dateTo | date('d.m.Y') }}
</td>
<td>
{% if price.type is not null %}
{% if price.type.value == 'discount' %}
{{ icon('discount', 'w-5 h-5') }}
{% elseif price.type.value == 'override' %}
{{ icon('stack', 'w-5 h-5') }}
{% endif %}
{% endif %}
</td>
<td>
{{ price.season.token | trans }}
</td>
<td>
{{ price.includedPax }}
</td>
<td>
{{ (price.pricePerNight / 100) | format_currency(accommodation.currency) }}
</td>
<td>
{{ (price.priceAdditionalPerson / 100) | format_currency(accommodation.currency) }}
</td>
<td>
{% if price.acceptUndersubscription %}
{{ icon('check', 'w-4 h-4') }}
{% endif %}
</td>
<td>
{% if price.acceptShortterm %}
{{ icon('check', 'w-4 h-4') }}
{% endif %}
</td>
<td class="text-right">
<twig:dropdown>
<twig:dropdown:link url="{{ path('app_admin_accommodationprice_edit', { 'id': price.id }) }}">
bearbeiten
</twig:dropdown:link>
<twig:dropdown:link url="{{ path('app_admin_accommodationprice_duplicate', { 'id': price.id }) }}">
duplizieren
</twig:dropdown:link>
<twig:dropdown:hxbutton url="{{ path('app_admin_accommodationprice_delete', { 'id': price.id }) }}" warning>
löschen
</twig:dropdown:hxbutton>
</twig:dropdown>
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="text-center">
Keine Daten
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="flex justify-end">
<a href="{{ path('app_admin_accommodationprice_create', { 'id': accommodation.id }) }}" class="button button--primary button--small">
neu
</a>
</div>
</section>
<hr class="my-8">
<section id="board-services">
<h2 class="text-xl font-bold">
Verpflegungsleistungen
</h2>
<div class="data-table-wrapper mb-4">
<div class="data-table-wrapper__inner text-sm">
<table class="data-table">
<thead>
<tr>
<th>
Datums-Bereich
</th>
@@ -178,18 +187,14 @@
<th></th>
</tr>
</thead>
<tbody data-controller="sortable" data-sortable-url-value="{{ path('app_admin_boardservice_sort', { 'id': accommodation.id }) }}">
<tbody>
{% for service in accommodation.boardServices %}
<tr data-sortable-target="item">
<td class="cursor-grab w-6" data-sortable-target="handle">
{{ icon('draggable', 'w-4 h-4') }}
</td>
<tr>
<td>
{{ service.dateFrom | date('d.m.Y') }} - {{ service.dateTo | date('d.m.Y') }}
</td>
<td>
{{ service.label }}
<input type="hidden" name="sorting[{{ service.id }}]" value="{{ loop.index }}" data-sortable-target="positionField" data-id="{{ service.id }}">
</td>
<td>
{{ (service.price / 100) | format_currency(accommodation.currency) }}
@@ -210,7 +215,7 @@
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center">
<td colspan="4" class="text-center">
Keine Daten
</td>
</tr>
@@ -224,79 +229,77 @@
neu
</a>
</div>
</section>
<hr class="my-8">
<h2 class="text-xl font-bold">
Optionale Zusatzleistungen
</h2>
<div class="data-table-wrapper mb-4">
<div class="data-table-wrapper__inner text-sm">
<table class="data-table">
<thead>
<tr>
<th></th>
<th>
Datums-Bereich
</th>
<th>
Bezeichnung
</th>
<th>
Preistyp
</th>
<th>
Preis
</th>
<th></th>
</tr>
</thead>
<tbody data-controller="sortable" data-sortable-url-value="{{ path('app_admin_additionalservice_sort', { 'id': accommodation.id }) }}">
{% for service in accommodation.additionalServices %}
<tr data-sortable-target="item">
<td class="cursor-grab w-6" data-sortable-target="handle">
{{ icon('draggable', 'w-4 h-4') }}
</td>
<td>
{{ service.dateFrom | date('d.m.Y') }} - {{ service.dateTo | date('d.m.Y') }}
</td>
<td>
{{ service.label }}
<input type="hidden" name="sorting[{{ service.id }}]" value="{{ loop.index }}" data-sortable-target="positionField" data-id="{{ service.id }}">
</td>
<td>
{{ service.type.label | trans }}
</td>
<td>
{{ (service.price / 100) | format_currency(accommodation.currency) }}
</td>
<td class="text-right">
<twig:dropdown>
<twig:dropdown:link url="{{ path('app_admin_additionalservice_edit', { 'id': service.id }) }}">
bearbeiten
</twig:dropdown:link>
<twig:dropdown:link url="{{ path('app_admin_additionalservice_duplicate', { 'id': service.id }) }}">
duplizieren
</twig:dropdown:link>
<twig:dropdown:hxbutton url="{{ path('app_admin_additionalservice_delete', { 'id': service.id }) }}" warning>
löschen
</twig:dropdown:hxbutton>
</twig:dropdown>
</td>
</tr>
{% else %}
<section id="additional-services">
<h2 class="text-xl font-bold">
Optionale Zusatzleistungen
</h2>
<div class="data-table-wrapper mb-4">
<div class="data-table-wrapper__inner text-sm">
<table class="data-table">
<thead>
<tr>
<td colspan="6" class="text-center">
Keine Daten
</td>
<th>
Datums-Bereich
</th>
<th>
Bezeichnung
</th>
<th>
Preistyp
</th>
<th>
Preis
</th>
<th></th>
</tr>
{% endfor %}
</tbody>
</table>
</thead>
<tbody>
{% for service in accommodation.additionalServices %}
<tr>
<td>
{{ service.dateFrom | date('d.m.Y') }} - {{ service.dateTo | date('d.m.Y') }}
</td>
<td>
{{ service.label }}
</td>
<td>
{{ service.type.label | trans }}
</td>
<td>
{{ (service.price / 100) | format_currency(accommodation.currency) }}
</td>
<td class="text-right">
<twig:dropdown>
<twig:dropdown:link url="{{ path('app_admin_additionalservice_edit', { 'id': service.id }) }}">
bearbeiten
</twig:dropdown:link>
<twig:dropdown:link url="{{ path('app_admin_additionalservice_duplicate', { 'id': service.id }) }}">
duplizieren
</twig:dropdown:link>
<twig:dropdown:hxbutton url="{{ path('app_admin_additionalservice_delete', { 'id': service.id }) }}" warning>
löschen
</twig:dropdown:hxbutton>
</twig:dropdown>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center">
Keine Daten
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="flex justify-end">
<a href="{{ path('app_admin_additionalservice_create', { 'id': accommodation.id }) }}" class="button button--primary button--small">
neu
</a>
</div>
<div class="flex justify-end">
<a href="{{ path('app_admin_additionalservice_create', { 'id': accommodation.id }) }}" class="button button--primary button--small">
neu
</a>
</div>
</section>
@@ -26,7 +26,9 @@
{% for accommodation in accommodations %}
<tr>
<td>
{{ accommodation.name }}
<a href="{{ path('app_admin_accommodation_edit', { 'id': accommodation.id, 'r': return_url() }) }}">
{{ accommodation.name }}
</a>
</td>
<td>
{{ accommodation.calendarCode }}
@@ -8,8 +8,7 @@
{{ form_start(form) }}
<div class="grid lg:grid-cols-2 gap-y-4 lg:gap-x-8">
{{ form_row(form.name) }}
{{ form_row(form.code) }}
{{ form_row(form.minPaxSummer) }}
{{ form_row(form.calendarCode) }}
{{ form_row(form.maxAdolescentAge) }}
{{ form_row(form.currency) }}
</div>
@@ -0,0 +1,51 @@
{% extends 'layout_admin.html.twig' %}
{% block content %}
<twig:page:heading>Neue Buchung</twig:page:heading>
{% form_theme form 'forms_admin.html.twig' %}
{{ form_start(form) }}
<div class="grid lg:grid-cols-2 gap-y-4 lg:gap-x-8">
<div class="lg:col-span-2">
{{ form_row(form.accommodation) }}
</div>
<h2 class="lg:col-span-2 text-base font-bold pt-2">Kontaktdaten</h2>
{{ form_row(form.groupName) }}
{{ form_row(form.salutation) }}
{{ form_row(form.firstName) }}
{{ form_row(form.lastName) }}
{{ form_row(form.email) }}
{{ form_row(form.phone) }}
{{ form_row(form.street) }}
{{ form_row(form.zip) }}
{{ form_row(form.city) }}
<h2 class="lg:col-span-2 text-base font-bold pt-2">Reisedaten</h2>
{{ form_row(form.dateFrom) }}
{{ form_row(form.dateTo) }}
{{ form_row(form.paxCount) }}
{{ form_row(form.minorsCount) }}
{{ form_row(form.childrenCount) }}
<h2 class="lg:col-span-2 text-base font-bold pt-2">Status & Rabatt</h2>
<div class="lg:col-span-2">
{{ form_row(form.isInquiry) }}
</div>
{{ form_row(form.discount) }}
<div class="lg:col-span-2">
{{ form_row(form.remarks) }}
</div>
</div>
<div class="flex justify-between pt-8">
<a href="{{ path('app_admin_accommodationbooking') }}" class="button button--secondary button--small">
zurück
</a>
<button type="submit" class="button button--primary button--small">
speichern
</button>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}
@@ -0,0 +1,60 @@
{% extends 'layout_admin.html.twig' %}
{% block content %}
<twig:page:heading>Buchung {{ booking.groupName }}</twig:page:heading>
<p class="text-sm text-gray-500 mb-6">{{ booking.accommodation.name }}</p>
{% form_theme form 'forms_admin.html.twig' %}
{{ form_start(form) }}
<div class="grid lg:grid-cols-2 gap-y-4 lg:gap-x-8">
<h2 class="lg:col-span-2 text-base font-bold">Kontaktdaten</h2>
{{ form_row(form.groupName) }}
{{ form_row(form.salutation) }}
{{ form_row(form.firstName) }}
{{ form_row(form.lastName) }}
{{ form_row(form.email) }}
{{ form_row(form.phone) }}
{{ form_row(form.street) }}
{{ form_row(form.zip) }}
{{ form_row(form.city) }}
<h2 class="lg:col-span-2 text-base font-bold pt-2">Reisedaten</h2>
{{ form_row(form.dateFrom) }}
{{ form_row(form.dateTo) }}
{{ form_row(form.paxCount) }}
{{ form_row(form.minorsCount) }}
{{ form_row(form.childrenCount) }}
{% if form.boardService is defined or form.selectedAdditionalServices is defined %}
<h2 class="lg:col-span-2 text-base font-bold pt-2">Leistungen</h2>
{% if form.boardService is defined %}
{{ form_row(form.boardService) }}
{% endif %}
{% if form.selectedAdditionalServices is defined %}
<div class="lg:col-span-2">
{{ form_row(form.selectedAdditionalServices) }}
</div>
{% endif %}
{% endif %}
<h2 class="lg:col-span-2 text-base font-bold pt-2">Status & Rabatt</h2>
<div class="lg:col-span-2">
{{ form_row(form.isInquiry) }}
</div>
{{ form_row(form.discount) }}
<div class="lg:col-span-2">
{{ form_row(form.remarks) }}
</div>
</div>
<div class="flex justify-between pt-8">
<a href="{{ path('app_admin_accommodationbooking_show', { 'id': booking.id }) }}" class="button button--secondary button--small">
zurück
</a>
<button type="submit" class="button button--primary button--small">
speichern
</button>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}
@@ -0,0 +1,94 @@
{% extends 'layout_admin.html.twig' %}
{% block content %}
<twig:page:heading>Buchungen & Anfragen</twig:page:heading>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner text-sm">
<table class="data-table">
<thead>
<tr>
<th>
{{ knp_pagination_sortable(pagination, 'Eingang', 'booking.createdAt') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Gruppenhaus', 'accommodation.name') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Gruppe', 'booking.groupName') }}
</th>
<th>
{{ knp_pagination_sortable(pagination, 'Anreise', 'booking.dateFrom') }}
</th>
<th>
Personen
</th>
<th>
Status
</th>
<th>
Rabatt
</th>
<th></th>
</tr>
</thead>
<tbody>
{% for booking in pagination %}
<tr>
<td>
{{ booking.createdAt | date('d.m.Y, H:i') }}
</td>
<td>
{{ booking.accommodation.name }}
</td>
<td>
{{ booking.groupName }}
</td>
<td>
{{ booking.dateFrom | date('d.m.Y') }} – {{ booking.dateTo | date('d.m.Y') }}
</td>
<td>
{{ booking.paxCount }}
</td>
<td>
{% if booking.isInquiry %}
Anfrage
{% else %}
Buchung
{% endif %}
</td>
<td>
{% if booking.discount is not null %}
{{ booking.discount }}&nbsp;%
{% else %}
–
{% endif %}
</td>
<td class="text-right">
<twig:dropdown>
<twig:dropdown:link url="{{ path('app_admin_accommodationbooking_show', { 'id': booking.id, 'r': return_url() }) }}">
Details
</twig:dropdown:link>
<twig:dropdown:link url="{{ path('app_admin_accommodationbooking_edit', { 'id': booking.id }) }}">
bearbeiten
</twig:dropdown:link>
</twig:dropdown>
</td>
</tr>
{% else %}
<tr>
<td colspan="8">
Keine Daten...
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(pagination) }}
</div>
</div>
<div class="flex justify-end">
<a href="{{ path('app_admin_accommodationbooking_create') }}" class="button button--primary button--small">
neu
</a>
</div>
{% endblock %}
@@ -0,0 +1,200 @@
{% extends 'layout_admin.html.twig' %}
{% block content %}
<twig:page:heading>
{% if booking.isInquiry %}Anfrage{% else %}Buchung{% endif %}
{{ booking.groupName }}
</twig:page:heading>
<div class="grid lg:grid-cols-2 gap-8">
<div>
<h2 class="text-lg font-bold mb-4">Kontakt</h2>
<dl class="grid grid-cols-2 gap-y-2 text-sm">
<dt class="font-medium">Gruppenname</dt>
<dd>{{ booking.groupName }}</dd>
<dt class="font-medium">Anrede</dt>
<dd>{{ booking.salutation | default('–') }}</dd>
<dt class="font-medium">Name</dt>
<dd>{{ booking.firstName }} {{ booking.lastName }}</dd>
<dt class="font-medium">E-Mail</dt>
<dd>{{ booking.email }}</dd>
<dt class="font-medium">Telefon</dt>
<dd>{{ booking.phone | default('–') }}</dd>
<dt class="font-medium">Adresse</dt>
<dd>
{% if booking.street %}
{{ booking.street }}<br>
{{ booking.zip }} {{ booking.city }}
{% else %}
–
{% endif %}
</dd>
</dl>
</div>
<div>
<h2 class="text-lg font-bold mb-4">Reisedaten</h2>
<dl class="grid grid-cols-2 gap-y-2 text-sm">
<dt class="font-medium">Gruppenhaus</dt>
<dd>{{ booking.accommodation.name }}</dd>
<dt class="font-medium">Anreise</dt>
<dd>{{ booking.dateFrom | date('d.m.Y') }}</dd>
<dt class="font-medium">Abreise</dt>
<dd>{{ booking.dateTo | date('d.m.Y') }}</dd>
<dt class="font-medium">Nächte</dt>
<dd>{{ booking.nights }}</dd>
<dt class="font-medium">Anzahl Personen</dt>
<dd>{{ booking.paxCount }}</dd>
{% if booking.minorsCount > 0 %}
<dt class="font-medium">davon Kinder (0–3 Jahre)</dt>
<dd>{{ booking.minorsCount }}</dd>
{% endif %}
{% if booking.childrenCount > 0 %}
<dt class="font-medium">davon Kinder (4–{{ booking.accommodation.maxAdolescentAge }} Jahre)</dt>
<dd>{{ booking.childrenCount }}</dd>
{% endif %}
<dt class="font-medium">Status</dt>
<dd>{% if booking.isInquiry %}Anfrage{% else %}Buchung{% endif %}</dd>
</dl>
</div>
{% if booking.boardServiceLabel or booking.additionalServices | length > 0 %}
<div>
<h2 class="text-lg font-bold mb-4">Gebuchte Leistungen</h2>
<dl class="grid grid-cols-2 gap-y-2 text-sm">
{% if booking.boardServiceLabel %}
<dt class="font-medium">Verpflegung</dt>
<dd>{{ booking.boardServiceLabel }}</dd>
{% endif %}
{% for service in booking.additionalServices %}
<dt class="font-medium">{{ loop.first ? 'Zusatzleistungen' : '' }}</dt>
<dd>{{ service.label }}</dd>
{% endfor %}
</dl>
</div>
{% endif %}
{% if priceBreakdown is not null %}
<div>
<h2 class="text-lg font-bold mb-4">Preise</h2>
{% set currency = booking.accommodation.currency %}
<table class="w-full text-sm">
{% if priceBreakdown.basePrice > 0 %}
<tr>
<td class="py-1">
Basispreis
<span class="text-xs text-gray-500">für {{ priceBreakdown.includedPax }} Pers.</span>
</td>
<td class="py-1 text-right whitespace-nowrap">{{ (priceBreakdown.basePrice / 100) | format_currency(currency) }}</td>
</tr>
{% endif %}
{% if priceBreakdown.additionalPersonsPrice > 0 %}
<tr>
<td class="py-1">
Aufpreis Personenzahl
<span class="text-xs text-gray-500">für {{ priceBreakdown.effectivePax - priceBreakdown.includedPax }} Pers.</span>
</td>
<td class="py-1 text-right whitespace-nowrap">{{ (priceBreakdown.additionalPersonsPrice / 100) | format_currency(currency) }}</td>
</tr>
{% endif %}
{% if priceBreakdown.shortTermSurcharge > 0 %}
<tr>
<td class="py-1">Aufpreis Kurzzeit</td>
<td class="py-1 text-right whitespace-nowrap">{{ (priceBreakdown.shortTermSurcharge / 100) | format_currency(currency) }}</td>
</tr>
{% endif %}
{% if priceBreakdown.boardPrice > 0 %}
<tr>
<td class="py-1">Verpflegung</td>
<td class="py-1 text-right whitespace-nowrap">{{ (priceBreakdown.boardPrice / 100) | format_currency(currency) }}</td>
</tr>
{% endif %}
{% if priceBreakdown.undersubscriptionSurcharge > 0 %}
<tr>
<td class="py-1">Verpflegungs-Aufschlag für Gruppen unter {{ priceBreakdown.undersubscriptionThreshold }} Personen</td>
<td class="py-1 text-right whitespace-nowrap">{{ (priceBreakdown.undersubscriptionSurcharge / 100) | format_currency(currency) }}</td>
</tr>
{% endif %}
{% for service in priceBreakdown.serviceDetails %}
{% if service.price > 0 %}
<tr>
<td class="py-1">{{ service.label }}</td>
<td class="py-1 text-right whitespace-nowrap">{{ (service.price / 100) | format_currency(currency) }}</td>
</tr>
{% endif %}
{% endfor %}
{% if priceBreakdown.runningCosts > 0 %}
<tr>
<td class="py-1">Strom- und Abfallgebühren</td>
<td class="py-1 text-right whitespace-nowrap">{{ (priceBreakdown.runningCosts / 100) | format_currency(currency) }}</td>
</tr>
{% endif %}
<tr class="border-t border-gray-200 font-semibold">
<td class="pt-2">
Gesamtpreis
<span class="block text-xs font-normal text-gray-500">zzgl. ortsabhängiger Gebühren</span>
</td>
<td class="pt-2 text-right whitespace-nowrap">{{ (priceBreakdown.total / 100) | format_currency(currency) }}</td>
</tr>
{% if booking.discount is not null %}
{% set discountAmount = (priceBreakdown.total * booking.discount / 100) | round | number_format(0, '.', '') %}
{% set discountedTotal = priceBreakdown.total - discountAmount %}
<tr class="text-green-700">
<td class="pt-1">Rabatt {{ booking.discount }}&nbsp;%</td>
<td class="pt-1 text-right whitespace-nowrap">– {{ (discountAmount / 100) | format_currency(currency) }}</td>
</tr>
<tr class="border-t border-gray-200 font-bold text-green-700">
<td class="pt-2">Gesamtpreis nach Rabatt</td>
<td class="pt-2 text-right whitespace-nowrap">{{ (discountedTotal / 100) | format_currency(currency) }}</td>
</tr>
{% endif %}
</table>
</div>
{% endif %}
{% if booking.remarks %}
<div class="lg:col-span-2">
<h2 class="text-lg font-bold mb-4">Bemerkungen</h2>
<p class="text-sm">{{ booking.remarks | nl2br }}</p>
</div>
{% endif %}
</div>
<div class="text-xs text-gray-400 mt-6">
Eingegangen: {{ booking.createdAt | date('d.m.Y, H:i') }}
{% if booking.updatedAt is not null and booking.updatedAt != booking.createdAt %}
· Aktualisiert: {{ booking.updatedAt | date('d.m.Y, H:i') }}
{% endif %}
</div>
<div class="mt-6 border-t border-gray-200 pt-6">
<h2 class="text-lg font-bold mb-2">Zugangslink</h2>
{% if accessLink %}
<p class="text-sm break-all">
<a href="{{ accessLink }}" target="_blank">{{ accessLink }}</a>
</p>
<p class="text-xs text-gray-500 mt-1">
Gültig bis {{ accessLinkExpiresAt | date('d.m.Y') }}
{% if accessLinkExpiresAt < date() %}
<span class="text-red-600 font-medium">(abgelaufen)</span>
{% endif %}
</p>
{% else %}
<p class="text-sm text-gray-500">Es wurde noch kein Zugangslink generiert.</p>
{% endif %}
<form method="post" action="{{ path('app_admin_accommodationbooking_generate_access_link', { 'id': booking.id }) }}" class="mt-2">
<button type="submit" class="button button--secondary button--small">
{{ accessLink ? 'Link erneut generieren' : 'Link generieren' }}
</button>
</form>
</div>
<div class="flex justify-between pt-8">
<a href="{{ returnUrl }}" class="button button--secondary button--small">
zurück
</a>
<a href="{{ path('app_admin_accommodationbooking_edit', { 'id': booking.id }) }}" class="button button--primary button--small">
bearbeiten
</a>
</div>
{% endblock %}
@@ -13,12 +13,15 @@
{{ form_row(form.includedPax) }}
{{ form_row(form.minNights) }}
{{ form_row(form.acceptUndersubscription) }}
{{ form_row(form.acceptShortTerm) }}
</div>
<div class="flex justify-between pt-8">
<a href="{{ path('app_admin_accommodation_edit', { 'id': accommodation.id }) }}" class="button button--secondary button--small">
<a href="{{ path('app_admin_accommodation_edit', { 'id': accommodation.id }) }}#prices"
class="button button--secondary button--small">
zurück
</a>
<button type="submit" class="button button--primary button--small">
<button type="submit"
class="button button--primary button--small">
speichern
</button>
</div>
@@ -4,16 +4,21 @@
<div class="grid lg:grid-cols-2 gap-y-4 lg:gap-x-8">
{{ form_row(form.label) }}
{{ form_row(form.type) }}
<div class="lg:col-span-2">
{{ form_row(form.description) }}
</div>
{{ form_row(form.price) }}
{{ form_row(form.selectionGroup) }}
{{ form_row(form.dateFrom) }}
{{ form_row(form.dateTo) }}
</div>
<div class="flex justify-between pt-8">
<a href="{{ path('app_admin_accommodation_edit', { 'id': accommodation.id }) }}" class="button button--secondary button--small">
<a href="{{ path('app_admin_accommodation_edit', { 'id': accommodation.id }) }}#additional-services"
class="button button--secondary button--small">
zurück
</a>
<button type="submit" class="button button--primary button--small">
<button type="submit"
class="button button--primary button--small">
speichern
</button>
</div>
@@ -4,14 +4,19 @@
<div class="grid lg:grid-cols-2 gap-y-4 lg:gap-x-8">
{{ form_row(form.label) }}
{{ form_row(form.price) }}
<div class="lg:col-span-2">
{{ form_row(form.description) }}
</div>
{{ form_row(form.dateFrom) }}
{{ form_row(form.dateTo) }}
</div>
<div class="flex justify-between pt-8">
<a href="{{ path('app_admin_accommodation_edit', { 'id': accommodation.id }) }}" class="button button--secondary button--small">
<a href="{{ path('app_admin_accommodation_edit', { 'id': accommodation.id }) }}#board-services"
class="button button--secondary button--small">
zurück
</a>
<button type="submit" class="button button--primary button--small">
<button type="submit"
class="button button--primary button--small">
speichern
</button>
</div>
@@ -0,0 +1,163 @@
{% extends 'email/layout.html.twig' %}
{% block body %}
<h1>
Neue Unterkunfts{% if booking.inquiry %}anfrage{% else %}buchung{% endif %}
</h1>
<p>
<strong>Art:</strong>
{% if booking.inquiry %}
Unverbindliche Anfrage
{% else %}
Verbindliche Buchung
{% endif %}
</p>
{% if accessLink %}
<p>
<a href="{{ accessLink }}" class="button">Kundenansicht öffnen</a>
</p>
{% endif %}
<h2>Unterkunft</h2>
<table>
<tr>
<td><strong>Name</strong></td>
<td>{{ booking.accommodation.name }}</td>
</tr>
<tr>
<td><strong>Anreise</strong></td>
<td>{{ booking.dateFrom|date('d.m.Y') }}</td>
</tr>
<tr>
<td><strong>Abreise</strong></td>
<td>{{ booking.dateTo|date('d.m.Y') }}</td>
</tr>
<tr>
<td><strong>Nächte</strong></td>
<td>{{ booking.nights }}</td>
</tr>
</table>
<h2>Personen</h2>
<table>
<tr>
<td><strong>Personen</strong></td>
<td>{{ booking.paxCount }}</td>
</tr>
{% if booking.minorsCount > 0 %}
<tr>
<td><strong>Kinder (0–3 J.)</strong></td>
<td>{{ booking.minorsCount }}</td>
</tr>
{% endif %}
{% if booking.childrenCount > 0 %}
<tr>
<td><strong>Kinder (4–{{ booking.accommodation.maxAdolescentAge }} J.)</strong></td>
<td>{{ booking.childrenCount }}</td>
</tr>
{% endif %}
</table>
{% if booking.boardServiceLabel %}
<h2>Verpflegung</h2>
<p>{{ booking.boardServiceLabel }}</p>
{% endif %}
{% if booking.additionalServices is not empty %}
<h2>Zusatzleistungen</h2>
<ul>
{% for service in booking.additionalServices %}
<li>{{ service.label }}</li>
{% endfor %}
</ul>
{% endif %}
{% if priceBreakdown is not empty %}
{% set currency = priceBreakdown.currency %}
<h2>Preise</h2>
<table>
{% if priceBreakdown.basePrice > 0 %}
<tr>
<td><strong>Basispreis</strong>{% if priceBreakdown.includedPax is defined %} ({{ priceBreakdown.includedPax }} Pers.){% endif %}</td>
<td>{{ (priceBreakdown.basePrice / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% if priceBreakdown.additionalPersonsPrice > 0 %}
<tr>
<td><strong>Aufpreis Personenzahl</strong>{% if priceBreakdown.includedPax is defined %} ({{ priceBreakdown.effectivePax - priceBreakdown.includedPax }} Pers.){% endif %}</td>
<td>{{ (priceBreakdown.additionalPersonsPrice / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% if priceBreakdown.shortTermSurcharge > 0 %}
<tr>
<td><strong>Aufpreis Kurzzeit</strong></td>
<td>{{ (priceBreakdown.shortTermSurcharge / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% if priceBreakdown.boardPrice > 0 %}
<tr>
<td><strong>Verpflegung</strong></td>
<td>{{ (priceBreakdown.boardPrice / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% if priceBreakdown.undersubscriptionSurcharge > 0 %}
<tr>
<td><strong>Verpflegungs-Aufschlag</strong> (unter {{ priceBreakdown.undersubscriptionThreshold }} Pers.)</td>
<td>{{ (priceBreakdown.undersubscriptionSurcharge / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% for service in priceBreakdown.serviceDetails %}
{% if service.price > 0 %}
<tr>
<td><strong>{{ service.label }}</strong></td>
<td>{{ (service.price / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% endfor %}
{% if priceBreakdown.runningCosts > 0 %}
<tr>
<td><strong>Strom- und Abfallgebühren</strong></td>
<td>{{ (priceBreakdown.runningCosts / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
<tr>
<td><strong>Gesamtpreis</strong> <em>(zzgl. ortsabhängiger Gebühren)</em></td>
<td><strong>{{ (priceBreakdown.total / 100)|format_currency(currency) }}</strong></td>
</tr>
</table>
{% endif %}
<h2>Kontakt</h2>
<table>
<tr>
<td><strong>Name</strong></td>
<td>{{ booking.salutation }} {{ booking.firstName }} {{ booking.lastName }}</td>
</tr>
<tr>
<td><strong>E-Mail</strong></td>
<td><a href="mailto:{{ booking.email }}">{{ booking.email }}</a></td>
</tr>
{% if booking.phone %}
<tr>
<td><strong>Telefon</strong></td>
<td>{{ booking.phone }}</td>
</tr>
{% endif %}
{% if booking.street or booking.zip or booking.city %}
<tr>
<td><strong>Adresse</strong></td>
<td>
{% if booking.street %}{{ booking.street }}<br>{% endif %}
{% if booking.zip or booking.city %}{{ booking.zip }} {{ booking.city }}{% endif %}
</td>
</tr>
{% endif %}
</table>
{% if booking.remarks %}
<h2>Anmerkungen</h2>
<p>{{ booking.remarks|nl2br }}</p>
{% endif %}
{% endblock %}
@@ -0,0 +1,66 @@
{% extends 'email/layout.html.twig' %}
{% block body %}
{% if accessLink %}
<h1>{{ booking.inquiry ? 'Dein Angebot ist bereit' : 'Deine Buchung ist bestätigt' }}</h1>
<p>
Hallo {{ booking.firstName }},
{% if booking.inquiry %}
vielen Dank für deine Anfrage! Wir haben sie geprüft — dein Angebot wartet auf dich.
{% else %}
vielen Dank! Deine Buchung ist bei uns eingegangen und bestätigt.
{% endif %}
</p>
<p>
<a href="{{ accessLink }}" class="button">
{{ booking.inquiry ? 'Angebot ansehen & bestätigen' : 'Buchung ansehen' }}
</a>
</p>
{% else %}
<h1>Deine Anfrage ist bei uns eingegangen</h1>
<p>
Hallo {{ booking.firstName }}, vielen Dank für deine Anfrage! Wir melden uns so schnell wie möglich
bei dir mit deinem persönlichen Angebot.
</p>
{% endif %}
<h2>Unterkunft</h2>
<table>
<tr>
<td><strong>Name</strong></td>
<td>{{ booking.accommodation.name }}</td>
</tr>
<tr>
<td><strong>Anreise</strong></td>
<td>{{ booking.dateFrom|date('d.m.Y') }}</td>
</tr>
<tr>
<td><strong>Abreise</strong></td>
<td>{{ booking.dateTo|date('d.m.Y') }}</td>
</tr>
<tr>
<td><strong>Nächte</strong></td>
<td>{{ booking.nights }}</td>
</tr>
<tr>
<td><strong>Personen</strong></td>
<td>{{ booking.paxCount }}</td>
</tr>
</table>
{% if priceBreakdown is not empty %}
{% set currency = priceBreakdown.currency %}
<h2>Preise</h2>
<table>
<tr>
<td><strong>Gesamtpreis</strong> <em>(zzgl. ortsabhängiger Gebühren)</em></td>
<td><strong>{{ (priceBreakdown.total / 100)|format_currency(currency) }}</strong></td>
</tr>
</table>
{% endif %}
<p class="small">Bei Fragen antworte einfach auf diese E-Mail.</p>
{% endblock %}
@@ -0,0 +1,32 @@
{% set current_step = current_step ?? 1 %}
{% set step_routes = {
1: 'app_groups_booking_step_1',
2: 'app_groups_booking_step_2',
3: 'app_groups_booking_step_3',
} %}
<nav class="pagination">
{% for step in 1..3 %}
<div class="pagination-item">
{% if step < current_step %}
<a href="{{ path(step_routes[step]) }}"
class="pagination-item__border">
<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 and booking_theme() == 'base',
'bg-sbw-primary text-white': step == current_step and booking_theme() == 'sbw',
'bg-snz-primary-dark text-white': step == current_step and booking_theme() == 'snz',
'bg-ser-primary text-white': step == current_step and booking_theme() == 'ser',
'bg-primary-bg text-gray-800': step != current_step
}) }}">
{{ step }}
</span>
</span>
{% endif %}
</div>
{% endfor %}
</nav>
@@ -0,0 +1,80 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 pb-4">
{% for monthData in months %}
<div class="mb-10">
<h3 class="text-base font-semibold mb-3">{{ monthData.label }}</h3>
{# Weekday headers (Mon–Sun) #}
<div class="grid grid-cols-7 text-center text-xs font-medium text-gray-400 mb-1">
<div>Mo</div>
<div>Di</div>
<div>Mi</div>
<div>Do</div>
<div>Fr</div>
<div>Sa</div>
<div>So</div>
</div>
{# Day grid #}
<div class="grid grid-cols-7 gap-0.5">
{% for week in monthData.weeks %}
{% for dayData in week %}
{% if dayData.inMonth %}
{% set dateStr = dayData.date|date('Y-m-d') %}
{% set entry = enrichedByDate[dateStr] ?? null %}
{% set status = (dateStr < todayStr) ? 'past' : (entry ? entry.status : 'ok') %}
{% set minNights = entry ? entry.minNights : 0 %}
<button type="button"
class="pc-day"
data-price-calendar-target="day"
data-action="price-calendar#selectDate"
data-date="{{ dateStr }}"
data-status="{{ status }}"
data-min-nights="{{ minNights }}"
{% if status == 'blocked' %}title="Nicht verfügbar"{% elseif minNights > 1 %}title="Mindestaufenthalt {{ minNights }} Nächte"{% endif %}>
{{ dayData.date|date('j') }}
</button>
{% else %}
<div class="pc-day pc-day--out-of-month"></div>
{% endif %}
{% endfor %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="flex justify-between">
{# Status message (arrival hint, short-stay warning, etc.) #}
<div class="text-sm text-primary-medium mb-2 min-h-[1.25rem]"
data-price-calendar-target="message"></div>
{# Reset link — revealed once a start date is selected #}
<button type="button"
class="text-sm text-gray-400 underline mb-4"
data-price-calendar-target="resetButton"
data-action="price-calendar#reset">
Auswahl zurücksetzen
</button>
</div>
{# Navigation — prev/next trigger HTMX month swap; no Stimulus involvement #}
<div class="flex items-center justify-between">
<button type="button"
class="button button--secondary"
{% if offset == 0 %}disabled{% endif %}
hx-get="{{ path('app_groups_booking_calendar_grid', {'offset': offset - 1}) }}"
hx-target="#calendar-mount"
hx-swap="innerHTML">
&larr; früher
</button>
<button type="button"
class="button button--secondary"
{% if offset >= totalMonths - 2 %}disabled{% endif %}
hx-get="{{ path('app_groups_booking_calendar_grid', {'offset': offset + 1}) }}"
hx-target="#calendar-mount"
hx-swap="innerHTML">
später &rarr;
</button>
</div>
@@ -0,0 +1,24 @@
{% extends 'htmx_modal.html.twig' %}
{% block title %}Buchung bestätigen{% endblock %}
{% block content %}
{% from '_partials/_validation_errors.html.twig' import validation_alert %}
{{ validation_alert(confirmationForm, 'Bitte akzeptiere die AGB, um die Buchung abzuschließen.', false) }}
{{ form_start(confirmationForm, {
'action': path('app_groups_booking_step_3_confirm'),
'attr': {
'hx-post': path('app_groups_booking_step_3_confirm'),
'hx-target': '#htmx-modal',
'hx-swap': 'outerHTML',
}
}) }}
{{ form_row(confirmationForm.termsAccepted) }}
<div class="mt-6 flex flex-col space-y-4 md:flex-row md:space-y-0 justify-between">
<button type="submit" class="button button--primary">Buchung verbindlich absenden</button>
<button type="button" class="button button--secondary" {{ stimulus_action('modal', 'close') }}>Abbrechen</button>
</div>
{{ form_end(confirmationForm) }}
{% endblock %}
+163
View File
@@ -0,0 +1,163 @@
{% block booking_summary %}
<div class="bg-white h-full flex flex-col lg:border-l lg:border-primary-bg"
id="booking-summary"
{% if htmx_oob_swap ?? false %}hx-swap-oob="outerHTML"{% endif %}>
{# Header #}
<div class="shrink-0 px-4 lg:px-8 py-4 lg:flex lg:flex-col lg:justify-center shadow-md relative z-10">
{% if ctx.priceBreakdown is not null %}
<div class="flex items-center justify-between pb-2">
<span class="block uppercase font-semibold">Gesamtpreis</span>
<span class="font-semibold">{{ (ctx.priceBreakdown.total / 100)|format_currency(ctx.accommodation.currency) }}</span>
</div>
{% endif %}
<div class="flex items-center justify-between">
<span class="block uppercase font-semibold">Ü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>
{# Content #}
<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">
{# Accommodation info #}
{% set hasImage = ctx.hotelCmsData is not null
and ctx.hotelCmsData.images is defined
and ctx.hotelCmsData.images.resized is defined
and ctx.hotelCmsData.images.resized.l is defined
and ctx.hotelCmsData.images.resized.l[0] is defined %}
<div class="border border-primary-bg mb-4">
<div class="p-2 bg-primary-bg uppercase font-semibold">Unterkunft</div>
<div class="grid grid-cols-3 gap-x-4 p-2">
{% if hasImage %}
<img src="{{ ctx.hotelCmsData.images.resized.l[0].url }}"
alt="{{ ctx.hotelCmsData.images.resized.l[0].alt ?? ctx.accommodation.name }}"
class="block w-full h-auto">
{% endif %}
<div class="{{ hasImage ? 'col-span-2' : 'col-span-3' }}">
<span class="block font-semibold">{{ ctx.accommodation.name }}</span>
{% if ctx.hotelCmsData is not null and ctx.hotelCmsData.address is defined %}
<div class="text-sm text-gray-500 mt-1">{{ ctx.hotelCmsData.address|nl2br }}</div>
{% endif %}
</div>
</div>
</div>
{# Price breakdown #}
{% if ctx.priceBreakdown is not null %}
<div class="border border-primary-bg mt-4">
<div class="p-2 bg-primary-bg uppercase font-semibold">Preise</div>
<table class="w-full table-fixed text-sm">
<tr>
<td class="p-2">
Zeitraum
</td>
<td class="p-2 text-right">
{% if dto.dateFrom %}
{{ dto.dateFrom|date('d.m.Y') }} – {{ dto.dateTo|date('d.m.Y') }},
<br>
{{ dto.nights }} {{ dto.nights != 1 ? 'Nächte' : 'Nacht' }}
{% else %}
<span class="italic">noch nicht gewählt</span>
{% endif %}
</td>
</tr>
<tr>
<td class="p-2">
Anzahl Personen
</td>
<td class="p-2 text-right">
{{ dto.paxCount }}
</td>
</tr>
{% if dto.minorsCount > 0 %}
<tr>
<td class="p-2">davon Kinder (0–3 J.)</td>
<td class="p-2 text-right">{{ dto.minorsCount }}</td>
</tr>
{% endif %}
{% if dto.childrenCount > 0 %}
<tr>
<td class="p-2">davon Kinder (4–{{ ctx.accommodation.maxAdolescentAge }} J.)</td>
<td class="p-2 text-right">{{ dto.childrenCount }}</td>
</tr>
{% endif %}
{% if ctx.priceBreakdown.basePrice > 0 %}
<tr>
<td class="p-2">
Basispreis
<p class="text-xs text-gray-500">für {{ ctx.priceBreakdown.includedPax }} Pers.</p>
</td>
<td class="p-2 text-right whitespace-nowrap">{{ (ctx.priceBreakdown.basePrice / 100)|format_currency(ctx.accommodation.currency) }}</td>
</tr>
{% endif %}
{% if ctx.priceBreakdown.additionalPersonsPrice > 0 %}
<tr>
<td class="p-2">
Aufpreis Personenzahl
<p class="text-xs text-gray-500">für {{ ctx.priceBreakdown.effectivePax - ctx.priceBreakdown.includedPax }} Pers.</p>
</td>
<td class="p-2 text-right whitespace-nowrap">{{ (ctx.priceBreakdown.additionalPersonsPrice / 100)|format_currency(ctx.accommodation.currency) }}</td>
</tr>
{% endif %}
{% if ctx.priceBreakdown.shortTermSurcharge > 0 %}
<tr>
<td class="p-2">Aufpreis Kurzzeit</td>
<td class="p-2 text-right whitespace-nowrap">{{ (ctx.priceBreakdown.shortTermSurcharge / 100)|format_currency(ctx.accommodation.currency) }}</td>
</tr>
{% endif %}
{% if ctx.priceBreakdown.boardPrice > 0 %}
<tr>
<td class="p-2">Verpflegung</td>
<td class="p-2 text-right whitespace-nowrap">{{ (ctx.priceBreakdown.boardPrice / 100)|format_currency(ctx.accommodation.currency) }}</td>
</tr>
{% endif %}
{% if ctx.priceBreakdown.undersubscriptionSurcharge > 0 %}
<tr>
<td class="p-2">
Verpflegungs-Aufschlag
<p class="text-xs text-gray-500">für Gruppen unter {{ ctx.priceBreakdown.undersubscriptionThreshold }} Pers.</p>
</td>
<td class="p-2 text-right whitespace-nowrap">{{ (ctx.priceBreakdown.undersubscriptionSurcharge / 100)|format_currency(ctx.accommodation.currency) }}</td>
</tr>
{% endif %}
{% for service in ctx.priceBreakdown.serviceDetails %}
{% if service.price > 0 %}
<tr>
<td class="p-2">{{ service.label }}</td>
<td class="p-2 text-right whitespace-nowrap">{{ (service.price / 100)|format_currency(ctx.accommodation.currency) }}</td>
</tr>
{% endif %}
{% endfor %}
{% if ctx.priceBreakdown.runningCosts > 0 %}
<tr>
<td class="p-2">Strom- und Abfallgebühren</td>
<td class="p-2 text-right whitespace-nowrap">{{ (ctx.priceBreakdown.runningCosts / 100)|format_currency(ctx.accommodation.currency) }}</td>
</tr>
{% endif %}
<tr class="border-t border-primary-bg font-semibold text-base">
<td class="p-2">
Gesamtpreis
<p class="text-xs font-bold">zzgl. ortsabhängiger Gebühren</p>
</td>
<td class="p-2 text-right whitespace-nowrap">
{{ (ctx.priceBreakdown.total / 100)|format_currency(ctx.accommodation.currency) }}
</td>
</tr>
</table>
</div>
{% endif %}
</div>
</div>
{% endblock %}
+18
View File
@@ -0,0 +1,18 @@
{% extends 'layout.html.twig' %}
{% block title %}Fehler{% endblock %}
{% block background %}bg-outer bg-outer--summer{% endblock %}
{% block content %}
<div class="flex-1 flex items-center justify-center px-4">
<div class="max-w-lg w-full">
{% set errorMessages = app.flashes('error')|default(['Es ist ein Fehler aufgetreten. Bitte überprüfe den Link oder versuche es erneut.']) %}
{% include '_partials/_alert.html.twig' with {
level: 'error',
title: 'Das hat nicht geklappt',
messages: errorMessages
} %}
</div>
</div>
{% endblock %}
+82
View File
@@ -0,0 +1,82 @@
{% extends 'layout.html.twig' %}
{% block title %}{{ booking.inquiry ? 'Dein Angebot' : 'Deine Buchung' }}{% endblock %}
{% block background %}bg-outer bg-outer--summer{% endblock %}
{% block content %}
<div class="max-w-2xl w-full mx-auto p-8">
<h1 class="text-2xl font-bold mb-2">
{{ booking.inquiry ? 'Dein Angebot' : 'Deine Buchung' }}
</h1>
<p class="text-gray-500 mb-6">{{ booking.accommodation.name }}</p>
<div class="grid grid-cols-2 gap-y-2 text-sm mb-8">
<dt class="font-medium">Anreise</dt>
<dd>{{ booking.dateFrom | date('d.m.Y') }}</dd>
<dt class="font-medium">Abreise</dt>
<dd>{{ booking.dateTo | date('d.m.Y') }}</dd>
<dt class="font-medium">Nächte</dt>
<dd>{{ booking.nights }}</dd>
<dt class="font-medium">Personen</dt>
<dd>{{ booking.paxCount }}</dd>
{% if booking.boardServiceLabel %}
<dt class="font-medium">Verpflegung</dt>
<dd>{{ booking.boardServiceLabel }}</dd>
{% endif %}
</div>
{% if booking.additionalServices | length > 0 %}
<div class="mb-8">
<h2 class="text-lg font-bold mb-2">Zusatzleistungen</h2>
<ul class="text-sm list-disc list-inside">
{% for service in booking.additionalServices %}
<li>{{ service.label }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if priceBreakdown is not null %}
{% set currency = booking.accommodation.currency %}
<div class="mb-8">
<h2 class="text-lg font-bold mb-2">Preise</h2>
<table class="w-full text-sm">
<tr class="border-t border-gray-200 font-semibold">
<td class="pt-2">
Gesamtpreis
<span class="block text-xs font-normal text-gray-500">zzgl. ortsabhängiger Gebühren</span>
</td>
<td class="pt-2 text-right whitespace-nowrap">{{ (priceBreakdown.total / 100) | format_currency(currency) }}</td>
</tr>
{% if booking.discount is not null %}
{% set discountAmount = (priceBreakdown.total * booking.discount / 100) | round | number_format(0, '.', '') %}
{% set discountedTotal = priceBreakdown.total - discountAmount %}
<tr class="text-green-700">
<td class="pt-1">Rabatt {{ booking.discount }}&nbsp;%</td>
<td class="pt-1 text-right whitespace-nowrap">– {{ (discountAmount / 100) | format_currency(currency) }}</td>
</tr>
<tr class="border-t border-gray-200 font-bold text-green-700">
<td class="pt-2">Gesamtpreis nach Rabatt</td>
<td class="pt-2 text-right whitespace-nowrap">{{ (discountedTotal / 100) | format_currency(currency) }}</td>
</tr>
{% endif %}
</table>
</div>
{% endif %}
{% if booking.inquiry %}
<form method="post" action="{{ app.request.requestUri }}">
<button type="submit" class="button button--primary">
Angebot verbindlich buchen
</button>
</form>
{% else %}
{% include '_partials/_alert.html.twig' with {
level: 'success',
title: 'Bestätigt',
messages: ['Deine Buchung ist verbindlich bestätigt.'],
} %}
{% endif %}
</div>
{% endblock %}
@@ -0,0 +1,13 @@
{% extends 'layout.html.twig' %}
{% block title %}Link nicht verfügbar{% endblock %}
{% block content %}
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 max-w-lg w-full">
{% include '_partials/_alert.html.twig' with {
level: 'error',
title: 'Dieser Link ist nicht (mehr) gültig',
messages: ['Bitte wende dich an uns, damit wir dir einen neuen Link zusenden können.'],
} %}
</div>
{% endblock %}
+89
View File
@@ -0,0 +1,89 @@
{% extends 'layout_booking.html.twig' %}
{% block title %}Schritt 1: Reisezeitraum wählen{% endblock %}
{% block background %}bg-outer bg-outer--summer{% endblock %}
{% block content %}
<div class="flex-1 flex flex-col min-h-0"
data-controller="price-calendar"
data-price-calendar-hotel-code-value="{{ hotelCode }}"
data-price-calendar-calendar-refresh-url-value="{{ path('app_groups_booking_calendar_refresh') }}"
{% if dto.dateFrom %}
data-price-calendar-selected-from-value="{{ dto.dateFrom|date('Y-m-d') }}"
data-price-calendar-selected-to-value="{{ dto.dateTo|date('Y-m-d') }}"
{% endif %}>
{# Pagination - mobile only (above summary) #}
<div class="lg:hidden">
{% include 'groups/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">
{# Sidebar summary #}
<div 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' }) }}>
{% include 'groups/booking/_summary.html.twig' with { 'dto': dto, 'ctx': ctx } %}
</div>
{# Calendar #}
<div class="flex-1 order-2 lg:order-1 lg:col-span-3 bg-white flex flex-col min-h-0">
{% include '_partials/_flashes.html.twig' %}
{# Pagination - desktop only (fixed above scrollable content) #}
<div class="hidden lg:block shrink-0">
{% include 'groups/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-2">
Gewünschter Zeitraum
</h2>
{# Calendar mount — HTMX loads the grid; Stimulus wires afterSwap → initAfterLoad() #}
<div id="calendar-mount"
data-price-calendar-target="mount"
hx-get="{{ path('app_groups_booking_calendar_grid') }}"
hx-trigger="load"
hx-swap="innerHTML">
<div class="text-sm text-gray-400 py-8 text-center">Kalender wird geladen…</div>
</div>
{# Error state shown by Stimulus on htmx:responseError #}
<div data-price-calendar-target="error"
class="hidden rounded-md p-3 bg-red-100 text-red-700 text-sm mt-4">
Kalender konnte nicht geladen werden. Bitte versuche es später erneut.
</div>
</div>
</div>
</div>
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
<form method="post">
<input type="hidden"
name="date_from"
data-price-calendar-target="dateFrom"
value="{{ dto.dateFrom ? dto.dateFrom|date('Y-m-d') : '' }}">
<input type="hidden"
name="date_to"
data-price-calendar-target="dateTo"
value="{{ dto.dateTo ? dto.dateTo|date('Y-m-d') : '' }}">
<div class="flex justify-end">
<button type="submit"
data-price-calendar-target="submit"
class="button button--primary{% if not dto.dateFrom %} opacity-50 cursor-not-allowed pointer-events-none{% endif %}"
{% if not dto.dateFrom %}disabled{% endif %}>
Weiter zu Schritt 2
</button>
</div>
</form>
</div>
</div>
{% endblock %}
+235
View File
@@ -0,0 +1,235 @@
{% extends 'layout_booking.html.twig' %}
{% block title %}Schritt 2: Personen & Leistungen{% endblock %}
{% block background %}bg-outer bg-outer--summer{% endblock %}
{% block content %}
{% form_theme form 'booking/_form_theme.html.twig' %}
{{ form_start(form, {
'attr': {
'class': 'flex-1 flex flex-col min-h-0',
}
}) }}
{# Pagination - mobile only (above summary) #}
<div class="lg:hidden">
{% include 'groups/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">
{# Sidebar summary #}
{% block accommodation_summary %}
<div id="accommodation-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 'groups/booking/_summary.html.twig' with { 'dto': dto, 'ctx': ctx } %}
</div>
{% endblock %}
{# Main form #}
{% block accommodation_form %}
<div id="accommodation-form"
class="flex-1 order-2 lg:order-1 lg:col-span-3 bg-white flex flex-col min-h-0"
{% if htmx_oob_swap is defined and htmx_oob_swap %}hx-swap-oob="true"{% endif %}>
{% include '_partials/_flashes.html.twig' %}
{# Pagination - desktop only (fixed above scrollable content) #}
<div class="hidden lg:block shrink-0">
{% include 'groups/booking/_pagination.html.twig' with { 'current_step': 2 } %}
</div>
<div id="participant-form"
class="flex-1 overflow-y-auto px-4 lg:px-8 py-8"
hx-post="{{ path('app_groups_booking_step_2_refresh') }}"
hx-trigger="change delay:300ms"
hx-swap="none">
{# Validation errors #}
{% from '_partials/_validation_errors.html.twig' import validation_alert %}
{{ validation_alert(form) }}
{% if dto.isInquiry %}
{% include '_partials/_alert.html.twig' with {
level: 'warning',
title: 'Diese Buchung wird als unverbindliche Anfrage behandelt',
messages: dto.inquiryReasons
} %}
{% endif %}
{# Pax counts #}
<h2 class="pb-4">
Personenanzahl und gewünschte Leistungen
</h2>
<div class="grid lg:grid-cols-3 lg:gap-x-8">
{{ form_row(form.paxCount) }}
{{ form_row(form.minorsCount) }}
{{ form_row(form.childrenCount) }}
</div>
{# Board services — rendered manually so price is formatted in Twig #}
{% do form.selectedBoardServiceId.setRendered %}
{% if ctx.boardServices is not empty %}
<div class="pb-4">
<fieldset class="border border-primary-bg">
<legend class="w-full bg-primary-bg p-2 font-semibold uppercase">
Verpflegungsleistungen
</legend>
<table class="w-full table-fixed border-collapse">
<tr>
<td class="border border-primary-bg p-2 align-top">
<label for="board_service_none" class="cursor-pointer">Selbstversorgung</label>
</td>
<td class="border border-primary-bg p-2 align-top w-32 text-right"></td>
<td class="border border-primary-bg p-2 align-top w-12 text-center">
<input type="radio"
id="board_service_none"
name="{{ form.selectedBoardServiceId.vars.full_name }}"
value=""
{% if dto.selectedBoardServiceId is null %}checked{% endif %}
class="form-radio">
</td>
</tr>
{% for service in ctx.boardServices %}
<tr>
<td class="border border-primary-bg p-2 align-top">
<label for="board_service_{{ service.id }}" class="cursor-pointer">{{ service.label }}</label>
{% if service.description %}
<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">{{ service.description|nl2br }}</div>
</template>
</div>
{% endif %}
</td>
<td class="border border-primary-bg p-2 align-top w-32 text-right">
<div>{{ (service.price / 100)|format_currency(ctx.accommodation.currency) }}</div>
<div class="text-xs text-gray-500">pro Person und Nacht</div>
</td>
<td class="border border-primary-bg p-2 align-top w-12 text-center">
<input type="radio"
id="board_service_{{ service.id }}"
name="{{ form.selectedBoardServiceId.vars.full_name }}"
value="{{ service.id }}"
{% if dto.selectedBoardServiceId == service.id %}checked{% endif %}
class="form-radio">
</td>
</tr>
{% endfor %}
</table>
</fieldset>
</div>
{% endif %}
{# Additional services — rendered manually to support checkbox/radio mix #}
{% do form.selectedAdditionalServiceIds.setRendered %}
{% set selectedAdditionalServiceBaseName = form.selectedAdditionalServiceIds.vars.full_name|replace({'[]': ''}) %}
{% if ctx.ungroupedAdditionalServices is not empty or ctx.groupedAdditionalServices is not empty %}
{# Ungrouped → checkboxes #}
{% if ctx.ungroupedAdditionalServices is not empty %}
<div class="pb-4">
<fieldset class="border border-primary-bg">
<legend class="w-full bg-primary-bg p-2 font-semibold uppercase">
Optionale Zusatzleistungen
</legend>
<table class="w-full table-fixed border-collapse">
{% for service in ctx.ungroupedAdditionalServices %}
<tr>
<td class="border border-primary-bg p-2 align-top">
<label for="add_service_{{ service.id }}" class="cursor-pointer">{{ service.label }}</label>
{% if service.description %}
<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">{{ service.description|nl2br }}</div>
</template>
</div>
{% endif %}
</td>
<td class="border border-primary-bg p-2 align-top w-32 text-right">
<div>{{ (service.price / 100)|format_currency(ctx.accommodation.currency) }}</div>
<div class="text-xs text-gray-500">{{ service.type.label | trans }}</div>
</td>
<td class="border border-primary-bg p-2 align-top w-12 text-center">
<input type="checkbox"
id="add_service_{{ service.id }}"
name="{{ form.selectedAdditionalServiceIds.vars.full_name }}"
value="{{ service.id }}"
{% if service.id in dto.selectedAdditionalServiceIds %}checked{% endif %}
class="form-checkbox">
</td>
</tr>
{% endfor %}
</table>
</fieldset>
</div>
{% endif %}
{# Grouped → radio buttons per group, one fieldset per group #}
{% for groupName, services in ctx.groupedAdditionalServices %}
<div class="pb-4">
<fieldset class="border border-primary-bg">
<legend class="w-full bg-primary-bg p-2 font-semibold uppercase">{{ groupName }}</legend>
<table class="w-full table-fixed border-collapse">
{% for service in services %}
<tr>
<td class="border border-primary-bg p-2 align-top">
<label for="add_service_{{ service.id }}" class="cursor-pointer">{{ service.label }}</label>
{% if service.description %}
<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">{{ service.description|nl2br }}</div>
</template>
</div>
{% endif %}
</td>
<td class="border border-primary-bg p-2 align-top w-32 text-right">
<div>{{ (service.price / 100)|format_currency(ctx.accommodation.currency) }}</div>
<div class="text-xs text-gray-500">{{ service.type.label | trans }}</div>
</td>
<td class="border border-primary-bg p-2 align-top w-12 text-center">
<input type="radio"
id="add_service_{{ service.id }}"
name="{{ selectedAdditionalServiceBaseName }}[{{ groupName|e('html_attr') }}]"
value="{{ service.id }}"
{% if service.id in dto.selectedAdditionalServiceIds %}checked{% endif %}
class="form-radio">
</td>
</tr>
{% endfor %}
</table>
</fieldset>
</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endblock %}
</div>
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
<div class="flex items-center justify-between">
<a href="{{ path('app_groups_booking_step_1') }}" class="button button--secondary">
Zurück
</a>
<button type="submit" class="button button--primary">
Weiter zu Schritt 3
</button>
</div>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}
+111
View File
@@ -0,0 +1,111 @@
{% extends 'layout_booking.html.twig' %}
{% block title %}Schritt 3: Persönliche Daten{% endblock %}
{% block background %}bg-outer bg-outer--summer{% endblock %}
{% block content %}
{% form_theme form 'booking/_form_theme.html.twig' %}
{{ form_start(form, {
'attr': {
'class': 'flex-1 flex flex-col min-h-0',
'novalidate': 'novalidate',
}
}) }}
{# Pagination - mobile only (above summary) #}
<div class="lg:hidden">
{% include 'groups/booking/_pagination.html.twig' with { 'current_step': 3 } %}
</div>
<div class="flex-1 flex flex-col lg:grid lg:grid-cols-5 min-h-0 relative">
{# Static sidebar recap #}
<div 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' }) }}>
{% include 'groups/booking/_summary.html.twig' with { 'dto': dto, 'ctx': ctx } %}
</div>
{# Personal data form #}
<div class="flex-1 order-2 lg:order-1 lg:col-span-3 bg-white flex flex-col min-h-0">
{% include '_partials/_flashes.html.twig' %}
{# Pagination - desktop only (fixed above scrollable content) #}
<div class="hidden lg:block shrink-0">
{% include 'groups/booking/_pagination.html.twig' with { 'current_step': 3 } %}
</div>
<div id="participant-form" class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
<h2 class="pb-4">
Persönliche Daten
</h2>
{# Validation errors #}
{% from '_partials/_validation_errors.html.twig' import validation_alert %}
{{ validation_alert(form, 'Bitte fülle alle Pflichtfelder aus.') }}
{% if dto.isInquiry %}
{% include '_partials/_alert.html.twig' with {
level: 'warning',
title: 'Diese Buchung wird als unverbindliche Anfrage behandelt',
messages: dto.inquiryReasons
} %}
{% endif %}
<div class="grid lg:grid-cols-2 lg:gap-x-8">
{{ form_row(form.groupName) }}
{{ form_row(form.salutation) }}
</div>
<div class="grid lg:grid-cols-2 lg:gap-x-8">
{{ form_row(form.firstName) }}
{{ form_row(form.lastName) }}
</div>
<div class="grid lg:grid-cols-2 lg:gap-x-8">
{{ form_row(form.email) }}
{{ form_row(form.phone) }}
</div>
<h3 class="pt-4 pb-2">Adresse</h3>
{{ form_row(form.street) }}
<div class="grid lg:grid-cols-2 lg:gap-x-8">
{{ form_row(form.zip) }}
{{ form_row(form.city) }}
</div>
<h3 class="pt-4 pb-2">Anmerkungen</h3>
{{ form_row(form.remarks) }}
<div class="pt-6">
{% if form.forceInquiry is defined %}
{{ form_row(form.forceInquiry) }}
{% endif %}
</div>
</div>
</div>
</div>
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
<div class="flex items-center justify-between">
<a href="{{ path('app_groups_booking_step_2') }}" class="button button--secondary">
Zurück
</a>
<div class="space-x-2">
<button type="submit" name="submitInquiry" class="button button--secondary">anfragen</button>
<button type="submit" name="submitBooking" class="{{ html_classes('button button--primary', { 'opacity-50': dto.isInquiry }) }}"{% if dto.isInquiry %} disabled{% endif %}>buchen</button>
</div>
</div>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
{% if confirmationForm is defined %}
{% include 'groups/booking/_step3_confirmation_modal.html.twig' with { confirmationForm: confirmationForm } %}
{% endif %}
{% endblock %}
@@ -0,0 +1,19 @@
{% extends 'layout.html.twig' %}
{% block title %}{{ resultType == 'booking' ? 'Buchung erfolgreich' : 'Anfrage erfolgreich' }}{% endblock %}
{% block background %}bg-outer bg-outer--summer{% endblock %}
{% block content %}
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 max-w-lg w-full">
{% include '_partials/_alert.html.twig' with {
level: 'success',
title: 'Vielen Dank!',
messages: [
resultType == 'booking'
? 'Deine Buchung ist bei uns eingegangen. Du erhältst in Kürze eine Bestätigung.'
: 'Deine Anfrage ist bei uns eingegangen. Wir melden uns so schnell wie möglich bei dir.'
]
} %}
</div>
{% endblock %}