feat: split nights off an accommodation booking as separate periods

addresses #869f6z36v
This commit is contained in:
fromme committed 2026-09-30 18:06:20 +02:00
1 parent 972839286c
commit 7001bc0cf2
35 files changed
+2221 -25

No files matched your search

@@ -41,6 +41,48 @@
{{ 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>
{% endif %}
</td>
</tr>
{% if booking.trailingPeriod %}
{% set p = booking.trailingPeriod %}
{{ block('period_row') }}
{% endif %}
</table>
</div>
{% 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 %}
@@ -98,3 +140,37 @@
{{ 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 %}
@@ -0,0 +1,57 @@
{% extends 'htmx_modal_admin.html.twig' %}
{% form_theme form 'forms_admin.html.twig' %}
{% 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>
<button type="button"
class="mt-9 text-gray-500 hover:text-red-500"
data-action="form-collection#removeItem"
title="Position entfernen">
{{ icon('close', 'w-5 h-5') }}
</button>
</div>
{% endmacro %}
{% block content %}
{% import _self as period %}
<div class="text-xl font-bold pb-4">
{{ title }}
</div>
<p class="text-sm text-gray-500 pb-4">
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>
{% 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>
<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">
<button type="submit" class="button button--primary button--small">
speichern
</button>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}
@@ -0,0 +1,13 @@
{% extends 'htmx_confirmation_modal.html.twig' %}
{% block title %}Zeitraum entfernen{% endblock %}
{% block content %}
<div>
Möchtest du den Zeitraum {{ period.dateFrom | date('d.m.Y') }} – {{ period.dateTo | date('d.m.Y') }}
mit allen Positionen entfernen? Die Nächte gehen an den Zeitraum der Gruppe zurück, und der
Preis wird neu berechnet.
</div>
{% endblock %}
{% block button_confirm %}entfernen{% endblock %}
@@ -43,6 +43,13 @@
<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 %}
@@ -89,6 +96,17 @@
<h2 class="text-lg font-bold mb-4">Preise</h2>
{% set currency = booking.pricingCurrency ?? priceBreakdown.currency %}
<table class="w-full text-sm">
{% for period in priceBreakdown.periods | filter(p => p.position == 'leading') %}
{{ block('period_rows') }}
{% endfor %}
{% if priceBreakdown.periods is not empty %}
<tr class="border-t border-gray-200 font-medium">
<td class="pt-2 pb-1" colspan="2">
Gruppe · {{ booking.mainDateFrom | date('d.m.Y') }} – {{ booking.mainDateTo | date('d.m.Y') }},
{{ priceBreakdown.nights }} {{ priceBreakdown.nights != 1 ? 'Nächte' : 'Nacht' }}
</td>
</tr>
{% endif %}
{% if priceBreakdown.basePrice > 0 %}
<tr>
<td class="py-1">
@@ -139,6 +157,9 @@
<td class="py-1 text-right whitespace-nowrap">{{ (priceBreakdown.runningCosts / 100) | format_currency(currency) }}</td>
</tr>
{% endif %}
{% for period in priceBreakdown.periods | filter(p => p.position == 'trailing') %}
{{ block('period_rows') }}
{% endfor %}
<tr class="border-t border-gray-200 font-semibold">
<td class="pt-2">
Gesamtpreis
@@ -304,3 +325,20 @@
</div>
</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 %}
@@ -11,6 +11,17 @@
<h2>Preise</h2>
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
{% for period in priceBreakdown.periods|filter(p => p.position == 'leading') %}
{% include 'email/_price_breakdown_period.html.twig' %}
{% endfor %}
{% if priceBreakdown.periods is not empty %}
<tr>
<td colspan="2" style="{{ cell }} border-top: 1px solid #d1d5db; font-weight: bold;">
Gruppe · {{ booking.mainDateFrom|date('d.m.Y') }} – {{ booking.mainDateTo|date('d.m.Y') }},
{{ priceBreakdown.nights }} {{ priceBreakdown.nights != 1 ? 'Nächte' : 'Nacht' }}
</td>
</tr>
{% endif %}
{% if priceBreakdown.basePrice > 0 %}
<tr>
<td style="{{ cell }}">
@@ -64,6 +75,9 @@
<td style="{{ amount }}">{{ (priceBreakdown.runningCosts / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% for period in priceBreakdown.periods|filter(p => p.position == 'trailing') %}
{% include 'email/_price_breakdown_period.html.twig' %}
{% endfor %}
<tr>
<td style="{{ cell }} border-top: 1px solid #d1d5db; font-weight: bold;">
Gesamtpreis
@@ -103,3 +117,4 @@
{% endif %}
</table>
{% endif %}
@@ -0,0 +1,15 @@
{#- One split-off period of _price_breakdown.html.twig: its range as a heading, then its lines.
Context: period, cell, amount, currency. -#}
<tr>
<td colspan="2" style="{{ cell }} border-top: 1px solid #d1d5db; font-weight: bold;">
{% 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 style="{{ cell }}">{{ line.label }}</td>
<td style="{{ amount }}">{{ (line.amount / 100)|format_currency(currency) }}</td>
</tr>
{% endfor %}
+6
View File
@@ -19,6 +19,12 @@
<td><strong>Nächte</strong></td>
<td>{{ booking.nights }}</td>
</tr>
{% if booking.periods is not empty %}
<tr>
<td><strong>Zeitraum Gruppe</strong></td>
<td>{{ booking.mainDateFrom|date('d.m.Y') }} – {{ booking.mainDateTo|date('d.m.Y') }}</td>
</tr>
{% endif %}
<tr>
<td><strong>Anzahl Personen</strong></td>
<td>{{ booking.paxCount }}</td>
+21
View File
@@ -67,6 +67,16 @@
{{ booking.nights }} {{ booking.nights != 1 ? 'Nächte' : 'Nacht' }}
</td>
</tr>
{% if ctx.priceBreakdown.periods is not empty %}
<tr>
<td class="p-2">Zeitraum Gruppe</td>
<td class="p-2 text-right">
{{ booking.mainDateFrom|date('d.m.Y') }} – {{ booking.mainDateTo|date('d.m.Y') }},
<br>
{{ ctx.priceBreakdown.nights }} {{ ctx.priceBreakdown.nights != 1 ? 'Nächte' : 'Nacht' }}
</td>
</tr>
{% endif %}
<tr>
<td class="p-2">
Anzahl Personen
@@ -88,6 +98,14 @@
</tr>
{% endif %}
{% for period in ctx.priceBreakdown.periods|filter(p => p.position == 'leading') %}
{% include 'groups/offer/_summary_period.html.twig' %}
{% endfor %}
{% if ctx.priceBreakdown.periods is not empty %}
<tr class="border-t border-primary-bg font-semibold">
<td class="p-2" colspan="2">Gruppe</td>
</tr>
{% endif %}
{% if ctx.priceBreakdown.basePrice > 0 %}
<tr>
<td class="p-2">
@@ -141,6 +159,9 @@
<td class="p-2 text-right whitespace-nowrap">{{ (ctx.priceBreakdown.runningCosts / 100)|format_currency(currency) }}</td>
</tr>
{% endif %}
{% for period in ctx.priceBreakdown.periods|filter(p => p.position == 'trailing') %}
{% include 'groups/offer/_summary_period.html.twig' %}
{% endfor %}
<tr class="border-t border-primary-bg font-semibold text-base">
<td class="p-2">
Gesamtpreis
@@ -0,0 +1,15 @@
{#- One split-off period of the offer summary: its range as a heading, then its lines.
Context: period, currency. -#}
<tr class="border-t border-primary-bg font-semibold">
<td class="p-2" 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="p-2">{{ line.label }}</td>
<td class="p-2 text-right whitespace-nowrap">{{ (line.amount / 100)|format_currency(currency) }}</td>
</tr>
{% endfor %}
@@ -83,6 +83,9 @@
<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>
{% if booking.periods is not empty %}
<tr><th>Zeitraum Gruppe</th><td>{{ booking.mainDateFrom|date('d.m.Y') }} – {{ booking.mainDateTo|date('d.m.Y') }}</td></tr>
{% endif %}
<tr><th>Anzahl Personen</th><td>{{ booking.paxCount }}</td></tr>
{% if booking.minorsCount > 0 %}
<tr><th>davon Kinder (0–3 J.)</th><td>{{ booking.minorsCount }}</td></tr>