feat: price calendar and price table for group houses

Ports AjaxCalendarController::priceCalendarAction and priceTableAction
(MyE&P price config and price table), used on the /gruppe/gruppenhaus-*
pages and example trips. Page cache tags and "do not cache" on API
errors go through the frontend.cache.collector and
frontend.cache.instruction request attributes instead of TSFE.

Left out: priceConfig (no caller) and range (daytrip calendar via
bpn-connect, out of scope), so bpn-connect needs no credentials.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
frommeandClaude Opus 5.5 committed 2026-10-21 15:11:12 +02:00
1 parent 294d270bc1
commit c1b4979583
5 files changed
+446 -3

No files matched your search

@@ -0,0 +1,76 @@
<html data-namespace-typo3-fluid="true" lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
<f:if condition="{error}">
<f:then>
<div class="p-4 text-sm text-red-600">
Der Kalender konnte aktuell nicht geladen werden.
</div>
<div class="hidden" data-price-calendar-error-detail>{errorMessage}</div>
</f:then>
<f:else>
<div class="flex items-center justify-between mb-4">
<button type="button" class="px-3 py-1 text-sm bg-zinc-100 hover:bg-zinc-200 rounded transition-colors disabled:opacity-40 disabled:cursor-default disabled:pointer-events-none"
data-price-calendar-target="prevButton"
data-action="click->price-calendar#prevMonths"
disabled>&#8592; früher</button>
<button type="button" class="px-3 py-1 text-sm bg-zinc-100 hover:bg-zinc-200 rounded transition-colors disabled:opacity-40 disabled:cursor-default disabled:pointer-events-none"
data-price-calendar-target="nextButton"
data-action="click->price-calendar#nextMonths">später &#8594;</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<f:for each="{calendarMonths}" as="month" iteration="i">
<div data-month-index="{i.index}" data-price-calendar-target="month">
<div class="font-semibold text-center py-2 text-zinc-700">
<f:format.date format="%B %Y">{month.date}</f:format.date>
</div>
<table class="w-full border-collapse">
<thead>
<tr>
<th class="py-1 text-center text-xs text-zinc-500 font-normal">Mo</th>
<th class="py-1 text-center text-xs text-zinc-500 font-normal">Di</th>
<th class="py-1 text-center text-xs text-zinc-500 font-normal">Mi</th>
<th class="py-1 text-center text-xs text-zinc-500 font-normal">Do</th>
<th class="py-1 text-center text-xs text-zinc-500 font-normal">Fr</th>
<th class="py-1 text-center text-xs text-zinc-500 font-normal">Sa</th>
<th class="py-1 text-center text-xs text-zinc-500 font-normal">So</th>
</tr>
</thead>
<tbody>
<f:for each="{month.weeks}" as="week">
<tr>
<f:for each="{week}" as="day">
<td class="p-px">
<f:if condition="{day.inMonth}">
<f:then>
<f:variable name="dateKey" value="{f:format.date(format: 'Y-m-d', date: day.date)}"/>
<f:variable name="dayData" value="{priceDataByDate.{dateKey}}"/>
<button type="button"
class="pc-day"
data-price-calendar-target="day"
data-action="click->price-calendar#selectDate"
data-date="{dateKey}"
data-status="{dayData.status}"
data-min-nights="{dayData.minNights}"
<f:if condition="{dayData.tooltip}">aria-label="{dayData.tooltip}" data-cooltipz-dir="top"</f:if>>
{f:format.date(format: 'j', date: day.date)}
</button>
</f:then>
<f:else>
<div class="pc-day">&nbsp;</div>
</f:else>
</f:if>
</td>
</f:for>
</tr>
</f:for>
</tbody>
</table>
</div>
</f:for>
</div>
</f:else>
</f:if>
</html>
@@ -0,0 +1,79 @@
<html data-namespace-typo3-fluid="true" lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
<f:if condition="{priceTable.success} && {priceTable.data -> f:count()}">
<div class="w-full overflow-x-auto mb-4">
<h3 class="font-bold mt-4 mb-2">
Preise {year}
</h3>
<table class="w-full mb-4 bg-zinc-200">
<thead>
<tr>
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-left text-sm md:text-base" scope="col">
von
</th>
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-left text-sm md:text-base" scope="col">
bis
</th>
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-left text-sm md:text-base" scope="col">
Saison
</th>
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-right text-sm md:text-base" scope="col">
Pax bis
</th>
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-right text-sm md:text-base" scope="col">
Mindestmiete pro Nacht
</th>
<th class="px-2 py-1 md:p-2 text-right text-sm md:text-base" scope="col">
weitere Personen pro Nacht
</th>
</tr>
</thead>
<tbody>
<f:for each="{priceTable.data}" as="row">
<tr class="odd:bg-zinc-50 hover:bg-zinc-100 odd:hover:bg-zinc-100">
<td class="border-r border-zinc-200 px-2 py-1 md:p-2 text-sm md:text-base">
{row.dateFrom -> f:format.date(format: 'd.m.Y')}
</td>
<td class="border-r border-zinc-200 px-2 py-1 md:p-2 text-sm md:text-base">
{row.dateTo -> f:format.date(format: 'd.m.Y')}
</td>
<td class="border-r border-zinc-200 px-2 py-1 md:p-2 text-sm md:text-base">
<f:switch expression="{row.season}">
<f:case value="adv_secondary">VNS</f:case>
<f:case value="secondary">NS</f:case>
<f:case value="peak">HS</f:case>
<f:defaultCase>{row.season}</f:defaultCase>
</f:switch>
</td>
<td class="border-r border-zinc-200 px-2 py-1 md:p-2 text-right text-sm md:text-base">
{row.includedPax}
</td>
<td class="border-r border-zinc-200 px-2 py-1 md:p-2 text-right text-sm md:text-base">
<f:if condition="{row.type} == 'discount'">
<f:then>
<f:if condition="{row.defaultPricePerNight}">
<span class="line-through text-zinc-400 mr-1">{row.defaultPricePerNight -> f:format.currency(currencySign: row.currency)}</span>
</f:if>
{row.pricePerNight -> f:format.currency(currencySign: row.currency)}
</f:then>
<f:else>
{row.pricePerNight -> f:format.currency(currencySign: row.currency)}
</f:else>
</f:if>
</td>
<td class="px-2 py-1 md:p-2 text-right text-sm md:text-base">
{row.priceAdditionalPerson -> f:format.currency(currencySign: row.currency)}
</td>
</tr>
</f:for>
</tbody>
</table>
<div>
VNS = Vorteils-Nebensaison, NS = Nebensaison, HS = Hauptsaison
</div>
</div>
</f:if>
</html>