feat: groups price calculator admin crud, booking/offer flow and api

This commit is contained in:
Björn Fromme
2026-08-03 15:25:10 +02:00
parent eabed8295a
commit 9d2aa11fdb
258 changed files with 16231 additions and 582 deletions
@@ -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 (03 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 }}&nbsp;%</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 }}&nbsp;%</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 }}&nbsp;%</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>
+5 -12
View File
@@ -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>
+3 -5
View File
@@ -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 -3
View File
@@ -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">