feat: groups price calculator admin crud, booking/offer flow and api
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
{% if app.session.flashBag.peek('error')|length > 0 %}
|
||||
{% include '_partials/_toast.html.twig' with { level: 'warning', messages: app.flashes('error') } %}
|
||||
{% endif %}
|
||||
{% if app.session.flashBag.peek('success')|length > 0 %}
|
||||
{% include '_partials/_toast.html.twig' with { level: 'success', messages: app.flashes('success') } %}
|
||||
{% endif %}
|
||||
{% if app.session.flashBag.peek('info')|length > 0 %}
|
||||
{% include '_partials/_toast.html.twig' with { level: 'info', messages: app.flashes('info') } %}
|
||||
{% endif %}
|
||||
@@ -41,9 +41,10 @@
|
||||
{% import _self as knp_menu %}
|
||||
<a href="{{ item.uri }}"
|
||||
class="{{ html_classes('', {
|
||||
'group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold text-gray-700 hover:bg-gray-50': item.level == 1,
|
||||
'block rounded-md py-2 pr-2 pl-9 text-sm leading-6 text-gray-700 hover:bg-gray-50': item.level > 1,
|
||||
'bg-gray-50': matcher.isCurrent(item) or matcher.isAncestor(item)
|
||||
'group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold': item.level == 1,
|
||||
'block rounded-md py-2 pr-2 pl-9 text-sm leading-6': item.level > 1,
|
||||
'text-gray-700 hover:bg-gray-50': not (matcher.isCurrent(item) or matcher.isAncestor(item)),
|
||||
'bg-gray-100 text-gray-900': matcher.isCurrent(item) or matcher.isAncestor(item),
|
||||
}) }}"
|
||||
{{ knp_menu.attributes(item.linkAttributes) }}>
|
||||
{{ block('label') }}
|
||||
@@ -73,7 +74,7 @@
|
||||
|
||||
{% block label %}
|
||||
{% if item.extras.icon is defined %}
|
||||
{{ icon(item.extras.icon, 'h-5 w-5 shrink-0 text-gray-400') }}
|
||||
{{ icon(item.extras.icon, 'h-5 w-5 shrink-0 ' ~ ((matcher.isCurrent(item) or matcher.isAncestor(item)) ? 'text-gray-600' : 'text-gray-400')) }}
|
||||
<span class="flex-1">{{ item.label|raw }}</span>
|
||||
{% else %}
|
||||
{{ item.label|raw }}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{% apply spaceless %}
|
||||
{% for message in messages %}
|
||||
{% if message is iterable %}
|
||||
{% set message = message.id | trans(message.parameters | default({}), message.domain | default(null), message.locale | default(null)) | raw %}
|
||||
{% else %}
|
||||
{% set message = message | trans | raw %}
|
||||
{% endif %}
|
||||
<div {{ stimulus_controller('toast', { 'text': message, 'class': 'toastify--' ~ level}) }}{{ qa_attribute('toast', level)}}></div>
|
||||
{% endfor %}
|
||||
{% endapply %}
|
||||
@@ -0,0 +1 @@
|
||||
<twig:calendar :accommodation="accommodation" :startMonth="startMonth ?? null"/>
|
||||
@@ -0,0 +1,305 @@
|
||||
{% form_theme form 'forms_admin.html.twig' %}
|
||||
|
||||
{{ form_start(form) }}
|
||||
<div class="grid lg:grid-cols-2 lg:gap-x-8 items-start">
|
||||
<div class="grid lg:grid-cols-2 gap-y-4 lg:gap-x-4">
|
||||
<div class="lg:col-span-2">
|
||||
{{ form_row(form.name) }}
|
||||
</div>
|
||||
{{ form_row(form.calendarCode) }}
|
||||
{{ form_row(form.cmsCode) }}
|
||||
{{ form_row(form.maxAdolescentAge) }}
|
||||
{{ form_row(form.currency) }}
|
||||
</div>
|
||||
{% if cmsData is not null and cmsData.name is not null %}
|
||||
<div class="space-y-4">
|
||||
{% if cmsData.images.resized.m | length > 0 %}
|
||||
{% set image = cmsData.images.resized.m[0] %}
|
||||
<div class="max-w-64">
|
||||
<img src="{{ image.url }}" alt="{{ image.alt }}" class="block w-full h-auto">
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<strong>{{ cmsData.name }}</strong>
|
||||
<br>
|
||||
{{ cmsData.address | default('-') | nl2br }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="space-y-4">
|
||||
<div class="max-w-64">
|
||||
<svg class="w-full h-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4 5V19H20V7H11.5858L9.58579 5H4ZM12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5ZM10 10.5C10 11.3284 9.32843 12 8.5 12C7.67157 12 7 11.3284 7 10.5C7 9.67157 7.67157 9 8.5 9C9.32843 9 10 9.67157 10 10.5ZM18 17L14 11L7 17H18Z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<strong>CMS-Code falsch oder Haus in TYPO3 nicht angelegt</strong>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex justify-between pt-8">
|
||||
<a href="{{ path('app_admin_accommodation') }}" 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) }}
|
||||
|
||||
<hr class="my-8">
|
||||
|
||||
<twig:calendar :accommodation="accommodation"/>
|
||||
|
||||
<hr class="my-8">
|
||||
|
||||
<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>
|
||||
<th>
|
||||
Bezeichnung
|
||||
</th>
|
||||
<th>
|
||||
Preis pro Person/Nacht
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for service in accommodation.boardServices %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ service.dateFrom | date('d.m.Y') }} - {{ service.dateTo | date('d.m.Y') }}
|
||||
</td>
|
||||
<td>
|
||||
{{ service.label }}
|
||||
</td>
|
||||
<td>
|
||||
{{ (service.price / 100) | format_currency(accommodation.currency) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<twig:dropdown>
|
||||
<twig:dropdown:link url="{{ path('app_admin_boardservice_edit', { 'id': service.id }) }}">
|
||||
bearbeiten
|
||||
</twig:dropdown:link>
|
||||
<twig:dropdown:link url="{{ path('app_admin_boardservice_duplicate', { 'id': service.id }) }}">
|
||||
duplizieren
|
||||
</twig:dropdown:link>
|
||||
<twig:dropdown:hxbutton url="{{ path('app_admin_boardservice_delete', { 'id': service.id }) }}" warning>
|
||||
löschen
|
||||
</twig:dropdown:hxbutton>
|
||||
</twig:dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<a href="{{ path('app_admin_boardservice_create', { 'id': accommodation.id }) }}" class="button button--primary button--small">
|
||||
neu
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="my-8">
|
||||
|
||||
<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>
|
||||
<th>
|
||||
Datums-Bereich
|
||||
</th>
|
||||
<th>
|
||||
Bezeichnung
|
||||
</th>
|
||||
<th>
|
||||
Preistyp
|
||||
</th>
|
||||
<th>
|
||||
Preis
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</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 class="flex justify-end">
|
||||
<a href="{{ path('app_admin_additionalservice_create', { 'id': accommodation.id }) }}" class="button button--primary button--small">
|
||||
neu
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Gruppenhaus neu</twig:page:heading>
|
||||
{% include 'admin/accommodation/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Gruppenhaus {{ accommodation.name }}</twig:page:heading>
|
||||
{% include 'admin/accommodation/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,77 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Gruppenhäuser</twig:page:heading>
|
||||
<div class="data-table-wrapper">
|
||||
<div class="data-table-wrapper__inner text-sm">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Code
|
||||
</th>
|
||||
<th>
|
||||
erstellt
|
||||
</th>
|
||||
<th>
|
||||
aktualisiert
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for accommodation in accommodations %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ path('app_admin_accommodation_edit', { 'id': accommodation.id, 'r': return_url() }) }}">
|
||||
{{ accommodation.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ accommodation.calendarCode }}
|
||||
</td>
|
||||
<td>
|
||||
{{ accommodation.createdAt | date('d.m.Y, H:i') }}
|
||||
</td>
|
||||
<td>
|
||||
{% if accommodation.updatedAt is not null %}
|
||||
{{ accommodation.updatedAt | date('d.m.Y, H:i') }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<twig:dropdown>
|
||||
<twig:dropdown:link url="{{ path('app_admin_accommodation_edit', { 'id': accommodation.id, 'r': return_url() }) }}">
|
||||
bearbeiten
|
||||
</twig:dropdown:link>
|
||||
<twig:dropdown:hxbutton url="{{ path('app_admin_accommodation_delete', { 'id': accommodation.id, 'r': return_url() }) }}" 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 class="flex justify-end">
|
||||
<button type="button"
|
||||
hx-get="{{ path('app_admin_accommodation_create') }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
class="button button--primary button--small">
|
||||
neu
|
||||
</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,22 @@
|
||||
{% extends 'htmx_modal_admin.html.twig' %}
|
||||
{% form_theme form 'forms_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="text-xl font-bold pb-4">
|
||||
Neues Gruppenhaus
|
||||
</div>
|
||||
{{ form_start(form) }}
|
||||
<div class="grid lg:grid-cols-2 gap-y-4 lg:gap-x-8">
|
||||
{{ form_row(form.name) }}
|
||||
{{ form_row(form.calendarCode) }}
|
||||
{{ form_row(form.maxAdolescentAge) }}
|
||||
{{ form_row(form.currency) }}
|
||||
</div>
|
||||
<div class="flex justify-end pt-8">
|
||||
<button type="submit" class="button button--primary button--small">
|
||||
speichern
|
||||
</button>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends 'htmx_confirmation_modal.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
Möchtest du das Gruppenhaus <em>{{ accommodation.name }}</em> wirklich löschen?
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,53 @@
|
||||
{% 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.accommodationDiscount) }}
|
||||
{{ form_row(form.boardServiceDiscount) }}
|
||||
{{ form_row(form.additionalServicesDiscount) }}
|
||||
<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,67 @@
|
||||
{% 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>
|
||||
{% if booking.acceptedAt is not null %}
|
||||
<p class="lg:col-span-2 text-xs text-gray-500">
|
||||
Angenommen am {{ booking.acceptedAt | date('d.m.Y, H:i') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{{ form_row(form.accommodationDiscount) }}
|
||||
{{ form_row(form.boardServiceDiscount) }}
|
||||
{{ form_row(form.additionalServicesDiscount) }}
|
||||
<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,100 @@
|
||||
{% 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>
|
||||
{% set discounts = [] %}
|
||||
{% if booking.accommodationDiscount is not null %}
|
||||
{% set discounts = discounts | merge(['Unterkunft ' ~ booking.accommodationDiscount ~ '%']) %}
|
||||
{% endif %}
|
||||
{% if booking.boardServiceDiscount is not null %}
|
||||
{% set discounts = discounts | merge(['Verpflegung ' ~ booking.boardServiceDiscount ~ '%']) %}
|
||||
{% endif %}
|
||||
{% if booking.additionalServicesDiscount is not null %}
|
||||
{% set discounts = discounts | merge(['Zusatzleistungen ' ~ booking.additionalServicesDiscount ~ '%']) %}
|
||||
{% endif %}
|
||||
{{ discounts | length > 0 ? (discounts | join(', ')) : '–' }}
|
||||
</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,8 @@
|
||||
{% extends 'htmx_confirmation_modal.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
Möchtest du für <em>{{ booking.groupName }}</em> einen neuen Zugangslink generieren?
|
||||
Ein zuvor generierter Link wird dadurch ungültig.
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends 'htmx_confirmation_modal.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
Möchtest du den aktuellen Zugangslink für <em>{{ booking.groupName }}</em> an
|
||||
<em>{{ booking.email }}</em> senden?
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,237 @@
|
||||
{% 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>
|
||||
{% if booking.acceptedAt is not null %}
|
||||
<dt class="font-medium">Angenommen am</dt>
|
||||
<dd>{{ booking.acceptedAt | date('d.m.Y, H:i') }}</dd>
|
||||
{% endif %}
|
||||
</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.pricingCurrency ?? priceBreakdown.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.accommodationDiscount is not null or booking.boardServiceDiscount is not null or booking.additionalServicesDiscount is not null %}
|
||||
{% if booking.accommodationDiscount is not null %}
|
||||
{% set accommodationDiscountAmount = ((priceBreakdown.basePrice + priceBreakdown.additionalPersonsPrice) * booking.accommodationDiscount / 100) | round %}
|
||||
<tr class="text-green-700">
|
||||
<td class="pt-1">Rabatt Unterkunft {{ booking.accommodationDiscount }} %</td>
|
||||
<td class="pt-1 text-right whitespace-nowrap">– {{ (accommodationDiscountAmount / 100) | format_currency(currency) }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if booking.boardServiceDiscount is not null %}
|
||||
{% set boardDiscountAmount = (priceBreakdown.boardPrice * booking.boardServiceDiscount / 100) | round %}
|
||||
<tr class="text-green-700">
|
||||
<td class="pt-1">Rabatt Verpflegung {{ booking.boardServiceDiscount }} %</td>
|
||||
<td class="pt-1 text-right whitespace-nowrap">– {{ (boardDiscountAmount / 100) | format_currency(currency) }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if booking.additionalServicesDiscount is not null %}
|
||||
{% set servicesDiscountAmount = (priceBreakdown.servicesPrice * booking.additionalServicesDiscount / 100) | round %}
|
||||
<tr class="text-green-700">
|
||||
<td class="pt-1">Rabatt Zusatzleistungen {{ booking.additionalServicesDiscount }} %</td>
|
||||
<td class="pt-1 text-right whitespace-nowrap">– {{ (servicesDiscountAmount / 100) | format_currency(currency) }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<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">{{ ((booking.totalPrice ?? priceBreakdown.total) / 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 %}
|
||||
<button type="button"
|
||||
class="button button--secondary button--small"
|
||||
data-controller="clipboard"
|
||||
data-clipboard-content-value="{{ accessLink }}"
|
||||
data-clipboard-label-value="Kopiert!"
|
||||
data-action="clipboard#copy">
|
||||
{{ icon('copy', 'w-4 h-4') }} Link kopieren
|
||||
</button>
|
||||
<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 %}
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<button type="button"
|
||||
class="button button--secondary button--small"
|
||||
hx-get="{{ path('app_admin_accommodationbooking_generate_access_link', { id: booking.id }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
{{ accessLink ? 'Link erneut generieren' : 'Link generieren' }}
|
||||
</button>
|
||||
{% if accessLink %}
|
||||
<button type="button"
|
||||
class="button button--secondary button--small"
|
||||
hx-get="{{ path('app_admin_accommodationbooking_send_access_link', { id: booking.id }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
Link senden
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</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 %}
|
||||
@@ -0,0 +1,29 @@
|
||||
{% 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 grid lg:grid-cols-4 gap-y-4 lg:gap-x-8">
|
||||
{{ form_row(form.dateFrom) }}
|
||||
{{ form_row(form.dateTo) }}
|
||||
{{ form_row(form.season) }}
|
||||
{{ form_row(form.type) }}
|
||||
</div>
|
||||
{{ form_row(form.pricePerNight) }}
|
||||
{{ form_row(form.priceAdditionalPerson) }}
|
||||
{{ 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 }) }}#prices"
|
||||
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) }}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Preis neu für Gruppenhaus {{ accommodation.name }}</twig:page:heading>
|
||||
{% include 'admin/accommodation_price/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Preis für Gruppenhaus {{ accommodation.name }}{% if duplicate %} duplizieren{% endif %}</twig:page:heading>
|
||||
{% include 'admin/accommodation_price/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends 'htmx_confirmation_modal.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
Möchtest du den Gruppenhaus-Preis <em>{{ accommodation.name }}, {{ accommodationPrice.dateFrom | date('d.m.Y') }} - {{ accommodationPrice.dateTo | date('d.m.Y') }}</em> wirklich löschen?
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{% form_theme form 'forms_admin.html.twig' %}
|
||||
|
||||
{{ form_start(form) }}
|
||||
<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 }) }}#additional-services"
|
||||
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) }}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Zusatzleistung neu für Gruppenhaus {{ accommodation.name }}</twig:page:heading>
|
||||
{% include 'admin/additional_service/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Zusatzleistung für Gruppenhaus {{ accommodation.name }}{% if duplicate %} duplizieren{% endif %}</twig:page:heading>
|
||||
{% include 'admin/additional_service/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends 'htmx_confirmation_modal.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
Möchtest du die Zusatzleistung <em>{{ additionalService.label }}, {{ accommodation.name }}</em> wirklich löschen?
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% form_theme form 'forms_admin.html.twig' %}
|
||||
|
||||
{{ form_start(form) }}
|
||||
<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 }) }}#board-services"
|
||||
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) }}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Verpflegungsleistung neu für Gruppenhaus {{ accommodation.name }}</twig:page:heading>
|
||||
{% include 'admin/board_service/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>Verpflegungsleistung für Gruppenhaus {{ accommodation.name }}{% if duplicate %} duplizieren{% endif %}</twig:page:heading>
|
||||
{% include 'admin/board_service/_form.html.twig' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends 'htmx_confirmation_modal.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
Möchtest du die Verpflegungsleistung <em>{{ boardService.label }}, {{ accommodation.name }}</em> wirklich löschen?
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-2xl font-bold pb-8">
|
||||
Buchungsentwürfe
|
||||
</h1>
|
||||
<twig:page:heading>Buchungsentwürfe</twig:page:heading>
|
||||
<div class="data-table-wrapper">
|
||||
<div class="data-table-wrapper__inner text-sm">
|
||||
<table class="data-table">
|
||||
@@ -44,32 +42,17 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% embed '_partials/_dropdown.html.twig' %}
|
||||
{% block content %}
|
||||
<a href="{{ path('app_admin_bookingeditdraft_show', { 'id': draft.id, 'r': return_url() }) }}"
|
||||
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm"
|
||||
role="menuitem"
|
||||
tabindex="-1">
|
||||
Details
|
||||
</a>
|
||||
<a href="{{ path('app_admin_bookingeditdraft_export', { 'id': draft.id }) }}"
|
||||
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm"
|
||||
role="menuitem"
|
||||
tabindex="-1">
|
||||
XLS-Export
|
||||
</a>
|
||||
<button type="button"
|
||||
class="text-red-500 hover:text-primary block px-4 py-2 text-sm w-full text-left"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
hx-get="{{ path('app_admin_bookingeditdraft_delete', { 'id': draft.id, 'r': return_url() }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
löschen
|
||||
</button>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
<twig:dropdown>
|
||||
<twig:dropdown:link url="{{ path('app_admin_bookingeditdraft_show', { 'id': draft.id, 'r': return_url() }) }}">
|
||||
Details
|
||||
</twig:dropdown:link>
|
||||
<twig:dropdown:link url="{{ path('app_admin_bookingeditdraft_export', { 'id': draft.id }) }}">
|
||||
XLS-Export
|
||||
</twig:dropdown:link>
|
||||
<twig:dropdown:hxbutton url="{{ path('app_admin_bookingeditdraft_delete', { 'id': draft.id, 'r': return_url() }) }}" warning>
|
||||
löschen
|
||||
</twig:dropdown:hxbutton>
|
||||
</twig:dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-2xl font-bold pb-8">
|
||||
Buchungsentwurf
|
||||
</h1>
|
||||
<twig:page:heading>Buchungsentwurf</twig:page:heading>
|
||||
<h2 class="text-xl font-bold pb-4">
|
||||
Vorgang {{ draft.bookingNumber }}
|
||||
</h2>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-2xl font-bold pb-8">
|
||||
Logs
|
||||
</h1>
|
||||
<twig:page:heading>Logs</twig:page:heading>
|
||||
<div class="data-table-wrapper">
|
||||
<div class="data-table-wrapper__inner text-sm">
|
||||
<table class="data-table">
|
||||
@@ -53,16 +51,11 @@
|
||||
</td>
|
||||
<td>
|
||||
{% if entry.channel == 'bpn' %}
|
||||
{% embed '_partials/_dropdown.html.twig' %}
|
||||
{% block content %}
|
||||
<a href="{{ path('app_admin_log_xmldumps', { 'id': entry.id }) }}"
|
||||
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm"
|
||||
role="menuitem"
|
||||
tabindex="-1">
|
||||
<twig:dropdown>
|
||||
<twig:dropdown:link url="{{ path('app_admin_log_xmldumps', { 'id': entry.id }) }}">
|
||||
XML-Dumps
|
||||
</a>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
</twig:dropdown:link>
|
||||
</twig:dropdown>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-2xl font-bold pb-8">
|
||||
XML-Dumps für Request ID {{ logEntry.requestId }}
|
||||
</h1>
|
||||
<twig:page:heading>XML-Dumps für Request ID {{ logEntry.requestId }}</twig:page:heading>
|
||||
{% if dumps is empty %}
|
||||
<div class="alert alert-warning">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
@@ -37,9 +35,9 @@
|
||||
</td>
|
||||
<td>
|
||||
{% if dump.type == 'request' %}
|
||||
<span class="badge bg-primary">Request</span>
|
||||
<twig:badge>Request</twig:badge>
|
||||
{% else %}
|
||||
<span class="badge bg-success">Response</span>
|
||||
<twig:badge variant="success">Response</twig:badge>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-2xl font-bold pb-8">
|
||||
Benutzeraccounts
|
||||
</h1>
|
||||
<twig:page:heading>Benutzeraccounts</twig:page:heading>
|
||||
<div class="data-table-wrapper">
|
||||
<div class="data-table-wrapper__inner text-sm">
|
||||
<table class="data-table">
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<link rel="icon" href="{{ asset('build/favicon/icon.svg') }}" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="{{ asset('build/favicon/apple-touch-icon.png') }}">
|
||||
{% block stylesheets %}
|
||||
{{ encore_entry_link_tags('app') }}
|
||||
{{ encore_entry_link_tags('admin') }}
|
||||
{% endblock %}
|
||||
{% block javascripts %}
|
||||
{{ encore_entry_script_tags('app') }}
|
||||
{{ encore_entry_script_tags('admin') }}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body class="bg-white text-gray-700 font-sans antialiased {{ booking_theme() }}">
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{% set variant = variant ?? 'primary' %}
|
||||
{% set cva = html_cva('badge', {
|
||||
variant: {
|
||||
primary: 'bg-primary text-white',
|
||||
success: 'bg-green-500 text-white',
|
||||
warning: 'bg-red-500 text-white',
|
||||
}
|
||||
}, [], { variant: 'primary' }) %}
|
||||
<span {{ attributes.defaults({class: cva.apply({variant: variant})}).without('variant') }}>
|
||||
{{ block('content') }}
|
||||
</span>
|
||||
@@ -0,0 +1,71 @@
|
||||
{% set weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'] %}
|
||||
<div class="space-y-4 mb-4">
|
||||
{% for month in months %}
|
||||
{% set totalCols = month.firstDow + (month.days | length) %}
|
||||
<div>
|
||||
<div class="text-sm font-semibold mb-1">{{ month.label }}</div>
|
||||
<div class="overflow-x-auto">
|
||||
<div class="w-max">
|
||||
<div class="flex">
|
||||
<div class="w-20 shrink-0"></div>
|
||||
{% for col in 0..(totalCols - 1) %}
|
||||
{% set dow = col % 7 %}
|
||||
<div class="w-6 text-center">
|
||||
<span class="block text-xs leading-none {{ dow >= 5 ? 'text-red-400' : 'text-gray-300' }}">{{ weekdays[dow] }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="flex mb-0.5">
|
||||
<div class="w-20 shrink-0"></div>
|
||||
{% if month.firstDow > 0 %}
|
||||
{% for i in 1..month.firstDow %}
|
||||
<div class="w-6"></div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% for dayNum, day in month.days %}
|
||||
<div class="w-6 text-center">
|
||||
<span class="{{ html_classes('block text-xs leading-none cursor-default', {'font-bold text-secondary': day.isToday, 'text-gray-500': not day.isToday}) }}"
|
||||
{% if day.tooltip %}{{ stimulus_controller('tooltip', {'content': day.tooltip}) }}{% endif %}
|
||||
>{{ dayNum }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% for row in [
|
||||
{label: 'Preis', key: 'hasPrice', color: 'bg-blue-500'},
|
||||
{label: 'Verpfl.', key: 'hasBoard', color: 'bg-green-500'},
|
||||
{label: 'Zusatzl.', key: 'hasAdditional', color: 'bg-amber-500'},
|
||||
{label: 'n. v.', key: 'isBlocked', color: 'bg-red-500'},
|
||||
] %}
|
||||
<div class="flex items-center mt-0.5">
|
||||
<div class="w-20 shrink-0 text-xs text-gray-500">{{ row.label }}</div>
|
||||
{% if month.firstDow > 0 %}
|
||||
{% for i in 1..month.firstDow %}
|
||||
<div class="w-6 px-px"><div class="h-3"></div></div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% for dayNum, day in month.days %}
|
||||
<div class="w-6 px-px">
|
||||
<div class="h-3 {{ attribute(day, row.key) ? row.color : 'bg-gray-100' }}"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-x-6 gap-y-2 mb-4 text-sm text-gray-600">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="inline-block w-4 h-3 bg-blue-500"></span> Preiskonfigurationen
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="inline-block w-4 h-3 bg-green-500"></span> Verpflegungsleistungen
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="inline-block w-4 h-3 bg-amber-500"></span> optionale Zusatzleistungen
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="inline-block w-4 h-3 bg-red-500"></span> nicht verfügbar
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<div id="calendar-wrapper">
|
||||
<twig:calendar:grid :months="months"/>
|
||||
<div class="flex items-center justify-between mt-4">
|
||||
<button class="button button--secondary button--small"
|
||||
hx-get="{{ path('app_admin_accommodation_calendar', {id: accommodation.id, month: prevMonth}) }}"
|
||||
hx-target="#calendar-wrapper"
|
||||
hx-swap="outerHTML">
|
||||
← {{ prevLabel }}
|
||||
</button>
|
||||
<button class="button button--secondary button--small"
|
||||
hx-get="{{ path('app_admin_accommodation_calendar', {id: accommodation.id, month: nextMonth}) }}"
|
||||
hx-target="#calendar-wrapper"
|
||||
hx-swap="outerHTML">
|
||||
{{ nextLabel }} →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
{% set warning = warning ?? false %}
|
||||
{% set method = method ?? 'get' %}
|
||||
<button type="button"
|
||||
class="{{ html_classes('block px-4 py-2 text-sm w-full text-left', {
|
||||
'text-red-500 hover:text-primary': warning,
|
||||
'text-gray-700 hover:text-primary': not warning
|
||||
}) }}"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
{{ attributes
|
||||
.defaults({
|
||||
'hx-target': 'body',
|
||||
'hx-swap': 'beforeend',
|
||||
})
|
||||
.without('url', 'warning', 'method')
|
||||
}}
|
||||
hx-{{ method | lower }}="{{ url }}"
|
||||
>
|
||||
{{ block('content') }}
|
||||
</button>
|
||||
+10
-3
@@ -1,9 +1,16 @@
|
||||
<div class="relative inline-block text-left group">
|
||||
<button type="button" class="flex items-center text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-0" aria-expanded="true" aria-haspopup="true">
|
||||
<div class="relative inline-block text-left group cursor-pointer">
|
||||
<button type="button"
|
||||
class="flex items-center text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-0"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true">
|
||||
<span class="sr-only">Funktionen</span>
|
||||
{{ icon('dots', 'w-6 h-6') }}
|
||||
</button>
|
||||
<div class="hidden group-hover:block absolute right-0 top-0 z-10 w-56 origin-top-right pt-4" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
|
||||
<div class="hidden group-hover:block absolute right-0 top-0 z-10 w-56 origin-top-right pt-4"
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="menu-button"
|
||||
tabindex="-1">
|
||||
<div class="rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div class="py-1 menu menu--pulldown" role="none">
|
||||
{% block content %}{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% set warning = warning ?? false %}
|
||||
<a href="{{ url }}"
|
||||
class="{{ html_classes('block px-4 py-2 text-sm w-full', {
|
||||
'text-red-500 hover:text-primary': warning,
|
||||
'text-gray-700 hover:text-primary': not warning
|
||||
}) }}"
|
||||
role="menuitem"
|
||||
tabindex="-1">
|
||||
{{ block('content') }}
|
||||
</a>
|
||||
@@ -0,0 +1 @@
|
||||
<h1 class="text-2xl font-bold pb-8">{{ block('content') }}</h1>
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends 'email/layout.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<h1>
|
||||
Neue Unterkunfts{% if booking.inquiry %}anfrage{% else %}buchung{% endif %}
|
||||
</h1>
|
||||
|
||||
{% 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>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,47 @@
|
||||
{% 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>
|
||||
</table>
|
||||
|
||||
<p class="small">Bei Fragen antworte einfach auf diese E-Mail.</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends 'email/layout.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Angebot angenommen</h1>
|
||||
|
||||
{% 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>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends 'email/layout.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Deine Buchung ist bestätigt</h1>
|
||||
|
||||
<p>
|
||||
Hallo {{ booking.firstName }}, vielen Dank! Deine Buchung ist jetzt verbindlich bestätigt.
|
||||
</p>
|
||||
|
||||
{% if accessLink %}
|
||||
<p>
|
||||
<a href="{{ accessLink }}" class="button">Buchung ansehen</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>
|
||||
</table>
|
||||
|
||||
<p class="small">Bei Fragen antworte einfach auf diese E-Mail.</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,245 @@
|
||||
{% use 'form_div_layout.html.twig' %}
|
||||
|
||||
{%- block form_row -%}
|
||||
{%- set widget_attr = {} -%}
|
||||
{%- if help is not empty -%}
|
||||
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
|
||||
{%- endif -%}
|
||||
<div{% with {attr: row_attr} %}{{ block('attributes') }}{% endwith %}>
|
||||
{{- form_label(form) -}}
|
||||
{{- form_widget(form, widget_attr) -}}
|
||||
{{- form_errors(form) -}}
|
||||
{{- form_help(form) -}}
|
||||
</div>
|
||||
{%- endblock form_row -%}
|
||||
|
||||
{%- block form_label -%}
|
||||
{% set class = 'block font-bold pb-2' %}
|
||||
{% if errors|length %}
|
||||
{% set class = class ~ ' text-red-500' %}
|
||||
{% endif %}
|
||||
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ class)|trim}) %}
|
||||
{{ parent() }}
|
||||
{%- endblock form_label -%}
|
||||
|
||||
{%- block form_errors -%}
|
||||
{%- if errors|length > 0 -%}
|
||||
<ul class="pb-2">
|
||||
{%- for error in errors -%}
|
||||
<li class="text-red-500">{{ error.message }}</li>
|
||||
{%- endfor -%}
|
||||
</ul>
|
||||
{%- endif -%}
|
||||
{%- endblock form_errors -%}
|
||||
|
||||
{%- block form_widget_simple -%}
|
||||
{%- set type = type|default('text') -%}
|
||||
{%- if type != 'hidden' -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset text-sm')|trim }) -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
|
||||
{% else %}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-400 focus:ring-primary' }) -%}
|
||||
{%- endif -%}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{{ parent() }}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
<input type="hidden" name="{{ form.vars.full_name }}" value="{{ form.vars.value }}">
|
||||
{%- endif -%}
|
||||
{%- endblock form_widget_simple -%}
|
||||
|
||||
{%- block textarea_widget -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset text-sm')|trim }) -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
|
||||
{% else %}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-400 focus:ring-primary' }) -%}
|
||||
{%- endif -%}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
|
||||
{%- endif -%}
|
||||
{{ parent() }}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
<input type="hidden" name="{{ form.vars.full_name }}" value="{{ form.vars.value }}">
|
||||
{%- endif -%}
|
||||
{%- endblock textarea_widget -%}
|
||||
|
||||
{%- block checkbox_widget -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' h-4 w-4 rounded text-primary')|trim }) -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' border-red-500 text-red-500 ring-red-500 focus:ring-red-500' }) -%}
|
||||
{% else %}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' border-gray-300 focus:ring-primary' }) -%}
|
||||
{%- endif -%}
|
||||
{%- if attr.disabled is defined and attr.disabled == true -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
|
||||
{%- endif -%}
|
||||
<input type="checkbox" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
|
||||
{%- endblock checkbox_widget -%}
|
||||
|
||||
{% block checkbox_row %}
|
||||
<div class="relative flex items-start">
|
||||
<div class="flex h-6 items-center">
|
||||
{{ form_widget(form) }}
|
||||
</div>
|
||||
<div class="{{ html_classes('ml-3 leading-6', { 'text-red-500': errors|length }) }}">
|
||||
<label for="{{ form.vars.id }}">
|
||||
{{ form.vars.label }}
|
||||
</label>
|
||||
{{- form_errors(form) -}}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{%- block choice_widget_collapsed -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset text-sm')|trim }) -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
|
||||
{% else %}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-400 focus:ring-primary' }) -%}
|
||||
{%- endif -%}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
|
||||
{%- endif -%}
|
||||
{{ parent() }}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
<input type="hidden" name="{{ form.vars.full_name }}" value="{{ form.vars.value }}">
|
||||
{%- endif -%}
|
||||
{%- endblock choice_widget_collapsed -%}
|
||||
|
||||
{%- block choice_widget_expanded -%}
|
||||
{%- for child in form %}
|
||||
<div class="relative flex items-start py-2 first:pt-0">
|
||||
<div class="min-w-0 flex-1 text-sm leading-4">
|
||||
<label for="{{ child.vars.id }}" class="select-none font-medium text-gray-900">
|
||||
{{ child.vars.label }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="ml-3 flex h-4 items-center">
|
||||
{{- form_widget(child) -}}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor -%}
|
||||
{%- endblock choice_widget_expanded -%}
|
||||
|
||||
{%- block birthday_widget -%}
|
||||
<div class="grid grid-cols-3 gap-x-1">
|
||||
{{ form_widget(form.children['day']) }}
|
||||
{{ form_widget(form.children['month']) }}
|
||||
{{ form_widget(form.children['year']) }}
|
||||
</div>
|
||||
{%- endblock -%}
|
||||
|
||||
{% block abbreviated_date_widget %}
|
||||
{%- set class = 'flex items-center space-x-1 ' ~ attr.class|default('') -%}
|
||||
{%- do form.setRendered -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
|
||||
{%- endif -%}
|
||||
<div class="{{ class }}">
|
||||
{{ form_widget(form.month, { 'attr': attr })}}
|
||||
<span>/</span>
|
||||
{{ form_widget(form.year, { 'attr': attr })}}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{%- block money_widget -%}
|
||||
{% set currency_class = 'absolute top-1/2 transform -translate-y-1/2 right-0 mr-2' %}
|
||||
{% if errors|length %}
|
||||
{% set currency_class = currency_class ~ ' text-red-500' %}
|
||||
{% else %}
|
||||
{% set currency_class = currency_class ~ ' text-gray-600' %}
|
||||
{% endif %}
|
||||
<div class="relative">
|
||||
{{ block('form_widget_simple') }}
|
||||
<span class="{{ currency_class }}">€</span>
|
||||
</div>
|
||||
{%- endblock money_widget -%}
|
||||
|
||||
{%- block datepicker_widget -%}
|
||||
{%- set minDate = form.vars.min_date ? form.vars.min_date | date('Y-m-d') : null -%}
|
||||
{%- set maxDate = form.vars.max_date ? form.vars.max_date | date('Y-m-d') : null -%}
|
||||
{%- set displayValue = value ? value|date('d.m.Y') : '' -%}
|
||||
{%- set inputClass = 'block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset text-sm' -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set inputClass = inputClass ~ ' ring-red-500 placeholder:red-500 focus:ring-red-500' -%}
|
||||
{%- else -%}
|
||||
{%- set inputClass = inputClass ~ ' placeholder:text-gray-900 focus:ring-primary' -%}
|
||||
{%- endif -%}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
{%- set inputClass = inputClass ~ ' cursor-not-allowed' -%}
|
||||
{%- endif -%}
|
||||
<div {{ stimulus_controller('datepicker', { 'minDate': minDate, 'maxDate': maxDate, 'disableWeekends': form.vars.disable_weekends }) }}>
|
||||
<input type="text"
|
||||
id="{{ id }}"
|
||||
class="{{ inputClass }}"
|
||||
value="{{ displayValue }}"
|
||||
{% if attr.placeholder is defined %}placeholder="{{ attr.placeholder }}"{% endif %}
|
||||
{% if attr['aria-describedby'] is defined %}aria-describedby="{{ attr['aria-describedby'] }}"{% endif %}
|
||||
{% if required %}required{% endif %}
|
||||
{% if disabled is defined and disabled %}disabled{% endif %}
|
||||
{{ stimulus_target('datepicker', 'field') }} />
|
||||
<input type="hidden"
|
||||
name="{{ form.vars.full_name }}"
|
||||
value="{{ value }}"
|
||||
{{ stimulus_target('datepicker', 'hiddenField') }} />
|
||||
</div>
|
||||
{%- endblock datepicker_widget %}
|
||||
|
||||
{%- block autocomplete_widget -%}
|
||||
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' autocomplete-input block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset text-sm')|trim }) -%}
|
||||
{%- if errors|length -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
|
||||
{% else %}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-400 focus:ring-primary' }) -%}
|
||||
{%- endif -%}
|
||||
{%- if disabled is defined and disabled == true -%}
|
||||
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
|
||||
{%- endif -%}
|
||||
<div class="autocomplete"
|
||||
{{ stimulus_controller('autocomplete', {
|
||||
'action': form.vars.action ?? null,
|
||||
'endpointUrl': form.vars.endpoint_url ?? null,
|
||||
'choices': form.vars.choices ?? null
|
||||
}) }}
|
||||
>
|
||||
<input type="text"
|
||||
class="{{ attr.class }}"
|
||||
value="{{ form.vars.initial_label }}"
|
||||
{{ stimulus_target('autocomplete', 'input') }}>
|
||||
<ul class="autocomplete-result-list"></ul>
|
||||
<input type="hidden"
|
||||
name="{{ form.vars.full_name }}"
|
||||
{{ stimulus_target('autocomplete', 'field') }}
|
||||
value="{{ form.vars.value }}">
|
||||
<button type="button"
|
||||
class="autocomplete-icon hidden"
|
||||
{{ stimulus_action('autocomplete', 'reset') }}
|
||||
{{ stimulus_target('autocomplete', 'resetButton') }}>
|
||||
{{ icon('close', 'w-4 h-4 text-gray-700') }}
|
||||
</button>
|
||||
<div class="autocomplete-icon"
|
||||
{{ stimulus_target('autocomplete', 'searchIcon') }}>
|
||||
{{ icon('search', 'w-4 h-4 text-gray-700') }}
|
||||
</div>
|
||||
</div>
|
||||
{%- endblock autocomplete_widget -%}
|
||||
|
||||
{%- block multiselect_widget -%}
|
||||
<div class="relative" {{ stimulus_controller('multiselect', { 'placeholder': form.vars.empty_label }) }}>
|
||||
<div {{ stimulus_action('multiselect', 'toggle', 'click')}}
|
||||
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') }}
|
||||
</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 %}
|
||||
{{ form_row(item, { 'attr': { 'data-action': 'multiselect#update', 'data-multiselect-target': 'choice', 'data-label': item.vars.label } }) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endblock multiselect_widget -%}
|
||||
@@ -0,0 +1,31 @@
|
||||
{% extends 'htmx_modal.html.twig' %}
|
||||
|
||||
{% block title %}Angebot verbindlich buchen{% 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) }}
|
||||
|
||||
<p class="mb-6">
|
||||
Möchtest du dieses Angebot jetzt verbindlich buchen?
|
||||
</p>
|
||||
|
||||
{{ form_start(confirmationForm, {
|
||||
'action': path('app_groups_booking_offer_confirm', { uuid: booking.uuid }),
|
||||
'attr': {
|
||||
'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="button" class="button button--secondary" {{ stimulus_action('modal', 'close') }}>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button type="submit" class="button button--primary">
|
||||
Jetzt verbindlich buchen
|
||||
</button>
|
||||
</div>
|
||||
{{ form_end(confirmationForm) }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,196 @@
|
||||
{% block booking_summary %}
|
||||
<div class="bg-white h-full flex flex-col lg:border-l lg:border-primary-bg"
|
||||
id="booking-summary">
|
||||
{# Header #}
|
||||
<div class="shrink-0 px-4 lg:px-8 py-4 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">{{ (displayTotal / 100)|format_currency(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 not null
|
||||
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 not null %}
|
||||
<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">
|
||||
{{ booking.dateFrom|date('d.m.Y') }} – {{ booking.dateTo|date('d.m.Y') }},
|
||||
<br>
|
||||
{{ booking.nights }} {{ booking.nights != 1 ? 'Nächte' : 'Nacht' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2">
|
||||
Anzahl Personen
|
||||
</td>
|
||||
<td class="p-2 text-right">
|
||||
{{ booking.paxCount }}
|
||||
</td>
|
||||
</tr>
|
||||
{% if booking.minorsCount > 0 %}
|
||||
<tr>
|
||||
<td class="p-2">davon Kinder (0–3 J.)</td>
|
||||
<td class="p-2 text-right">{{ booking.minorsCount }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if booking.childrenCount > 0 %}
|
||||
<tr>
|
||||
<td class="p-2">davon Kinder (4–{{ ctx.accommodation.maxAdolescentAge }} J.)</td>
|
||||
<td class="p-2 text-right">{{ booking.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(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(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(currency) }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if ctx.priceBreakdown.boardPrice > 0 %}
|
||||
<tr>
|
||||
<td class="p-2">Verpflegung{% if booking.boardServiceLabel %} ({{ booking.boardServiceLabel }}){% endif %}</td>
|
||||
<td class="p-2 text-right whitespace-nowrap">{{ (ctx.priceBreakdown.boardPrice / 100)|format_currency(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(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(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(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(currency) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% if booking.accommodationDiscount is not null or booking.boardServiceDiscount is not null or booking.additionalServicesDiscount is not null %}
|
||||
{% if booking.accommodationDiscount is not null %}
|
||||
{% set accommodationDiscountAmount = ((ctx.priceBreakdown.basePrice + ctx.priceBreakdown.additionalPersonsPrice) * booking.accommodationDiscount / 100) | round %}
|
||||
<tr class="text-green-700">
|
||||
<td class="p-2">Rabatt Unterkunft {{ booking.accommodationDiscount }} %</td>
|
||||
<td class="p-2 text-right whitespace-nowrap">– {{ (accommodationDiscountAmount / 100)|format_currency(currency) }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if booking.boardServiceDiscount is not null %}
|
||||
{% set boardDiscountAmount = (ctx.priceBreakdown.boardPrice * booking.boardServiceDiscount / 100) | round %}
|
||||
<tr class="text-green-700">
|
||||
<td class="p-2">Rabatt Verpflegung {{ booking.boardServiceDiscount }} %</td>
|
||||
<td class="p-2 text-right whitespace-nowrap">– {{ (boardDiscountAmount / 100)|format_currency(currency) }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if booking.additionalServicesDiscount is not null %}
|
||||
{% set servicesDiscountAmount = (ctx.priceBreakdown.servicesPrice * booking.additionalServicesDiscount / 100) | round %}
|
||||
<tr class="text-green-700">
|
||||
<td class="p-2">Rabatt Zusatzleistungen {{ booking.additionalServicesDiscount }} %</td>
|
||||
<td class="p-2 text-right whitespace-nowrap">– {{ (servicesDiscountAmount / 100)|format_currency(currency) }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr class="border-t border-primary-bg font-bold text-base text-green-700">
|
||||
<td class="p-2">Gesamtpreis nach Rabatt</td>
|
||||
<td class="p-2 text-right whitespace-nowrap">{{ ((booking.totalPrice ?? ctx.priceBreakdown.total) / 100)|format_currency(currency) }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if booking.remarks %}
|
||||
<div class="border border-primary-bg mt-4">
|
||||
<div class="p-2 bg-primary-bg uppercase font-semibold">Anmerkungen</div>
|
||||
<div class="p-2 text-sm">
|
||||
{{ booking.remarks | nl2br }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% 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',
|
||||
4: 'app_groups_booking_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">
|
||||
<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">
|
||||
← 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 →
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
{% 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_4_confirm'),
|
||||
'attr': {
|
||||
'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 %}
|
||||
@@ -0,0 +1,18 @@
|
||||
{% extends 'htmx_modal.html.twig' %}
|
||||
|
||||
{% block title %}Anfrage bestätigen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p class="mb-6">Möchtest du diese unverbindliche Anfrage jetzt absenden?</p>
|
||||
|
||||
{{ form_start(inquiryForm, { 'action': path('app_groups_booking_step_4') }) }}
|
||||
<div class="flex flex-col space-y-4 md:flex-row md:space-y-0 justify-between">
|
||||
<button type="submit" class="button button--primary">
|
||||
Jetzt anfragen
|
||||
</button>
|
||||
<button type="button" class="button button--secondary" {{ stimulus_action('modal', 'close') }}>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
{{ form_end(inquiryForm) }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,31 @@
|
||||
<div class="border border-primary-bg mb-4">
|
||||
<div class="p-2 bg-primary-bg uppercase font-semibold">Persönliche Daten</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2 p-2 text-sm">
|
||||
<div>
|
||||
<span class="text-gray-600 font-semibold">Gruppenname:</span>
|
||||
{{ dto.groupName }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-600 font-semibold">Name:</span>
|
||||
{{ dto.salutation }} {{ dto.firstName }} {{ dto.lastName }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-600 font-semibold">E-Mail:</span>
|
||||
{{ dto.email }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-600 font-semibold">Telefon:</span>
|
||||
{{ dto.phone }}
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<span class="text-gray-600 font-semibold">Adresse:</span>
|
||||
{{ dto.street }}, {{ dto.zip }} {{ dto.city }}
|
||||
</div>
|
||||
{% if dto.remarks %}
|
||||
<div class="md:col-span-2">
|
||||
<span class="text-gray-600 font-semibold">Anmerkungen:</span>
|
||||
{{ dto.remarks }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,42 @@
|
||||
{% set selectedBoardService = null %}
|
||||
{% for service in ctx.boardServices %}
|
||||
{% if service.id == dto.selectedBoardServiceId %}
|
||||
{% set selectedBoardService = service %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% set selectedAdditionalServices = ctx.additionalServices|filter(service => service.id in dto.selectedAdditionalServiceIds) %}
|
||||
|
||||
<div class="border border-primary-bg mb-4">
|
||||
<div class="p-2 bg-primary-bg uppercase font-semibold">Ausgewählte Leistungen</div>
|
||||
<div class="p-2 text-sm">
|
||||
<div class="mb-2">
|
||||
<span class="text-gray-600 font-semibold">Verpflegung:</span>
|
||||
{% if selectedBoardService %}
|
||||
{{ selectedBoardService.label }}
|
||||
{% if selectedBoardService.price > 0 %}
|
||||
— {{ (selectedBoardService.price / 100)|format_currency(ctx.accommodation.currency) }} pro Person und Nacht
|
||||
{% endif %}
|
||||
{% if selectedBoardService.description %}
|
||||
<div class="text-xs text-gray-500">{{ selectedBoardService.description|nl2br }}</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Selbstversorgung
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if selectedAdditionalServices is not empty %}
|
||||
<div>
|
||||
<span class="text-gray-600 font-semibold">Zusatzleistungen:</span>
|
||||
<ul class="list-disc list-inside">
|
||||
{% for service in selectedAdditionalServices %}
|
||||
<li>
|
||||
{{ service.label }}
|
||||
— {{ (service.price / 100)|format_currency(ctx.accommodation.currency) }} {{ service.type.label|trans }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 not null
|
||||
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 not null %}
|
||||
<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 %}
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,216 @@
|
||||
{% extends 'layout_booking.html.twig' %}
|
||||
|
||||
{% macro imageGallery(images, thumbImages, defaultAlt) %}
|
||||
{% if images|length > 0 %}
|
||||
<div class="grid md:grid-cols-2 gap-8" {{ stimulus_controller('gallery') }}>
|
||||
{% for image in images %}
|
||||
<div class="relative block w-full h-auto cursor-pointer{{ loop.index0 >= 2 ? ' hidden' : '' }}"
|
||||
data-gallery-target="item"
|
||||
data-gallery-href="{{ image.url }}"
|
||||
data-gallery-alt="{{ image.alt ?? defaultAlt }}">
|
||||
<img src="{{ thumbImages[loop.index0].url ?? image.url }}"
|
||||
alt="{{ image.alt ?? defaultAlt }}"
|
||||
class="block w-full h-auto">
|
||||
{% if loop.index0 == 1 and images|length > 2 %}
|
||||
<div class="absolute inset-0 bg-black/50 flex items-center justify-center text-white font-bold text-lg">
|
||||
+{{ images|length - 2 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% import _self as macros %}
|
||||
|
||||
{% block title %}{{ booking.inquiry ? 'Dein Angebot' : 'Deine Buchung' }}{% endblock %}
|
||||
|
||||
{% block background %}bg-outer bg-outer--summer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% set currency = booking.pricingCurrency ?? priceBreakdown.currency %}
|
||||
{% set displayTotal = booking.totalPrice ?? (priceBreakdown.total ?? null) %}
|
||||
|
||||
<div class="flex-1 flex flex-col lg:grid lg:grid-cols-5 min-h-0 relative">
|
||||
|
||||
{# Sidebar summary #}
|
||||
<div id="booking-summary-wrapper"
|
||||
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/_offer_summary.html.twig' with {
|
||||
booking: booking,
|
||||
ctx: ctx,
|
||||
currency: currency,
|
||||
displayTotal: displayTotal,
|
||||
} %}
|
||||
</div>
|
||||
|
||||
{# Main area #}
|
||||
<div class="flex-1 order-2 lg:order-1 lg:col-span-3 bg-white flex flex-col gap-y-8 min-h-0 overflow-y-auto px-4 lg:px-8 py-8">
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
|
||||
<hgroup>
|
||||
<h1 class="text-2xl lg:text-4xl font-bold mb-2">
|
||||
{{ booking.inquiry ? 'Dein Angebot' : 'Deine Buchung' }}
|
||||
</h1>
|
||||
<p class="text-2xl">
|
||||
{{ ctx.accommodation.name }}
|
||||
</p>
|
||||
</hgroup>
|
||||
|
||||
{# Booked services #}
|
||||
{% if booking.boardServiceLabel or booking.additionalServices | length > 0 %}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-2">
|
||||
{{ booking.inquiry ? 'Angefragte' : 'Gebuchte' }} Leistungen
|
||||
</h2>
|
||||
<ul class="list-disc pl-4">
|
||||
{% if booking.boardServiceLabel %}
|
||||
<li>{{ booking.boardServiceLabel }}</li>
|
||||
{% endif %}
|
||||
{% for service in booking.additionalServices %}
|
||||
<li>{{ service.label }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if booking.inquiry %}
|
||||
<div>
|
||||
<button type="button"
|
||||
class="button button--primary w-full"
|
||||
hx-get="{{ path('app_groups_booking_offer_confirm', { uuid: booking.uuid }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
Angebot verbindlich buchen
|
||||
</button>
|
||||
</div>
|
||||
{% elseif booking.acceptedAt is not null %}
|
||||
<div class="flex items-center space-x-2">
|
||||
{{ icon('info', 'w-5 h-5') }}
|
||||
<div class="text-gray-500">
|
||||
Angebot angenommen am {{ booking.acceptedAt | date('d.m.Y') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# hotel description #}
|
||||
{% if ctx.hotelCmsData is not null %}
|
||||
<div {{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'iconOpen': 'rotate-180' }) }}>
|
||||
<button type="button"
|
||||
class="flex items-center justify-between w-full text-left"
|
||||
data-action="toggle#toggle">
|
||||
<h2>
|
||||
Die Unterkunft
|
||||
</h2>
|
||||
<svg class="w-5 h-5 shrink-0 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 class="rte-content" data-toggle-target="content">
|
||||
{% set icons = ctx.hotelCmsData.icons %}
|
||||
{% if icons is not empty %}
|
||||
<ul class="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
{% for icon, label in icons %}
|
||||
<li class="flex items-center gap-2">
|
||||
<div class="border-2 border-gray-900 rounded-full w-16 h-16 flex flex-col items-center justify-center">
|
||||
{{ icon(icon, 'w-8 h-8 shrink-0 text-gray-900') }}
|
||||
</div>
|
||||
{{ label }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{{ ctx.hotelCmsData.description | raw }}
|
||||
|
||||
<h2>
|
||||
Details zur Unterkunft
|
||||
</h2>
|
||||
{{ ctx.hotelCmsData.features | raw }}
|
||||
|
||||
<h2>
|
||||
Die Zimmer
|
||||
</h2>
|
||||
{{ ctx.hotelCmsData.roomTypes | raw }}
|
||||
|
||||
<h2>
|
||||
Weitere Infos
|
||||
</h2>
|
||||
{{ ctx.hotelCmsData.additionalInformation | raw }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ macros.imageGallery(ctx.hotelCmsData.images.resized.l, ctx.hotelCmsData.images.resized.m, ctx.accommodation.name) }}
|
||||
{% endif %}
|
||||
|
||||
{# region description #}
|
||||
{% if ctx.hotelCmsData is not null and ctx.hotelCmsData.region is not null %}
|
||||
{% set region = ctx.hotelCmsData.region %}
|
||||
<div {{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'iconOpen': 'rotate-180' }) }}>
|
||||
<button type="button"
|
||||
class="flex items-center justify-between w-full text-left"
|
||||
data-action="toggle#toggle">
|
||||
<h2>
|
||||
Die Region {{ region.name }}
|
||||
</h2>
|
||||
<svg class="w-5 h-5 shrink-0 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 class="rte-content" data-toggle-target="content">
|
||||
{% if region.altitude or region.length or region.lifts %}
|
||||
<ul class="flex flex-wrap gap-x-6 text-sm not-prose">
|
||||
{% if region.altitude %}
|
||||
<li>{{ region.altitude }} m Höhe</li>
|
||||
{% endif %}
|
||||
{% if region.length %}
|
||||
<li>{{ region.length }} km Pisten</li>
|
||||
{% endif %}
|
||||
{% if region.lifts %}
|
||||
<li>{{ region.lifts }} Lifte</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if region.webcam %}
|
||||
<p>
|
||||
<a href="{{ region.webcam }}" target="_blank" rel="noopener">Webcam der Region</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{{ region.description | default('') | raw }}
|
||||
|
||||
<h2>
|
||||
Skigebiet
|
||||
</h2>
|
||||
{{ region.skiArea | default('') | raw }}
|
||||
{{ region.skiAreaExtended | default('') | raw }}
|
||||
|
||||
{% if region.news %}
|
||||
<h2>Aktuelles aus der Region</h2>
|
||||
{{ region.news | raw }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if region.regionMaps is not null and region.regionMaps | length > 0 %}
|
||||
{% set regionMapImages = region.regionMaps | map(url => {url: url}) %}
|
||||
{{ macros.imageGallery(regionMapImages, [], region.name) }}
|
||||
{% endif %}
|
||||
|
||||
{{ macros.imageGallery(region.images.resized.l ?? [], region.images.resized.m ?? [], region.name) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,108 @@
|
||||
{% 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" class="button button--primary">
|
||||
weiter zu Schritt 4
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,74 @@
|
||||
{% extends 'layout_booking.html.twig' %}
|
||||
|
||||
{% block title %}Buchung bestätigen{% endblock %}
|
||||
|
||||
{% block background %}bg-outer bg-outer--summer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{# Pagination - mobile only (above summary) #}
|
||||
<div class="lg:hidden">
|
||||
{% include 'groups/booking/_pagination.html.twig' with { 'current_step': 4 } %}
|
||||
</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>
|
||||
|
||||
{# Confirmation recap #}
|
||||
<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': 4 } %}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
|
||||
<h2 class="pb-4">
|
||||
Bitte bestätige deine Angaben
|
||||
</h2>
|
||||
|
||||
{% if dto.isInquiry %}
|
||||
{% include '_partials/_alert.html.twig' with {
|
||||
level: 'warning',
|
||||
title: 'Diese Buchung wird als unverbindliche Anfrage behandelt',
|
||||
messages: dto.inquiryReasons
|
||||
} %}
|
||||
{% endif %}
|
||||
|
||||
{% include 'groups/booking/_step4_services_recap.html.twig' with { 'dto': dto, 'ctx': ctx } %}
|
||||
{% include 'groups/booking/_step4_personal_data_recap.html.twig' with { 'dto': dto } %}
|
||||
</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_3') }}" class="button button--secondary">
|
||||
Zurück
|
||||
</a>
|
||||
<div class="space-x-2">
|
||||
<button type="button"
|
||||
class="button button--secondary"
|
||||
hx-get="{{ path('app_groups_booking_step_4_confirm_inquiry') }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
anfragen
|
||||
</button>
|
||||
<button type="button"
|
||||
class="{{ html_classes('button button--primary', { 'opacity-50': dto.isInquiry }) }}"
|
||||
{% if dto.isInquiry %}disabled{% endif %}
|
||||
hx-get="{{ path('app_groups_booking_step_4_confirm') }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
Jetzt verbindlich buchen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
@@ -14,7 +14,12 @@
|
||||
</div>
|
||||
{% block content %}{% endblock %}
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<button type="button" class="button button--warning" {% block button_attribute %}hx-post="{{ app.request.uri }}"{% endblock %}>
|
||||
<button type="button"
|
||||
class="button button--warning"
|
||||
{% block button_attribute %}
|
||||
hx-post="{{ app.request.uri }}"
|
||||
hx-vals="{{ { _token: csrf_token(csrf_token_id) }|json_encode|e('html_attr') }}"
|
||||
{% endblock %}>
|
||||
{% block button_confirm %}Ja{% endblock %}
|
||||
</button>
|
||||
<button type="button" class="button button--neutral" {{ stimulus_action('modal', 'close') }}>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<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 inset-0 w-full h-full bg-gray-300/50 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-3xl">
|
||||
<div class="relative bg-white py-8 rounded-md">
|
||||
<div class="relative bg-white py-8 rounded-md shadow-lg">
|
||||
<div class="flex justify-between pb-4 px-8">
|
||||
<div class="text-2xl font-bold">
|
||||
{% block title %}{% endblock %}
|
||||
@@ -13,7 +13,7 @@
|
||||
<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">
|
||||
<div class="max-h-svh overflow-y-auto">
|
||||
<div class="px-8">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<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/70" {{ 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-3xl">
|
||||
<div class="relative bg-white py-8 rounded-md">
|
||||
<div class="flex justify-between pb-4 px-8">
|
||||
<div class="text-2xl font-bold">
|
||||
{% block title %}{% endblock %}
|
||||
</div>
|
||||
<button type="button"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10"
|
||||
{{ stimulus_action('modal', 'close') }}>
|
||||
<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-svh overflow-y-auto">
|
||||
<div class="px-8">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="hidden lg:block">
|
||||
<div class="hidden lg:block sticky top-0 h-screen">
|
||||
{{ knp_menu_render(knp_menu_get('admin_main')) }}
|
||||
</div>
|
||||
<div class="lg:col-span-3">
|
||||
@@ -37,8 +37,13 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-full h-full p-8 pt-0 overflow-y-scroll">
|
||||
{{ knp_menu_render(knp_menu_get('admin_main')) }}
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
{{ knp_menu_render(knp_menu_get('admin_main')) }}
|
||||
{% elseif is_granted('ROLE_GROUPS_MANAGER') %}
|
||||
{{ knp_menu_render(knp_menu_get('groups_main')) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include '_partials/_flashes_admin.html.twig' %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user