feat: panel layout for the accommodation booking edit and detail pages
This commit is contained in:
1 parent
5fdee68ccd
commit
eb45693257
9 files changed
+531
-398
No files matched your search
@@ -12,6 +12,7 @@ use App\Form\Admin\Groups\AccommodationBookingType;
|
||||
use App\Repository\Groups\AdditionalServiceRepository;
|
||||
use App\Repository\Groups\BoardServiceRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Service\AccommodationBookingBreakdownCalculator;
|
||||
use App\Service\AccommodationBookingService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -33,6 +34,7 @@ class EditController extends AbstractController
|
||||
private readonly AdditionalServiceRepository $additionalServiceRepo,
|
||||
private readonly UserRepository $userRepo,
|
||||
private readonly AccommodationBookingService $bookingService,
|
||||
private readonly AccommodationBookingBreakdownCalculator $breakdownCalculator,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -128,10 +130,7 @@ class EditController extends AbstractController
|
||||
// The submitted changes stay on the entity but are never flushed, so the form
|
||||
// comes back filled in with the error on it. Logging is safe here: the database
|
||||
// handler writes through DBAL, not the EntityManager.
|
||||
return $this->render('admin/accommodation_booking/edit.html.twig', [
|
||||
'booking' => $booking,
|
||||
'form' => $form,
|
||||
]);
|
||||
return $this->renderForm($booking, $form);
|
||||
}
|
||||
|
||||
// Editing never moves the booking on and never mails the customer: every status
|
||||
@@ -160,9 +159,22 @@ class EditController extends AbstractController
|
||||
return $this->redirectToRoute('app_admin_accommodationbooking_show', $parameters);
|
||||
}
|
||||
|
||||
return $this->renderForm($booking, $form);
|
||||
}
|
||||
|
||||
/**
|
||||
* The sidebar shows the stored price, not one computed from the submitted values: after a
|
||||
* refused save the entity carries the unsaved changes, and the price there is labelled as the
|
||||
* state of the last save.
|
||||
*
|
||||
* @param FormInterface<AccommodationBooking> $form
|
||||
*/
|
||||
private function renderForm(AccommodationBooking $booking, FormInterface $form): Response
|
||||
{
|
||||
return $this->render('admin/accommodation_booking/edit.html.twig', [
|
||||
'booking' => $booking,
|
||||
'form' => $form,
|
||||
'priceBreakdown' => $this->breakdownCalculator->compute($booking),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,11 @@ class AccommodationBookingPeriodType extends AbstractType
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
// "Überschrift" rather than "Bezeichnung", which the lines below use for their own field.
|
||||
->add('label', TextType::class, [
|
||||
'label' => 'Bezeichnung',
|
||||
'label' => 'Überschrift (optional)',
|
||||
'required' => false,
|
||||
'help' => 'Überschrift in der Preisaufstellung, z. B. „Vorausteam“. Ohne Bezeichnung steht dort nur der Zeitraum.',
|
||||
'attr' => ['placeholder' => 'z. B. Vorausteam'],
|
||||
])
|
||||
->add('nights', IntegerType::class, [
|
||||
'label' => 'Nächte',
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{#- One split-off period on the edit page, with its actions. Context: booking, period. -#}
|
||||
{% set period_params = { id: period.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">{{ period.dateFrom | date('d.m.Y') }} – {{ period.dateTo | date('d.m.Y') }}</td>
|
||||
<td class="py-1 whitespace-nowrap">{{ period.nights }} {{ period.nights != 1 ? 'Nächte' : 'Nacht' }}</td>
|
||||
<td class="py-1">
|
||||
{% if period.label %}
|
||||
<div class="font-medium">{{ period.label }}</div>
|
||||
{% endif %}
|
||||
{% for line in period.lines %}
|
||||
<div>{{ line.label }}</div>
|
||||
{% else %}
|
||||
<div class="text-gray-500">keine Positionen</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="py-1 text-right whitespace-nowrap">
|
||||
{{ (period.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>
|
||||
@@ -0,0 +1,14 @@
|
||||
{#- One split-off period of the price table on the detail page. Context: period, currency. -#}
|
||||
<tr class="border-t border-gray-200 font-medium">
|
||||
<td class="pt-2 pb-1" colspan="2">
|
||||
{% if period.label ?? null %}{{ period.label }} · {% endif %}
|
||||
{{ period.dateFrom | date('d.m.Y') }} – {{ period.dateTo | date('d.m.Y') }},
|
||||
{{ period.nights }} {{ period.nights != 1 ? 'Nächte' : 'Nacht' }}
|
||||
</td>
|
||||
</tr>
|
||||
{% for line in period.lines %}
|
||||
<tr>
|
||||
<td class="py-1">{{ line.label }}</td>
|
||||
<td class="py-1 text-right whitespace-nowrap">{{ (line.amount / 100) | format_currency(currency) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -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 %}
|
||||
@@ -1,12 +1,21 @@
|
||||
{% extends 'htmx_modal_admin.html.twig' %}
|
||||
{% form_theme form 'forms_admin.html.twig' %}
|
||||
|
||||
{# One line as a row of bare widgets; the column headings above name them once for all rows.
|
||||
Plain flex rather than an arbitrary grid template: the modal is loaded into a page whose
|
||||
stylesheet may predate any class used only here. #}
|
||||
{% macro line_row(line) %}
|
||||
<div class="flex items-start gap-2" data-form-collection-target="field">
|
||||
<div class="flex-1">{{ form_row(line.label) }}</div>
|
||||
<div class="w-40">{{ form_row(line.amount) }}</div>
|
||||
<div class="flex gap-2 items-start" data-form-collection-target="field">
|
||||
<div class="flex-1 min-w-0">
|
||||
{{ form_widget(line.label, { attr: { placeholder: 'z. B. Übernachtung' } }) }}
|
||||
{{ form_errors(line.label) }}
|
||||
</div>
|
||||
<div class="w-32 shrink-0">
|
||||
{{ form_widget(line.amount) }}
|
||||
{{ form_errors(line.amount) }}
|
||||
</div>
|
||||
<button type="button"
|
||||
class="mt-9 text-gray-500 hover:text-red-500"
|
||||
class="shrink-0 mt-2 text-gray-400 hover:text-red-500"
|
||||
data-action="form-collection#removeItem"
|
||||
title="Position entfernen">
|
||||
{{ icon('close', 'w-5 h-5') }}
|
||||
@@ -16,38 +25,45 @@
|
||||
|
||||
{% block content %}
|
||||
{% import _self as period %}
|
||||
<div class="text-xl font-bold pb-4">
|
||||
<div class="text-xl font-bold pb-1">
|
||||
{{ title }}
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 pb-4">
|
||||
<p class="text-sm text-gray-500 pb-6">
|
||||
Die Nächte werden vom Zeitraum der Gruppe abgezogen, der neu berechnet wird. Die Positionen
|
||||
kommen zum Gesamtpreis hinzu und fallen nur unter den Rabatt auf den Gesamtpreis.
|
||||
</p>
|
||||
{{ form_start(form) }}
|
||||
{{ form_errors(form) }}
|
||||
<div class="pb-4">
|
||||
{{ form_row(form.label) }}
|
||||
</div>
|
||||
<div class="w-40 pb-4">
|
||||
{{ form_row(form.nights) }}
|
||||
<div class="flex gap-4 items-start">
|
||||
{{ form_row(form.label, { row_attr: { class: 'flex-1 min-w-0' } }) }}
|
||||
{{ form_row(form.nights, { row_attr: { class: 'w-32 shrink-0' } }) }}
|
||||
</div>
|
||||
|
||||
{% set prototype %}{{ period.line_row(form.lines.vars.prototype) }}{% endset %}
|
||||
<div {{ stimulus_controller('form-collection', { prototype: prototype|json_encode }) }}>
|
||||
<div class="font-bold pb-2">Positionen</div>
|
||||
{{ form_errors(form.lines) }}
|
||||
<div class="space-y-2" data-form-collection-target="fields">
|
||||
{% for line in form.lines %}
|
||||
{{ period.line_row(line) }}
|
||||
{% endfor %}
|
||||
<div class="mt-6 border border-gray-200 rounded-md" {{ stimulus_controller('form-collection', { prototype: prototype|json_encode }) }}>
|
||||
<div class="flex gap-2 px-4 py-2 bg-gray-100 border-b border-gray-200 rounded-t-md text-sm font-bold">
|
||||
<div class="flex-1">Position</div>
|
||||
{# w-5 matches the remove button's icon, so the heading sits over the amounts. #}
|
||||
<div class="w-32 shrink-0">Betrag</div>
|
||||
<div class="w-5 shrink-0"></div>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
{{ form_errors(form.lines) }}
|
||||
<div class="space-y-2" data-form-collection-target="fields">
|
||||
{% for line in form.lines %}
|
||||
{{ period.line_row(line) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button"
|
||||
class="mt-3 text-sm font-medium text-primary hover:underline"
|
||||
data-action="form-collection#addItem">
|
||||
+ Position hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="button button--secondary button--small mt-2"
|
||||
data-action="form-collection#addItem">
|
||||
Position hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
{% do form.lines.setRendered %}
|
||||
<div class="flex justify-end pt-8">
|
||||
|
||||
<div class="flex justify-end pt-6">
|
||||
<button type="submit" class="button button--primary button--small">
|
||||
speichern
|
||||
</button>
|
||||
|
||||
@@ -1,103 +1,72 @@
|
||||
{% extends 'layout_admin.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% set edit_params = { 'id': booking.id } %}
|
||||
{% if app.request.query.get('r') %}
|
||||
{% set edit_params = edit_params | merge({ 'r': app.request.query.get('r') }) %}
|
||||
{% endif %}
|
||||
|
||||
<twig:page:heading>
|
||||
{{ booking.recordLabel }} · {% include 'admin/accommodation_booking/_status_label.html.twig' %}
|
||||
{{ booking.groupName }}
|
||||
</twig:page:heading>
|
||||
<p class="text-sm text-gray-500 mb-6">{{ booking.accommodation.name }}</p>
|
||||
|
||||
<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>
|
||||
{% if booking.periods is not empty %}
|
||||
<dt class="font-medium">Zeitraum Gruppe</dt>
|
||||
<dd>
|
||||
{{ booking.mainDateFrom | date('d.m.Y') }} – {{ booking.mainDateTo | date('d.m.Y') }},
|
||||
{{ booking.mainNights }} {{ booking.mainNights != 1 ? 'Nächte' : 'Nacht' }}
|
||||
</dd>
|
||||
{% endif %}
|
||||
<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">Herkunft</dt>
|
||||
<dd>{{ booking.origin.label }}</dd>
|
||||
<dt class="font-medium">Status</dt>
|
||||
<dd>{% include 'admin/accommodation_booking/_status_label.html.twig' %}</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 %}
|
||||
{% if booking.confirmedAt is not null %}
|
||||
<dt class="font-medium">Bestätigt am</dt>
|
||||
<dd>{{ booking.confirmedAt | 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>
|
||||
{# Same arrangement as the edit form: what was booked in the main column, status, the next
|
||||
step and the actions in a sidebar that stays 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">
|
||||
<dl class="grid grid-cols-2 gap-y-2 text-sm">
|
||||
{% if booking.boardServiceLabel %}
|
||||
<dt class="font-medium">Verpflegung</dt>
|
||||
<dd>{{ booking.boardServiceLabel }}</dd>
|
||||
<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>
|
||||
{% if booking.periods is not empty %}
|
||||
<dt class="font-medium">Zeitraum Gruppe</dt>
|
||||
<dd>
|
||||
{{ booking.mainDateFrom | date('d.m.Y') }} – {{ booking.mainDateTo | date('d.m.Y') }},
|
||||
{{ booking.mainNights }} {{ booking.mainNights != 1 ? 'Nächte' : 'Nacht' }}
|
||||
</dd>
|
||||
{% endif %}
|
||||
<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 %}
|
||||
{% for service in booking.additionalServices %}
|
||||
<dt class="font-medium">{{ loop.first ? 'Zusatzleistungen' : '' }}</dt>
|
||||
<dd>{{ service.label }}</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
</div>
|
||||
{% endif %}
|
||||
</twig:form:section>
|
||||
|
||||
{% if priceBreakdown is not null %}
|
||||
<div>
|
||||
<h2 class="text-lg font-bold mb-4">Preise</h2>
|
||||
{% if booking.boardServiceLabel or booking.additionalServices | length > 0 %}
|
||||
<twig:form:section title="Leistungen">
|
||||
<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>
|
||||
</twig:form:section>
|
||||
{% endif %}
|
||||
|
||||
{% if priceBreakdown is not null %}
|
||||
{% set currency = booking.pricingCurrency ?? priceBreakdown.currency %}
|
||||
<twig:form:section title="Preise">
|
||||
<table class="w-full text-sm">
|
||||
{% for period in priceBreakdown.periods | filter(p => p.position == 'leading') %}
|
||||
{{ block('period_rows') }}
|
||||
{% include 'admin/accommodation_booking/_price_period.html.twig' %}
|
||||
{% endfor %}
|
||||
{% if priceBreakdown.periods is not empty %}
|
||||
<tr class="border-t border-gray-200 font-medium">
|
||||
@@ -158,7 +127,7 @@
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for period in priceBreakdown.periods | filter(p => p.position == 'trailing') %}
|
||||
{{ block('period_rows') }}
|
||||
{% include 'admin/accommodation_booking/_price_period.html.twig' %}
|
||||
{% endfor %}
|
||||
<tr class="border-t border-gray-200 font-semibold">
|
||||
<td class="pt-2">
|
||||
@@ -192,153 +161,181 @@
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</twig:form:section>
|
||||
{% 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>
|
||||
<twig:form:section title="Kontakt">
|
||||
<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>
|
||||
</twig:form:section>
|
||||
|
||||
<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>
|
||||
|
||||
{% if booking.draft or booking.requested %}
|
||||
<div class="mt-6 border-t border-gray-200 pt-6">
|
||||
<h2 class="text-lg font-bold mb-2">Angebot</h2>
|
||||
<p class="text-sm text-gray-500 mb-2">
|
||||
Das Angebot wurde dem Kunden noch nicht zugestellt. Beim Versand wird der Zugangslink
|
||||
erzeugt und die Buchung wartet anschließend auf die Annahme durch den Kunden.
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button"
|
||||
class="button button--primary button--small"
|
||||
hx-get="{{ path('app_admin_accommodationbooking_send_offer', { id: booking.id }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
Angebot per E-Mail senden
|
||||
</button>
|
||||
<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">
|
||||
Link erzeugen
|
||||
</button>
|
||||
</div>
|
||||
{% if booking.remarks %}
|
||||
<twig:form:section title="Bemerkungen">
|
||||
<p class="text-sm">{{ booking.remarks | nl2br }}</p>
|
||||
</twig:form:section>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if booking.received %}
|
||||
<div class="mt-6 border-t border-gray-200 pt-6">
|
||||
<h2 class="text-lg font-bold mb-2">Bestätigung</h2>
|
||||
<p class="text-sm text-gray-500 mb-2">
|
||||
Die Buchung des Kunden ist eingegangen, aber es wurde noch keine Bestätigung verschickt.
|
||||
</p>
|
||||
<button type="button"
|
||||
class="button button--primary button--small"
|
||||
hx-get="{{ path('app_admin_accommodationbooking_confirm', { id: booking.id }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
Buchung per E-Mail bestätigen
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<aside class="space-y-6 xl:sticky xl:top-8">
|
||||
<twig:form:section title="Status">
|
||||
<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.managedBy is not null %}
|
||||
<dt class="text-gray-500">Bearbeiter:in</dt>
|
||||
<dd>{{ booking.managedBy.displayName }}</dd>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
<dt class="text-gray-500">Eingegangen</dt>
|
||||
<dd>{{ booking.createdAt | date('d.m.Y, H:i') }}</dd>
|
||||
{% if booking.updatedAt is not null and booking.updatedAt != booking.createdAt %}
|
||||
<dt class="text-gray-500">Aktualisiert</dt>
|
||||
<dd>{{ booking.updatedAt | date('d.m.Y, H:i') }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</twig:form:section>
|
||||
|
||||
<div class="mt-6 border-t border-gray-200 pt-6">
|
||||
<h2 class="text-lg font-bold mb-2">Zugangslink</h2>
|
||||
{% if not booking.customerAccessible %}
|
||||
<p class="text-sm text-gray-500">
|
||||
Ein Zugangslink wird beim Versand des Angebots erzeugt.
|
||||
</p>
|
||||
{% elseif accessLink %}
|
||||
<div class="flex items-center gap-2">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
{# One pink button per page, for the next step; everything else is grey, and the one
|
||||
destructive action is set apart below the others. #}
|
||||
{% set has_next_step = booking.draft or booking.requested or booking.received %}
|
||||
{% if booking.draft or booking.requested %}
|
||||
<twig:form:section title="Nächster Schritt: Angebot">
|
||||
<p class="text-sm text-gray-500 mb-3">
|
||||
Das Angebot wurde dem Kunden noch nicht zugestellt. Beim Versand wird der Zugangslink
|
||||
erzeugt und die Buchung wartet anschließend auf die Annahme durch den Kunden.
|
||||
</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
<button type="button"
|
||||
class="button button--primary button--small w-full"
|
||||
hx-get="{{ path('app_admin_accommodationbooking_send_offer', { id: booking.id }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
Angebot per E-Mail senden
|
||||
</button>
|
||||
<button type="button"
|
||||
class="button button--neutral button--small w-full"
|
||||
hx-get="{{ path('app_admin_accommodationbooking_generate_access_link', { id: booking.id }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
nur Link erzeugen
|
||||
</button>
|
||||
</div>
|
||||
</twig:form:section>
|
||||
{% elseif booking.received %}
|
||||
<twig:form:section title="Nächster Schritt: Bestätigung">
|
||||
<p class="text-sm text-gray-500 mb-3">
|
||||
Die Buchung des Kunden ist eingegangen, aber es wurde noch keine Bestätigung verschickt.
|
||||
</p>
|
||||
<button type="button"
|
||||
class="button button--primary button--small w-full"
|
||||
hx-get="{{ path('app_admin_accommodationbooking_confirm', { id: booking.id }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
Buchung per E-Mail bestätigen
|
||||
</button>
|
||||
</twig:form:section>
|
||||
{% endif %}
|
||||
|
||||
<twig:form:section title="Zugangslink">
|
||||
{% if not booking.customerAccessible %}
|
||||
<p class="text-sm text-gray-500">
|
||||
Ein Zugangslink wird beim Versand des Angebots erzeugt.
|
||||
</p>
|
||||
{% elseif accessLink %}
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button type="button"
|
||||
class="button button--neutral button--small sm:text-xs w-full"
|
||||
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>
|
||||
<button type="button"
|
||||
class="button button--neutral button--small sm:text-xs w-full"
|
||||
hx-get="{{ path('app_admin_accommodationbooking_send_access_link', { id: booking.id }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
Link senden
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
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">
|
||||
Der Zugangslink wird beim Versand des Angebots erzeugt.
|
||||
</p>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-500">
|
||||
Der Zugangslink wird beim Versand des Angebots erzeugt.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</twig:form:section>
|
||||
|
||||
<div class="flex justify-between pt-8">
|
||||
<a href="{{ returnUrl }}" class="button button--secondary button--small">
|
||||
zurück
|
||||
</a>
|
||||
<div class="flex items-center gap-2">
|
||||
{% if not booking.confirmed and not booking.discarded %}
|
||||
<button type="button"
|
||||
class="button button--warning button--small"
|
||||
hx-get="{{ path('app_admin_accommodationbooking_discard', { id: booking.id }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
Absagen
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if booking.confirmed %}
|
||||
<a href="{{ path('app_admin_accommodationbooking_pdf', { 'id': booking.id }) }}" class="button button--secondary button--small">
|
||||
PDF herunterladen
|
||||
</a>
|
||||
{% endif %}
|
||||
{% set edit_params = { 'id': booking.id } %}
|
||||
{% if app.request.query.get('r') %}
|
||||
{% set edit_params = edit_params | merge({ 'r': app.request.query.get('r') }) %}
|
||||
{% endif %}
|
||||
<button type="button"
|
||||
class="button button--secondary button--small"
|
||||
hx-get="{{ path('app_admin_accommodationbooking_duplicate', edit_params) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
duplizieren
|
||||
</button>
|
||||
<a href="{{ path('app_admin_accommodationbooking_edit', edit_params) }}" class="button button--primary button--small">
|
||||
bearbeiten
|
||||
<twig:form:section title="Aktionen">
|
||||
<div class="flex flex-col gap-2">
|
||||
<a href="{{ path('app_admin_accommodationbooking_edit', edit_params) }}"
|
||||
class="button {{ has_next_step ? 'button--neutral' : 'button--primary' }} button--small w-full">
|
||||
bearbeiten
|
||||
</a>
|
||||
<button type="button"
|
||||
class="button button--neutral button--small w-full"
|
||||
hx-get="{{ path('app_admin_accommodationbooking_duplicate', edit_params) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
duplizieren
|
||||
</button>
|
||||
{% if booking.confirmed %}
|
||||
<a href="{{ path('app_admin_accommodationbooking_pdf', { 'id': booking.id }) }}"
|
||||
class="button button--neutral button--small w-full">
|
||||
PDF herunterladen
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not booking.confirmed and not booking.discarded %}
|
||||
<div class="mt-4 pt-3 border-t border-gray-200 text-center">
|
||||
<button type="button"
|
||||
class="text-sm font-medium text-red-600 hover:underline"
|
||||
hx-get="{{ path('app_admin_accommodationbooking_discard', { id: booking.id }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
{{ booking.recordLabel }} absagen
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</twig:form:section>
|
||||
|
||||
<a href="{{ returnUrl }}" class="inline-block text-sm text-gray-500 hover:text-gray-900 hover:underline">
|
||||
← zurück
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{# One split-off period of the breakdown: its range as a heading, then its lines. #}
|
||||
{% block period_rows %}
|
||||
<tr class="border-t border-gray-200 font-medium">
|
||||
<td class="pt-2 pb-1" colspan="2">
|
||||
{% if period.label ?? null %}{{ period.label }} · {% endif %}
|
||||
{{ period.dateFrom | date('d.m.Y') }} – {{ period.dateTo | date('d.m.Y') }},
|
||||
{{ period.nights }} {{ period.nights != 1 ? 'Nächte' : 'Nacht' }}
|
||||
</td>
|
||||
</tr>
|
||||
{% for line in period.lines %}
|
||||
<tr>
|
||||
<td class="py-1">{{ line.label }}</td>
|
||||
<td class="py-1 text-right whitespace-nowrap">{{ (line.amount / 100) | format_currency(currency) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{#- A titled panel grouping the fields of a long admin form. No overflow-hidden: the dots
|
||||
dropdowns inside must be able to open past its edge. -#}
|
||||
<div {{ attributes.defaults({class: 'bg-white border border-gray-200 rounded-md'}).without('title') }}>
|
||||
<div class="flex items-center justify-between gap-4 px-4 py-2 bg-gray-100 border-b border-gray-200 rounded-t-md font-bold">
|
||||
{{ title }}
|
||||
{% block actions %}{% endblock %}
|
||||
</div>
|
||||
<div class="p-4">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -12,6 +12,7 @@ use App\Exception\ImplausibleBookingPriceException;
|
||||
use App\Repository\Groups\AdditionalServiceRepository;
|
||||
use App\Repository\Groups\BoardServiceRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Service\AccommodationBookingBreakdownCalculator;
|
||||
use App\Service\AccommodationBookingService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
@@ -72,6 +73,7 @@ class EditControllerTest extends TestCase
|
||||
$this->createStub(AdditionalServiceRepository::class),
|
||||
$this->createStub(UserRepository::class),
|
||||
$bookingService,
|
||||
$this->createStub(AccommodationBookingBreakdownCalculator::class),
|
||||
$form,
|
||||
);
|
||||
|
||||
@@ -108,6 +110,7 @@ class EditControllerTest extends TestCase
|
||||
$this->createStub(AdditionalServiceRepository::class),
|
||||
$this->createStub(UserRepository::class),
|
||||
$bookingService,
|
||||
$this->createStub(AccommodationBookingBreakdownCalculator::class),
|
||||
$form,
|
||||
);
|
||||
|
||||
@@ -171,6 +174,7 @@ class EditControllerTest extends TestCase
|
||||
$this->createStub(AdditionalServiceRepository::class),
|
||||
$userRepo,
|
||||
$this->createStub(AccommodationBookingService::class),
|
||||
$this->createStub(AccommodationBookingBreakdownCalculator::class),
|
||||
$form,
|
||||
$granted,
|
||||
);
|
||||
@@ -189,10 +193,11 @@ final class TestableEditController extends EditController
|
||||
AdditionalServiceRepository $additionalServiceRepo,
|
||||
UserRepository $userRepo,
|
||||
AccommodationBookingService $bookingService,
|
||||
AccommodationBookingBreakdownCalculator $breakdownCalculator,
|
||||
private readonly FormInterface $form,
|
||||
private readonly bool $granted = false,
|
||||
) {
|
||||
parent::__construct($entityManager, $logger, $boardServiceRepo, $additionalServiceRepo, $userRepo, $bookingService);
|
||||
parent::__construct($entityManager, $logger, $boardServiceRepo, $additionalServiceRepo, $userRepo, $bookingService, $breakdownCalculator);
|
||||
}
|
||||
|
||||
protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface
|
||||
|
||||
Reference in new issue
Block a user