fix: avoid form jumping to top after making a selection
This commit is contained in:
@@ -72,7 +72,7 @@ class Step2Controller extends AbstractAccommodationController
|
|||||||
|
|
||||||
return $this->htmxOobResponse(
|
return $this->htmxOobResponse(
|
||||||
'groups/booking/step_2.html.twig',
|
'groups/booking/step_2.html.twig',
|
||||||
['accommodation_form', 'accommodation_summary'],
|
['booking_form', 'accommodation_summary'],
|
||||||
['dto' => $dto, 'ctx' => $ctx, 'form' => $form->createView()],
|
['dto' => $dto, 'ctx' => $ctx, 'form' => $form->createView()],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,174 +33,22 @@
|
|||||||
{# Main form #}
|
{# Main form #}
|
||||||
{% block accommodation_form %}
|
{% block accommodation_form %}
|
||||||
<div id="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"
|
class="flex-1 order-2 lg:order-1 lg:col-span-3 bg-white flex flex-col min-h-0">
|
||||||
{% if htmx_oob_swap is defined and htmx_oob_swap %}hx-swap-oob="true"{% endif %}>
|
|
||||||
|
|
||||||
{% include '_partials/_flashes.html.twig' %}
|
|
||||||
|
|
||||||
{# Pagination - desktop only (fixed above scrollable content) #}
|
{# Pagination - desktop only (fixed above scrollable content) #}
|
||||||
<div class="hidden lg:block shrink-0">
|
<div class="hidden lg:block shrink-0">
|
||||||
{% include 'groups/booking/_pagination.html.twig' with { 'current_step': 2 } %}
|
{% include 'groups/booking/_pagination.html.twig' with { 'current_step': 2 } %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="participant-form"
|
{# 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 px-4 lg:px-8 py-8"
|
class="flex-1 overflow-y-auto px-4 lg:px-8 py-8"
|
||||||
hx-post="{{ path('app_groups_booking_step_2_refresh') }}"
|
hx-post="{{ path('app_groups_booking_step_2_refresh') }}"
|
||||||
hx-trigger="change delay:300ms"
|
hx-trigger="change delay:300ms"
|
||||||
hx-swap="none">
|
hx-target="#booking-form"
|
||||||
|
hx-swap="innerHTML">
|
||||||
{# Validation errors #}
|
{{ block('booking_form') }}
|
||||||
{% 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 %}
|
|
||||||
|
|
||||||
{# 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 %}
|
|
||||||
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 %}
|
|
||||||
class="form-radio">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -221,3 +69,163 @@
|
|||||||
{{ form_rest(form) }}
|
{{ form_rest(form) }}
|
||||||
{{ form_end(form) }}
|
{{ form_end(form) }}
|
||||||
{% endblock %}
|
{% 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 %}
|
||||||
|
|
||||||
|
{# 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 %}
|
||||||
|
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 %}
|
||||||
|
class="form-radio">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
{% include 'groups/booking/_pagination.html.twig' with { 'current_step': 3 } %}
|
{% include 'groups/booking/_pagination.html.twig' with { 'current_step': 3 } %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="participant-form" class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
|
<div id="booking-form" class="flex-1 overflow-y-auto px-4 lg:px-8 py-8">
|
||||||
<h2 class="pb-4">
|
<h2 class="pb-4">
|
||||||
Persönliche Daten
|
Persönliche Daten
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user