feat: panel layout for the accommodation booking edit and detail pages

This commit is contained in:
fromme committed 2026-09-30 18:06:45 +02:00
1 parent 5fdee68ccd
commit eb45693257
9 files changed
+531 -398

No files matched your search

@@ -22,155 +22,200 @@
{% 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) }}
{{ form_errors(form) }}
{# Ordered by what gets negotiated: the stay first, the contact data last. The sidebar keeps
status, price and the save button in view on wide screens. #}
<div class="xl:grid xl:grid-cols-3 xl:gap-6 xl:items-start space-y-6 xl:space-y-0">
<div class="xl:col-span-2 space-y-6">
<twig:form:section title="Aufenthalt">
<div class="grid sm:grid-cols-2 gap-4">
{{ form_row(form.dateFrom) }}
{{ form_row(form.dateTo) }}
</div>
<div class="grid sm:grid-cols-3 gap-4 pt-4">
{{ form_row(form.paxCount) }}
{{ form_row(form.minorsCount) }}
{{ form_row(form.childrenCount) }}
</div>
<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) }}
{# Saved by their own modals, which reprice the booking on the spot — see SplitPeriodController. #}
<div class="lg:col-span-2">
<h3 class="font-bold pb-1">Zeiträume</h3>
<p class="text-xs text-gray-500 pb-2">
Anreise und Abreise umfassen den ganzen Aufenthalt. Abgeteilte Zeiträume, etwa für ein
Vorausteam, werden mit eigenen Positionen berechnet; Personen und Leistungen oben gelten
nur für den Zeitraum der Gruppe.
</p>
<table class="w-full text-sm">
{% if booking.leadingPeriod %}
{% set p = booking.leadingPeriod %}
{{ block('period_row') }}
{% endif %}
<tr class="border-t border-gray-200">
<td class="py-1 whitespace-nowrap">{{ booking.mainDateFrom | date('d.m.Y') }} – {{ booking.mainDateTo | date('d.m.Y') }}</td>
<td class="py-1 whitespace-nowrap">{{ booking.mainNights }} {{ booking.mainNights != 1 ? 'Nächte' : 'Nacht' }}</td>
<td class="py-1" colspan="2">Gruppe</td>
<td class="py-1 text-right">
{# One period per side, so only the free sides are offered. #}
{% if booking.leadingPeriod is null or booking.trailingPeriod is null %}
<twig:dropdown>
{% if booking.leadingPeriod is null %}
<twig:dropdown:hxbutton url="{{ path('app_admin_accommodationbooking_period_split', back_params | merge({ position: 'leading' })) }}">
am Anfang abteilen
</twig:dropdown:hxbutton>
{% endif %}
{% if booking.trailingPeriod is null %}
<twig:dropdown:hxbutton url="{{ path('app_admin_accommodationbooking_period_split', back_params | merge({ position: 'trailing' })) }}">
am Ende abteilen
</twig:dropdown:hxbutton>
{% endif %}
</twig:dropdown>
{# Saved by their own modals, which reprice the booking on the spot — see SplitPeriodController. #}
<div class="mt-6 border-t border-gray-200 pt-4">
<div class="font-bold pb-1">Zeiträume</div>
<p class="text-xs text-gray-500 pb-2">
Anreise und Abreise umfassen den ganzen Aufenthalt. Abgeteilte Zeiträume, etwa für ein
Vorausteam, werden mit eigenen Positionen berechnet; Personen und Leistungen gelten
nur für den Zeitraum der Gruppe.
</p>
<table class="w-full text-sm">
{% if booking.leadingPeriod %}
{% include 'admin/accommodation_booking/_period_row.html.twig' with { period: booking.leadingPeriod } %}
{% endif %}
</td>
</tr>
{% if booking.trailingPeriod %}
{% set p = booking.trailingPeriod %}
{{ block('period_row') }}
<tr class="border-t border-gray-200">
<td class="py-1 whitespace-nowrap">{{ booking.mainDateFrom | date('d.m.Y') }} – {{ booking.mainDateTo | date('d.m.Y') }}</td>
<td class="py-1 whitespace-nowrap">{{ booking.mainNights }} {{ booking.mainNights != 1 ? 'Nächte' : 'Nacht' }}</td>
<td class="py-1" colspan="2">Gruppe</td>
<td class="py-1 text-right">
{# One period per side, so only the free sides are offered. #}
{% if booking.leadingPeriod is null or booking.trailingPeriod is null %}
<twig:dropdown>
{% if booking.leadingPeriod is null %}
<twig:dropdown:hxbutton url="{{ path('app_admin_accommodationbooking_period_split', back_params | merge({ position: 'leading' })) }}">
am Anfang abteilen
</twig:dropdown:hxbutton>
{% endif %}
{% if booking.trailingPeriod is null %}
<twig:dropdown:hxbutton url="{{ path('app_admin_accommodationbooking_period_split', back_params | merge({ position: 'trailing' })) }}">
am Ende abteilen
</twig:dropdown:hxbutton>
{% endif %}
</twig:dropdown>
{% endif %}
</td>
</tr>
{% if booking.trailingPeriod %}
{% include 'admin/accommodation_booking/_period_row.html.twig' with { period: booking.trailingPeriod } %}
{% endif %}
</table>
</div>
</twig:form:section>
{% if form.boardService is defined or form.selectedAdditionalServices is defined %}
<twig:form:section title="Leistungen">
<div class="space-y-4">
{% if form.boardService is defined %}
{{ form_row(form.boardService) }}
{% endif %}
{% if form.selectedAdditionalServices is defined %}
<div>
{{ form_label(form.selectedAdditionalServices) }}
{# Rows written out here rather than through the theme's choice_widget_expanded,
whose first:pt-0 would shift only the first cell of the two columns. #}
<div class="grid sm:grid-cols-2 gap-x-8">
{% for child in form.selectedAdditionalServices %}
<div class="relative flex items-start py-1">
<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 %}
</div>
{{ form_errors(form.selectedAdditionalServices) }}
</div>
{% endif %}
</div>
</twig:form:section>
{% endif %}
<twig:form:section title="Rabatte">
<div class="grid sm:grid-cols-3 gap-4">
{{ form_row(form.accommodationDiscount) }}
{{ form_row(form.boardServiceDiscount) }}
{{ form_row(form.additionalServicesDiscount) }}
</div>
<div class="grid sm:grid-cols-3 gap-4 pt-4">
{{ form_row(form.totalDiscountLabel, { row_attr: { class: 'sm:col-span-2' } }) }}
{{ form_row(form.totalDiscountAmount) }}
</div>
</twig:form:section>
<twig:form:section title="Kontakt">
<div class="grid sm:grid-cols-2 gap-4">
{{ form_row(form.groupName, { row_attr: { class: 'sm:col-span-2' } }) }}
{{ form_row(form.salutation) }}
<div class="hidden sm:block"></div>
{{ form_row(form.firstName) }}
{{ form_row(form.lastName) }}
{{ form_row(form.email) }}
{{ form_row(form.phone) }}
{{ form_row(form.street, { row_attr: { class: 'sm:col-span-2' } }) }}
{{ form_row(form.zip) }}
{{ form_row(form.city) }}
</div>
</twig:form:section>
<twig:form:section title="Bemerkungen">
{{ form_widget(form.remarks) }}
{{ form_errors(form.remarks) }}
</twig:form:section>
</div>
<aside class="space-y-6 xl:sticky xl:top-8">
<twig:form:section title="Status">
{# Read-only: the status only ever moves through a named action on the detail page,
so that it cannot be set without the email and timestamp that belong to it. #}
<dl class="grid grid-cols-2 gap-y-1 text-sm">
<dt class="text-gray-500">Status</dt>
<dd>{% include 'admin/accommodation_booking/_status_label.html.twig' %}</dd>
<dt class="text-gray-500">Herkunft</dt>
<dd>{{ booking.origin.label }}</dd>
{% if booking.acceptedAt is not null %}
<dt class="text-gray-500">Angenommen</dt>
<dd>{{ booking.acceptedAt | date('d.m.Y, H:i') }}</dd>
{% endif %}
{% if booking.confirmedAt is not null %}
<dt class="text-gray-500">Bestätigt</dt>
<dd>{{ booking.confirmedAt | date('d.m.Y, H:i') }}</dd>
{% endif %}
</dl>
{% if form.managedBy is defined %}
<div class="pt-4">
{{ form_row(form.managedBy) }}
</div>
{% endif %}
</table>
</div>
</twig:form:section>
{% 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) }}
{% if priceBreakdown is not null %}
{% set currency = booking.pricingCurrency ?? priceBreakdown.currency %}
<twig:form:section title="Preis">
<table class="w-full text-sm">
{% if priceBreakdown.periods is not empty %}
<tr>
<td class="py-1">Gruppe</td>
<td class="py-1 text-right whitespace-nowrap">{{ ((priceBreakdown.total - priceBreakdown.periodsTotal) / 100) | format_currency(currency) }}</td>
</tr>
<tr>
<td class="py-1">Zeiträume</td>
<td class="py-1 text-right whitespace-nowrap">{{ (priceBreakdown.periodsTotal / 100) | format_currency(currency) }}</td>
</tr>
{% endif %}
{% if priceBreakdown.discountedTotal != priceBreakdown.total %}
{% if priceBreakdown.periods is empty %}
<tr>
<td class="py-1">Preis</td>
<td class="py-1 text-right whitespace-nowrap">{{ (priceBreakdown.total / 100) | format_currency(currency) }}</td>
</tr>
{% endif %}
<tr class="text-green-700">
<td class="py-1">Rabatte</td>
<td class="py-1 text-right whitespace-nowrap">– {{ ((priceBreakdown.total - priceBreakdown.discountedTotal) / 100) | format_currency(currency) }}</td>
</tr>
{% endif %}
<tr class="border-t border-gray-200 font-bold">
<td class="pt-2">Gesamtpreis</td>
<td class="pt-2 text-right whitespace-nowrap">{{ (priceBreakdown.discountedTotal / 100) | format_currency(currency) }}</td>
</tr>
</table>
<p class="text-xs text-gray-500 pt-2">
Stand der letzten Speicherung. Änderungen oben werden erst beim Speichern neu berechnet.
</p>
</twig:form:section>
{% endif %}
{% if form.selectedAdditionalServices is defined %}
{{ form_row(form.selectedAdditionalServices) }}
{% endif %}
{% endif %}
<h2 class="lg:col-span-2 text-base font-bold pt-2">Status & Rabatt</h2>
{# Read-only: the status only ever moves through a named action on the detail page,
so that it cannot be set without the email and timestamp that belong to it. #}
<p class="lg:col-span-2 text-sm">
<span class="font-medium">Status:</span>
{% include 'admin/accommodation_booking/_status_label.html.twig' %}
<span class="text-gray-500">· Herkunft: {{ booking.origin.label }}</span>
</p>
{% 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 %}
{% if booking.confirmedAt is not null %}
<p class="lg:col-span-2 text-xs text-gray-500">
Bestätigt am {{ booking.confirmedAt | date('d.m.Y, H:i') }}
</p>
{% endif %}
{% if form.managedBy is defined %}
<div class="lg:col-span-2">
{{ form_row(form.managedBy) }}
<div class="flex flex-col gap-3">
<button type="submit" class="button button--primary button--small w-full">
speichern
</button>
<a href="{{ path('app_admin_accommodationbooking_show', back_params) }}" class="text-sm text-gray-500 hover:text-gray-900 hover:underline">
← zurück ohne speichern
</a>
</div>
{% endif %}
<div class="lg:col-span-2 grid lg:grid-cols-3 gap-y-4 lg:gap-x-8">
{{ form_row(form.accommodationDiscount) }}
{{ form_row(form.boardServiceDiscount) }}
{{ form_row(form.additionalServicesDiscount) }}
</div>
<div class="lg:col-span-2 grid lg:grid-cols-2 gap-y-4 lg:gap-x-8">
{{ form_row(form.totalDiscountLabel) }}
{{ form_row(form.totalDiscountAmount) }}
</div>
<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', back_params) }}" class="button button--secondary button--small">
zurück
</a>
<button type="submit" class="button button--primary button--small">
speichern
</button>
</aside>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}
{% block period_row %}
{% set period_params = { id: p.id } %}
{% if app.request.query.get('r') %}
{% set period_params = period_params | merge({ r: app.request.query.get('r') }) %}
{% endif %}
<tr class="border-t border-gray-200 align-top">
<td class="py-1 whitespace-nowrap">{{ p.dateFrom | date('d.m.Y') }} – {{ p.dateTo | date('d.m.Y') }}</td>
<td class="py-1 whitespace-nowrap">{{ p.nights }} {{ p.nights != 1 ? 'Nächte' : 'Nacht' }}</td>
<td class="py-1">
{% if p.label %}
<div class="font-medium">{{ p.label }}</div>
{% endif %}
{% for line in p.lines %}
<div>{{ line.label }}</div>
{% else %}
<div class="text-gray-500">keine Positionen</div>
{% endfor %}
</td>
<td class="py-1 text-right whitespace-nowrap">
{{ (p.total / 100) | format_currency(booking.pricingCurrency ?? booking.accommodation.currency) }}
</td>
<td class="py-1 text-right">
<twig:dropdown>
<twig:dropdown:hxbutton url="{{ path('app_admin_accommodationbooking_period_edit', period_params) }}">
bearbeiten
</twig:dropdown:hxbutton>
<twig:dropdown:hxbutton url="{{ path('app_admin_accommodationbooking_period_remove', period_params) }}" warning>
entfernen
</twig:dropdown:hxbutton>
</twig:dropdown>
</td>
</tr>
{% endblock %}