feat: booking details pdf

This commit is contained in:
Björn Fromme
2026-08-18 14:26:24 +02:00
parent d3666c4618
commit 2d7a02341d
19 changed files with 1054 additions and 100 deletions
@@ -89,6 +89,11 @@
<twig:dropdown:link url="{{ path('app_admin_accommodationbooking_edit', { 'id': booking.id }) }}">
bearbeiten
</twig:dropdown:link>
{% if booking.confirmed %}
<twig:dropdown:link url="{{ path('app_admin_accommodationbooking_pdf', { 'id': booking.id }) }}">
PDF herunterladen
</twig:dropdown:link>
{% endif %}
</twig:dropdown>
</td>
</tr>
@@ -4,9 +4,8 @@
{% 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.
Möchtest du die Buchung für <em>{{ booking.groupName }}</em> verbindlich bestätigen und die Bestätigungs-E-Mail
versenden?
</div>
{% endblock %}
@@ -181,15 +181,14 @@
<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.
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 bestätigen
Buchung per E-Mail bestätigen
</button>
</div>
{% endif %}
@@ -238,8 +237,15 @@
<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 class="flex items-center gap-2">
{% if booking.confirmed %}
<a href="{{ path('app_admin_accommodationbooking_pdf', { 'id': booking.id }) }}" class="button button--secondary button--small">
PDF herunterladen
</a>
{% endif %}
<a href="{{ path('app_admin_accommodationbooking_edit', { 'id': booking.id }) }}" class="button button--primary button--small">
bearbeiten
</a>
</div>
</div>
{% endblock %}
+5 -26
View File
@@ -1,6 +1,8 @@
{#- 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). -#}
{#- Prices only — travel dates and group size belong to _stay_details.html.twig and the
PDF's own data block. Mirrors groups/booking/_offer_summary.html.twig, but with inline
styles: neither the email layout nor dompdf ships table styling or Tailwind.
Context: booking, priceBreakdown, currency (set in AccommodationBookingService and
AccommodationBookingPdfGenerator). -#}
{% 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;' %}
@@ -9,29 +11,6 @@
<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 }}">
+38
View File
@@ -0,0 +1,38 @@
{#- Accommodation and group facts for booking emails, kept out of _price_breakdown.html.twig
so that partial holds prices only. Context: booking. -#}
<h2>Unterkunft</h2>
<table>
<tr>
<td><strong>Name</strong></td>
<td>{{ booking.accommodation.name }}</td>
</tr>
<tr>
<td><strong>Anreise</strong></td>
<td>{{ booking.dateFrom|date('d.m.Y') }}</td>
</tr>
<tr>
<td><strong>Abreise</strong></td>
<td>{{ booking.dateTo|date('d.m.Y') }}</td>
</tr>
<tr>
<td><strong>Nächte</strong></td>
<td>{{ booking.nights }}</td>
</tr>
<tr>
<td><strong>Anzahl Personen</strong></td>
<td>{{ booking.paxCount }}</td>
</tr>
{% if booking.minorsCount > 0 %}
<tr>
<td><strong>davon Kinder (03 J.)</strong></td>
<td>{{ booking.minorsCount }}</td>
</tr>
{% endif %}
{% if booking.childrenCount > 0 %}
<tr>
<td><strong>davon Kinder (4{{ booking.accommodation.maxAdolescentAge }} J.)</strong></td>
<td>{{ booking.childrenCount }}</td>
</tr>
{% endif %}
</table>
@@ -11,21 +11,7 @@
</p>
{% endif %}
<h2>Unterkunft</h2>
<table>
<tr>
<td><strong>Name</strong></td>
<td>{{ booking.accommodation.name }}</td>
</tr>
<tr>
<td><strong>Anreise</strong></td>
<td>{{ booking.dateFrom|date('d.m.Y') }}</td>
</tr>
<tr>
<td><strong>Abreise</strong></td>
<td>{{ booking.dateTo|date('d.m.Y') }}</td>
</tr>
</table>
{% include 'email/_stay_details.html.twig' %}
{% include 'email/_price_breakdown.html.twig' %}
{% endblock %}
@@ -36,21 +36,7 @@
</p>
{% endif %}
<h2>Unterkunft</h2>
<table>
<tr>
<td><strong>Name</strong></td>
<td>{{ booking.accommodation.name }}</td>
</tr>
<tr>
<td><strong>Anreise</strong></td>
<td>{{ booking.dateFrom|date('d.m.Y') }}</td>
</tr>
<tr>
<td><strong>Abreise</strong></td>
<td>{{ booking.dateTo|date('d.m.Y') }}</td>
</tr>
</table>
{% include 'email/_stay_details.html.twig' %}
{% include 'email/_price_breakdown.html.twig' %}
@@ -27,24 +27,7 @@
</p>
{% endif %}
<h2>
Unterkunft
</h2>
<table>
<tr>
<td><strong>Name</strong></td>
<td>{{ booking.accommodation.name }}</td>
</tr>
<tr>
<td><strong>Anreise</strong></td>
<td>{{ booking.dateFrom|date('d.m.Y') }}</td>
</tr>
<tr>
<td><strong>Abreise</strong></td>
<td>{{ booking.dateTo|date('d.m.Y') }}</td>
</tr>
</table>
{% include 'email/_stay_details.html.twig' %}
{% include 'email/_price_breakdown.html.twig' %}
+1 -15
View File
@@ -9,21 +9,7 @@
</p>
{% endif %}
<h2>Unterkunft</h2>
<table>
<tr>
<td><strong>Name</strong></td>
<td>{{ booking.accommodation.name }}</td>
</tr>
<tr>
<td><strong>Anreise</strong></td>
<td>{{ booking.dateFrom|date('d.m.Y') }}</td>
</tr>
<tr>
<td><strong>Abreise</strong></td>
<td>{{ booking.dateTo|date('d.m.Y') }}</td>
</tr>
</table>
{% include 'email/_stay_details.html.twig' %}
{% include 'email/_price_breakdown.html.twig' %}
@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Buchung {{ booking.id }} {{ booking.groupName }}</title>
<style>
@page { size: A4 portrait; margin: 16mm 15mm; }
body {
font-family: 'DejaVu Sans', sans-serif;
font-size: 11px;
line-height: 1.4;
color: #111827;
margin: 0;
}
/* The included price breakdown emits a bare <h2>; without these rules it would
fall back to the browser defaults instead of the email layout's typography. */
h1 { font-size: 16px; margin: 0 0 2px; }
h2 { font-size: 12px; margin: 14px 0 4px; padding-bottom: 2px; border-bottom: 1px solid #d1d5db; }
table { width: 100%; border-collapse: collapse; }
td { vertical-align: top; }
.header { width: 100%; margin-bottom: 14px; }
.header td { vertical-align: middle; }
.header .meta { text-align: right; font-size: 10px; color: #6b7280; }
.columns { table-layout: fixed; }
.columns > tr > td { width: 50%; padding-right: 12px; }
.data th { width: 42%; text-align: left; font-weight: normal; color: #6b7280; padding: 2px 8px 2px 0; }
.data td { padding: 2px 0; }
/* The shared breakdown partial is sized for emails and carries its metrics as
inline styles, so scaling it down for print needs !important. */
.prices table { font-size: 11px !important; }
.prices td { padding: 2px 0 !important; }
.prices span { font-size: 9px !important; }
.footer { margin-top: 18px; font-size: 9px; color: #9ca3af; }
</style>
</head>
<body>
<table class="header">
<tr>
<td><img src="{{ logoSrc }}" alt="" style="height: 34px;"></td>
{#- No booking number: the local id has no counterpart in BusPro. -#}
<td class="meta">
{% if booking.confirmedAt %}Bestätigt am {{ booking.confirmedAt|date('d.m.Y') }}{% endif %}
</td>
</tr>
</table>
<h1>{{ booking.type.label }} · {{ booking.groupName }}</h1>
<table class="columns">
<tr>
<td>
<h2>Kontakt</h2>
<table class="data">
<tr><th>Gruppenname</th><td>{{ booking.groupName }}</td></tr>
<tr><th>Anrede</th><td>{{ booking.salutation|default('') }}</td></tr>
<tr><th>Name</th><td>{{ booking.firstName }} {{ booking.lastName }}</td></tr>
<tr><th>E-Mail</th><td>{{ booking.email|default('') }}</td></tr>
<tr><th>Telefon</th><td>{{ booking.phone|default('') }}</td></tr>
<tr>
<th>Adresse</th>
<td>
{% if booking.street %}
{{ booking.street }}<br>{{ booking.zip }} {{ booking.city }}
{% else %}
{% endif %}
</td>
</tr>
</table>
</td>
<td>
<h2>Reisedaten</h2>
<table class="data">
<tr><th>Gruppenhaus</th><td>{{ booking.accommodation.name }}</td></tr>
<tr><th>Anreise</th><td>{{ booking.dateFrom|date('d.m.Y') }}</td></tr>
<tr><th>Abreise</th><td>{{ booking.dateTo|date('d.m.Y') }}</td></tr>
<tr><th>Nächte</th><td>{{ booking.nights }}</td></tr>
<tr><th>Anzahl Personen</th><td>{{ booking.paxCount }}</td></tr>
{% if booking.minorsCount > 0 %}
<tr><th>davon Kinder (03 J.)</th><td>{{ booking.minorsCount }}</td></tr>
{% endif %}
{% if booking.childrenCount > 0 %}
<tr><th>davon Kinder (4{{ booking.accommodation.maxAdolescentAge }} J.)</th><td>{{ booking.childrenCount }}</td></tr>
{% endif %}
<tr><th>Art</th><td>{{ booking.type.label }}</td></tr>
</table>
</td>
</tr>
</table>
{% if booking.boardServiceLabel or booking.additionalServices|length > 0 %}
<h2>Gebuchte Leistungen</h2>
<table class="data">
{% if booking.boardServiceLabel %}
<tr><th>Verpflegung</th><td>{{ booking.boardServiceLabel }}</td></tr>
{% endif %}
{% for service in booking.additionalServices %}
<tr><th>{{ loop.first ? 'Zusatzleistungen' : '' }}</th><td>{{ service.label }}</td></tr>
{% endfor %}
</table>
{% endif %}
<div class="prices">
{% include 'email/_price_breakdown.html.twig' %}
</div>
{% if booking.remarks %}
<h2>Bemerkungen</h2>
<p style="margin: 0;">{{ booking.remarks|nl2br }}</p>
{% endif %}
<p class="footer">Erstellt am {{ 'now'|date('d.m.Y') }}</p>
</body>
</html>