feat: groups price calculator admin crud, booking/offer flow and api
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
{% 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"
|
||||
{% 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) #}
|
||||
<div class="hidden lg:block shrink-0">
|
||||
{% include 'groups/booking/_pagination.html.twig' with { 'current_step': 2 } %}
|
||||
</div>
|
||||
|
||||
<div id="participant-form"
|
||||
class="flex-1 overflow-y-auto px-4 lg:px-8 py-8"
|
||||
hx-post="{{ path('app_groups_booking_step_2_refresh') }}"
|
||||
hx-trigger="change delay:300ms"
|
||||
hx-swap="none">
|
||||
|
||||
{# 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">
|
||||
<tr>
|
||||
<td class="border border-primary-bg p-2 align-top">
|
||||
<label for="board_service_none" class="cursor-pointer">Selbstversorgung</label>
|
||||
</td>
|
||||
<td class="border border-primary-bg p-2 align-top w-32 text-right"></td>
|
||||
<td class="border border-primary-bg p-2 align-top w-12 text-center">
|
||||
<input type="radio"
|
||||
id="board_service_none"
|
||||
name="{{ form.selectedBoardServiceId.vars.full_name }}"
|
||||
value=""
|
||||
{% if dto.selectedBoardServiceId is null %}checked{% endif %}
|
||||
class="form-radio">
|
||||
</td>
|
||||
</tr>
|
||||
{% 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>{{ (service.price / 100)|format_currency(ctx.accommodation.currency) }}</div>
|
||||
<div class="text-xs text-gray-500">pro Person und Nacht</div>
|
||||
</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>{{ (service.price / 100)|format_currency(ctx.accommodation.currency) }}</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>{{ (service.price / 100)|format_currency(ctx.accommodation.currency) }}</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>
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user