248 lines
13 KiB
Twig
248 lines
13 KiB
Twig
{% extends 'layout_booking.html.twig' %}
|
|
|
|
{% block title %}Schritt 2: Personen & Leistungen{% endblock %}
|
|
|
|
{% block background %}bg-outer bg-outer--summer{% endblock %}
|
|
|
|
{% block content %}
|
|
{% form_theme form 'booking/_form_theme.html.twig' %}
|
|
|
|
{{ form_start(form, {
|
|
'attr': {
|
|
'class': 'flex-1 flex flex-col min-h-0',
|
|
}
|
|
}) }}
|
|
|
|
{# Pagination - mobile only (above summary) #}
|
|
<div class="lg:hidden">
|
|
{% include 'groups/booking/_pagination.html.twig' with { 'current_step': 2 } %}
|
|
</div>
|
|
|
|
<div class="flex-1 flex flex-col lg:grid lg:grid-cols-5 min-h-0 relative">
|
|
|
|
{# Sidebar summary #}
|
|
{% block accommodation_summary %}
|
|
<div id="accommodation-summary"
|
|
class="order-1 lg:order-2 lg:flex-1 lg:min-h-0 lg:col-span-2"
|
|
{{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }}
|
|
{% if htmx_oob_swap is defined and htmx_oob_swap %}hx-swap-oob="true"{% endif %}>
|
|
{% include 'groups/booking/_summary.html.twig' with { 'dto': dto, 'ctx': ctx } %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{# Main form #}
|
|
{% block accommodation_form %}
|
|
<div id="accommodation-form"
|
|
class="flex-1 order-2 lg:order-1 lg:col-span-3 bg-white flex flex-col min-h-0">
|
|
|
|
{# Pagination - desktop only (fixed above scrollable content) #}
|
|
<div class="hidden lg:block shrink-0">
|
|
{% include 'groups/booking/_pagination.html.twig' with { 'current_step': 2 } %}
|
|
</div>
|
|
|
|
{# The scroll container itself must survive the htmx refresh — only its contents
|
|
are swapped, so assets/loading.js can restore the scroll position. #}
|
|
<div id="booking-form"
|
|
class="flex-1 overflow-y-auto scroll-stable px-4 lg:px-8 py-8"
|
|
hx-post="{{ path('app_groups_booking_step_2_refresh') }}"
|
|
hx-trigger="change delay:300ms"
|
|
hx-target="#booking-form"
|
|
hx-swap="innerHTML">
|
|
{{ block('booking_form') }}
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
</div>
|
|
|
|
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
|
|
<div class="flex items-center justify-between">
|
|
<a href="{{ path('app_groups_booking_step_1') }}" class="button button--secondary">
|
|
Zurück
|
|
</a>
|
|
<button type="submit" class="button button--primary">
|
|
Weiter zu Schritt 3
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{{ form_rest(form) }}
|
|
{{ form_end(form) }}
|
|
{% endblock %}
|
|
|
|
{# Swapped into #booking-form on every htmx refresh #}
|
|
{% block booking_form %}
|
|
|
|
{# Flash messages - must be inside the swapped region to be displayed #}
|
|
{% include '_partials/_flashes.html.twig' %}
|
|
|
|
{# Validation errors #}
|
|
{% from '_partials/_validation_errors.html.twig' import validation_alert %}
|
|
{{ validation_alert(form) }}
|
|
|
|
{% if dto.isInquiry %}
|
|
{% include '_partials/_alert.html.twig' with {
|
|
level: 'warning',
|
|
title: 'Diese Buchung wird als unverbindliche Anfrage behandelt',
|
|
messages: dto.inquiryReasons
|
|
} %}
|
|
{% endif %}
|
|
|
|
{# Pax counts #}
|
|
<h2 class="pb-4">
|
|
Personenanzahl und gewünschte Leistungen
|
|
</h2>
|
|
|
|
<div class="grid lg:grid-cols-3 lg:gap-x-8">
|
|
{{ form_row(form.paxCount) }}
|
|
{{ form_row(form.minorsCount) }}
|
|
{{ form_row(form.childrenCount) }}
|
|
</div>
|
|
|
|
{# Board services — rendered manually so price is formatted in Twig #}
|
|
{% do form.selectedBoardServiceId.setRendered %}
|
|
{% if ctx.boardServices is not empty %}
|
|
<div class="pb-4">
|
|
<fieldset class="border border-primary-bg">
|
|
<legend class="w-full bg-primary-bg p-2 font-semibold uppercase">
|
|
Verpflegungsleistungen
|
|
</legend>
|
|
<table class="w-full table-fixed border-collapse">
|
|
{% for service in ctx.boardServices %}
|
|
<tr>
|
|
<td class="border border-primary-bg p-2 align-top">
|
|
<label for="board_service_{{ service.id }}" class="cursor-pointer">{{ service.label }}</label>
|
|
{% if service.description %}
|
|
<div {{ stimulus_controller('tooltip') }}>
|
|
<button type="button" data-tooltip-target="trigger" class="text-sm text-primary-light">Details</button>
|
|
<template data-tooltip-target="template">
|
|
<div class="text-sm">{{ service.description|nl2br }}</div>
|
|
</template>
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
<td class="border border-primary-bg p-2 align-top w-32 text-right">
|
|
<div>{% if service.price == 0 %}inkl.{% else %}{{ (service.price / 100)|format_currency(ctx.accommodation.currency) }}{% endif %}</div>
|
|
{% if service.price != 0 %}
|
|
<div class="text-xs text-gray-500">pro Person und Nacht</div>
|
|
{% endif %}
|
|
</td>
|
|
<td class="border border-primary-bg p-2 align-top w-12 text-center">
|
|
<input type="radio"
|
|
id="board_service_{{ service.id }}"
|
|
name="{{ form.selectedBoardServiceId.vars.full_name }}"
|
|
value="{{ service.id }}"
|
|
{% if dto.selectedBoardServiceId == service.id %}checked{% endif %}
|
|
class="form-radio">
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</table>
|
|
</fieldset>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Additional services — rendered manually to support checkbox/radio mix #}
|
|
{% do form.selectedAdditionalServiceIds.setRendered %}
|
|
{% set selectedAdditionalServiceBaseName = form.selectedAdditionalServiceIds.vars.full_name|replace({'[]': ''}) %}
|
|
|
|
{% if ctx.ungroupedAdditionalServices is not empty or ctx.groupedAdditionalServices is not empty %}
|
|
|
|
{# Wraps every fieldset so mutually exclusive services can be unchecked across fieldsets.
|
|
Purely for instant feedback — the server resolves the same conflicts on submit. #}
|
|
<div {{ stimulus_controller('service-exclusion') }} {{ stimulus_action('service-exclusion', 'select', 'change') }}>
|
|
|
|
{# Ungrouped → checkboxes #}
|
|
{% if ctx.ungroupedAdditionalServices is not empty %}
|
|
<div class="pb-4">
|
|
<fieldset class="border border-primary-bg">
|
|
<legend class="w-full bg-primary-bg p-2 font-semibold uppercase">
|
|
Optionale Zusatzleistungen
|
|
</legend>
|
|
<table class="w-full table-fixed border-collapse">
|
|
{% for service in ctx.ungroupedAdditionalServices %}
|
|
<tr>
|
|
<td class="border border-primary-bg p-2 align-top">
|
|
<label for="add_service_{{ service.id }}" class="cursor-pointer">{{ service.label }}</label>
|
|
{% if service.description %}
|
|
<div {{ stimulus_controller('tooltip') }}>
|
|
<button type="button" data-tooltip-target="trigger" class="text-sm text-primary-light">Details</button>
|
|
<template data-tooltip-target="template">
|
|
<div class="text-sm">{{ service.description|nl2br }}</div>
|
|
</template>
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
<td class="border border-primary-bg p-2 align-top w-32 text-right">
|
|
<div>{% if service.price == 0 %}inkl.{% else %}{{ (service.price / 100)|format_currency(ctx.accommodation.currency) }}{% endif %}</div>
|
|
<div class="text-xs text-gray-500">{{ service.type.label | trans }}</div>
|
|
</td>
|
|
<td class="border border-primary-bg p-2 align-top w-12 text-center">
|
|
<input type="checkbox"
|
|
id="add_service_{{ service.id }}"
|
|
name="{{ form.selectedAdditionalServiceIds.vars.full_name }}"
|
|
value="{{ service.id }}"
|
|
{% if service.id in dto.selectedAdditionalServiceIds %}checked{% endif %}
|
|
{% if service.exclusionGroup %}
|
|
{{ stimulus_target('service-exclusion', 'input') }}
|
|
data-exclusion-group="{{ service.exclusionGroup }}"
|
|
data-exclusive="{{ service.isExclusive ? '1' : '0' }}"
|
|
{% endif %}
|
|
class="form-checkbox">
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</table>
|
|
</fieldset>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Grouped → radio buttons per group, one fieldset per group #}
|
|
{% for groupName, services in ctx.groupedAdditionalServices %}
|
|
<div class="pb-4">
|
|
<fieldset class="border border-primary-bg">
|
|
<legend class="w-full bg-primary-bg p-2 font-semibold uppercase">{{ groupName }}</legend>
|
|
<table class="w-full table-fixed border-collapse">
|
|
{% for service in services %}
|
|
<tr>
|
|
<td class="border border-primary-bg p-2 align-top">
|
|
<label for="add_service_{{ service.id }}" class="cursor-pointer">{{ service.label }}</label>
|
|
{% if service.description %}
|
|
<div {{ stimulus_controller('tooltip') }}>
|
|
<button type="button" data-tooltip-target="trigger" class="text-sm text-primary-light">Details</button>
|
|
<template data-tooltip-target="template">
|
|
<div class="text-sm">{{ service.description|nl2br }}</div>
|
|
</template>
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
<td class="border border-primary-bg p-2 align-top w-32 text-right">
|
|
<div>{% if service.price == 0 %}inkl.{% else %}{{ (service.price / 100)|format_currency(ctx.accommodation.currency) }}{% endif %}</div>
|
|
<div class="text-xs text-gray-500">{{ service.type.label | trans }}</div>
|
|
</td>
|
|
<td class="border border-primary-bg p-2 align-top w-12 text-center">
|
|
<input type="radio"
|
|
id="add_service_{{ service.id }}"
|
|
name="{{ selectedAdditionalServiceBaseName }}[{{ groupName|e('html_attr') }}]"
|
|
value="{{ service.id }}"
|
|
{% if service.id in dto.selectedAdditionalServiceIds %}checked{% endif %}
|
|
{% if service.exclusionGroup %}
|
|
data-service-exclusion-target="input"
|
|
data-exclusion-group="{{ service.exclusionGroup }}"
|
|
data-exclusive="{{ service.isExclusive ? '1' : '0' }}"
|
|
{% endif %}
|
|
class="form-radio">
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</table>
|
|
</fieldset>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
</div>
|
|
|
|
{% endif %}
|
|
|
|
{% endblock %}
|