feat: additional confirmation cycle and status for groups inquiries

This commit is contained in:
Björn Fromme
2026-08-18 12:48:17 +02:00
parent 5045846eed
commit 8dee65d0dc
28 changed files with 677 additions and 95 deletions
@@ -0,0 +1,5 @@
{#- Offen covers two situations that need different office action, told apart by the access
link: no link yet means the offer still has to be prepared, a link means the customer is
the one who has to act. Display only — both are the same status. -#}
{{- booking.status.label -}}
{%- if booking.open %} · {{ booking.accessLinkIssuedAt is not null ? 'Angebot versendet' : 'Angebot ausstehend' }}{% endif -%}
@@ -47,6 +47,11 @@
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) }}
@@ -66,7 +66,7 @@
{% endif %}
</td>
<td>
{{ booking.type.label }} · {{ booking.status.label }}
{{ booking.type.label }} · {% include 'admin/accommodation_booking/_status_label.html.twig' %}
</td>
<td>
{% set discounts = [] %}
@@ -0,0 +1,13 @@
{% extends 'htmx_confirmation_modal.html.twig' %}
{% block title %}Buchung bestätigen{% endblock %}
{% block content %}
<div>
Möchtest du die Buchung für <em>{{ booking.groupName }}</em> verbindlich bestätigen?
<em>{{ booking.email }}</em> erhält daraufhin die Buchungsbestätigung inklusive
Preisübersicht per E-Mail.
</div>
{% endblock %}
{% block button_confirm %}Bestätigen{% endblock %}
@@ -2,7 +2,7 @@
{% block content %}
<twig:page:heading>
{{ booking.type.label }} · {{ booking.status.label }}
{{ booking.type.label }} · {% include 'admin/accommodation_booking/_status_label.html.twig' %}
{{ booking.groupName }}
</twig:page:heading>
@@ -56,11 +56,15 @@
<dt class="font-medium">Art</dt>
<dd>{{ booking.type.label }}</dd>
<dt class="font-medium">Status</dt>
<dd>{{ booking.status.label }}</dd>
<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>
@@ -188,6 +192,23 @@
{% endif %}
</div>
{% 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">
Der Kunde hat die Buchung verbindlich abgeschickt, aber noch keine Bestätigung
erhalten. Prüfe Leistungen und Kapazitäten und bestätige die Buchung erst dann.
</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 bestätigen
</button>
</div>
{% endif %}
<div class="mt-6 border-t border-gray-200 pt-6">
<h2 class="text-lg font-bold mb-2">Zugangslink</h2>
{% if accessLink %}
+137
View File
@@ -0,0 +1,137 @@
{#- Price breakdown for booking emails. Mirrors groups/booking/_offer_summary.html.twig, but
with inline styles: the email layout ships no table styling and no Tailwind.
Context: booking, priceBreakdown, currency (set in AccommodationBookingService). -#}
{% if priceBreakdown is not null and currency is not null %}
{% set cell = 'padding: 4px 0; text-align: left; vertical-align: top;' %}
{% set amount = 'padding: 4px 0; text-align: right; vertical-align: top; white-space: nowrap;' %}
{% set note = 'display: block; font-size: 12px; color: #6b7280; margin: 0;' %}
<h2>Preise</h2>
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
<tr>
<td style="{{ cell }}">Zeitraum</td>
<td style="{{ amount }}">
{{ booking.dateFrom|date('d.m.Y') }} {{ booking.dateTo|date('d.m.Y') }},
{{ booking.nights }} {{ booking.nights != 1 ? 'Nächte' : 'Nacht' }}
</td>
</tr>
<tr>
<td style="{{ cell }}">Anzahl Personen</td>
<td style="{{ amount }}">{{ booking.paxCount }}</td>
</tr>
{% if booking.minorsCount > 0 %}
<tr>
<td style="{{ cell }}">davon Kinder (03 J.)</td>
<td style="{{ amount }}">{{ booking.minorsCount }}</td>
</tr>
{% endif %}
{% if booking.childrenCount > 0 %}
<tr>
<td style="{{ cell }}">davon Kinder (4{{ booking.accommodation.maxAdolescentAge }} J.)</td>
<td style="{{ amount }}">{{ booking.childrenCount }}</td>
</tr>
{% endif %}
{% if priceBreakdown.basePrice > 0 %}
<tr>
<td style="{{ cell }}">
Basispreis
<span style="{{ note }}">für {{ priceBreakdown.includedPax }} Pers.</span>
</td>
<td style="{{ amount }}">{{ (priceBreakdown.basePrice / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% if priceBreakdown.additionalPersonsPrice > 0 %}
<tr>
<td style="{{ cell }}">
Aufpreis Personenzahl
<span style="{{ note }}">für {{ priceBreakdown.effectivePax - priceBreakdown.includedPax }} Pers.</span>
</td>
<td style="{{ amount }}">{{ (priceBreakdown.additionalPersonsPrice / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% if priceBreakdown.shortTermSurcharge > 0 %}
<tr>
<td style="{{ cell }}">Aufpreis Kurzzeit</td>
<td style="{{ amount }}">{{ (priceBreakdown.shortTermSurcharge / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% if priceBreakdown.boardPrice > 0 %}
<tr>
<td style="{{ cell }}">Verpflegung{% if booking.boardServiceLabel %} ({{ booking.boardServiceLabel }}){% endif %}</td>
<td style="{{ amount }}">{{ (priceBreakdown.boardPrice / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% if priceBreakdown.undersubscriptionSurcharge > 0 %}
<tr>
<td style="{{ cell }}">
Verpflegungs-Aufschlag
<span style="{{ note }}">für Gruppen unter {{ priceBreakdown.undersubscriptionThreshold }} Pers.</span>
</td>
<td style="{{ amount }}">{{ (priceBreakdown.undersubscriptionSurcharge / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% for service in priceBreakdown.serviceDetails %}
{% if service.price > 0 %}
<tr>
<td style="{{ cell }}">{{ service.label }}</td>
<td style="{{ amount }}">{{ (service.price / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% endfor %}
{% if priceBreakdown.runningCosts > 0 %}
<tr>
<td style="{{ cell }}">Strom- und Abfallgebühren</td>
<td style="{{ amount }}">{{ (priceBreakdown.runningCosts / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
<tr>
<td style="{{ cell }} border-top: 1px solid #d1d5db; font-weight: bold;">
Gesamtpreis
<span style="{{ note }} font-weight: normal;">zzgl. ortsabhängiger Gebühren</span>
</td>
<td style="{{ amount }} border-top: 1px solid #d1d5db; font-weight: bold;">
{{ (priceBreakdown.total / 100)|format_currency(currency) }}
</td>
</tr>
{% set accommodationDiscountAmount = booking.accommodationDiscount is not null
? ((priceBreakdown.basePrice + priceBreakdown.additionalPersonsPrice) * booking.accommodationDiscount / 100) | round
: 0 %}
{% set boardDiscountAmount = booking.boardServiceDiscount is not null
? (priceBreakdown.boardPrice * booking.boardServiceDiscount / 100) | round
: 0 %}
{% set servicesDiscountAmount = booking.additionalServicesDiscount is not null
? (priceBreakdown.servicesPrice * booking.additionalServicesDiscount / 100) | round
: 0 %}
{% set discountSum = accommodationDiscountAmount + boardDiscountAmount + servicesDiscountAmount %}
{% if booking.accommodationDiscount is not null %}
<tr>
<td style="{{ cell }}">Rabatt Unterkunft {{ booking.accommodationDiscount }}&nbsp;%</td>
<td style="{{ amount }}"> {{ (accommodationDiscountAmount / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% if booking.boardServiceDiscount is not null %}
<tr>
<td style="{{ cell }}">Rabatt Verpflegung {{ booking.boardServiceDiscount }}&nbsp;%</td>
<td style="{{ amount }}"> {{ (boardDiscountAmount / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% if booking.additionalServicesDiscount is not null %}
<tr>
<td style="{{ cell }}">Rabatt Zusatzleistungen {{ booking.additionalServicesDiscount }}&nbsp;%</td>
<td style="{{ amount }}"> {{ (servicesDiscountAmount / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% if discountSum > 0 %}
{#- The frozen snapshot wins where it exists; without one the discounted total has to
be derived from the very rows shown above, so the mail can never contradict itself. -#}
<tr>
<td style="{{ cell }} border-top: 1px solid #d1d5db; font-weight: bold;">Gesamtpreis nach Rabatt</td>
<td style="{{ amount }} border-top: 1px solid #d1d5db; font-weight: bold;">
{{ ((booking.totalPrice ?? (priceBreakdown.total - discountSum)) / 100)|format_currency(currency) }}
</td>
</tr>
{% endif %}
</table>
{% endif %}
@@ -26,4 +26,6 @@
<td>{{ booking.dateTo|date('d.m.Y') }}</td>
</tr>
</table>
{% include 'email/_price_breakdown.html.twig' %}
{% endblock %}
@@ -1,37 +1,25 @@
{% extends 'email/layout.html.twig' %}
{% block body %}
{% set info_url = 'https://www.ep-reisen.de/reisen-fuer-gruppen/infos-zusatzleistungen/reiseinfos/ablauf-und-fristen-eurer-buchung/ablauf-und-fristen-eurer-buchung-fuer-sommeraufenthalte/' %}
{# Two states only: the office has published an offer (access link present), or the
inquiry has just arrived and the offer still has to be prepared. #}
{% if accessLink %}
<h1>
{{ booking.accepted ? 'Deine Buchung ist bestätigt' : 'Dein Angebot ist bereit' }}
Dein Angebot ist bereit
</h1>
<p>
Hallo {{ booking.firstName }},
</p>
{% if not booking.accepted %}
<p>
vielen Dank! Hiermit bestätigen wir den Eingang deiner Buchung.
</p>
<p>
Wir übergeben die Buchung nun für die Detailabstimmung an die Kollegen und Kolleginnen in der
Gruppenabwicklung. Sie werden sich rechtzeitig bei Euch melden und alle weiteren Details mit euch
besprechen. Keine Sorge, sollte es sich ein wenig verzögern, der Termin ist fest für euch reserviert und
geblockt! Wie es danach für euch weitergeht, könnt ihr euch <a href="{{ info_url }}" class="underline">hier schon einmal anschauen</a>.
</p>
{% else %}
<p>
vielen Dank für deine Anfrage! Wir melden uns so schnell wie möglich bei dir mit deinem persönlichen
Angebot.
</p>
{% endif %}
<p>
vielen Dank für deine Anfrage! Dein persönliches Angebot ist fertig. Schau es dir in Ruhe an und buche
verbindlich, wenn alles passt. Wir prüfen die Buchung anschließend und bestätigen sie dir per E-Mail.
</p>
<p>
<a href="{{ accessLink }}" class="button">
{{ booking.accepted ? 'Buchung ansehen' : 'Angebot ansehen & bestätigen' }}
Angebot ansehen &amp; buchen
</a>
</p>
{% else %}
@@ -64,6 +52,8 @@
</tr>
</table>
{% include 'email/_price_breakdown.html.twig' %}
<p>
Bei Fragen antworte einfach auf diese E-Mail.
</p>
@@ -12,7 +12,7 @@
</p>
<p>
vielen Dank! Hiermit bestätigen wir den Eingang deiner Buchung.
wir haben deine Buchung geprüft und bestätigen sie dir hiermit verbindlich.
</p>
<p>
Wir übergeben die Buchung nun für die Detailabstimmung an die Kollegen und Kolleginnen in der
@@ -46,6 +46,8 @@
</tr>
</table>
{% include 'email/_price_breakdown.html.twig' %}
{% if booking.remarks %}
<h2>
Anmerkungen
+2
View File
@@ -25,6 +25,8 @@
</tr>
</table>
{% include 'email/_price_breakdown.html.twig' %}
{% if booking.remarks %}
<h2>Anmerkungen</h2>
<p>{{ booking.remarks|nl2br }}</p>
+12 -4
View File
@@ -24,7 +24,7 @@
{% import _self as macros %}
{% block title %}{{ booking.accepted ? 'Deine Buchung' : 'Dein Angebot' }}{% endblock %}
{% block title %}{{ booking.acceptedAt is not null ? 'Deine Buchung' : 'Dein Angebot' }}{% endblock %}
{% block background %}bg-outer bg-outer--summer{% endblock %}
@@ -52,7 +52,7 @@
<hgroup>
<h1 class="text-2xl lg:text-4xl font-bold mb-2">
{{ booking.accepted ? 'Deine Buchung' : 'Dein Angebot' }}
{{ booking.acceptedAt is not null ? 'Deine Buchung' : 'Dein Angebot' }}
</h1>
<p class="text-2xl">
{{ ctx.accommodation.name }}
@@ -63,7 +63,7 @@
{% if booking.boardServiceLabel or booking.additionalServices | length > 0 %}
<div>
<h2 class="text-xl font-bold mb-2">
{{ booking.accepted ? 'Gebuchte' : 'Angefragte' }} Leistungen
{{ booking.acceptedAt is not null ? 'Gebuchte' : 'Angefragte' }} Leistungen
</h2>
<ul class="list-disc pl-4">
{% if booking.boardServiceLabel %}
@@ -86,11 +86,19 @@
Angebot verbindlich buchen
</button>
</div>
{% elseif booking.confirmedAt is not null %}
<div class="flex items-center space-x-2">
{{ icon('info', 'w-5 h-5') }}
<div class="text-gray-500">
Buchung bestätigt am {{ booking.confirmedAt | date('d.m.Y') }}
</div>
</div>
{% elseif booking.acceptedAt is not null %}
<div class="flex items-center space-x-2">
{{ icon('info', 'w-5 h-5') }}
<div class="text-gray-500">
Angebot angenommen am {{ booking.acceptedAt | date('d.m.Y') }}
Am {{ booking.acceptedAt | date('d.m.Y') }} bei uns eingegangen — wir prüfen deine
Buchung und melden uns mit der Bestätigung bei dir.
</div>
</div>
{% endif %}
+1 -1
View File
@@ -11,7 +11,7 @@
title: 'Vielen Dank!',
messages: [
resultType == 'booking'
? 'Deine Buchung ist bei uns eingegangen. Du erhältst in Kürze eine Bestätigung.'
? 'Deine Buchung ist bei uns eingegangen. Sobald sie geprüft ist, erhälst du eine Bestätigung per E-Mail.'
: 'Deine Anfrage ist bei uns eingegangen. Wir melden uns so schnell wie möglich bei dir.'
]
} %}