feat: groups bookings calendar
This commit is contained in:
+185
@@ -0,0 +1,185 @@
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
static targets = ['mount', 'error', 'message', 'button', 'resetButton', 'day', 'month', 'prevButton', 'nextButton']
|
||||
|
||||
static values = {
|
||||
hotelCode: String,
|
||||
groupsBookingUrl: String,
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.startDate = null
|
||||
this.endDate = null
|
||||
this.currentIndex = 0
|
||||
|
||||
// mountTarget carries hx-get, so HTMX fires its events there; listening here
|
||||
// scopes to this calendar instance and avoids catching events from other HTMX elements.
|
||||
this.mountTarget.addEventListener('htmx:afterSwap', () => {
|
||||
this.showMonths(0)
|
||||
this.showButton()
|
||||
})
|
||||
this.mountTarget.addEventListener('htmx:responseError', () => this.showError())
|
||||
}
|
||||
|
||||
showMonths(startIndex) {
|
||||
this.currentIndex = startIndex
|
||||
this.monthTargets.forEach((m, i) => {
|
||||
m.classList.toggle('hidden', i < startIndex || i >= startIndex + 2)
|
||||
})
|
||||
if (this.hasPrevButtonTarget) this.prevButtonTarget.disabled = startIndex === 0
|
||||
if (this.hasNextButtonTarget) this.nextButtonTarget.disabled = startIndex >= this.monthTargets.length - 2
|
||||
}
|
||||
|
||||
prevMonths() {
|
||||
this.showMonths(Math.max(0, this.currentIndex - 1))
|
||||
}
|
||||
|
||||
nextMonths() {
|
||||
this.showMonths(Math.min(this.currentIndex + 1, this.monthTargets.length - 2))
|
||||
}
|
||||
|
||||
selectDate(event) {
|
||||
const btn = event.currentTarget
|
||||
// cooltipz shows on :focus as well as :hover; blurring dismisses it after a click.
|
||||
btn.blur()
|
||||
const date = btn.dataset.date
|
||||
const status = btn.dataset.status
|
||||
|
||||
// All day buttons carry data-action for template simplicity; non-selectable ones are filtered here.
|
||||
if (!status || status === 'blocked') return
|
||||
|
||||
if (this.startDate && this.endDate) {
|
||||
this.resetSelection()
|
||||
}
|
||||
|
||||
if (!this.startDate) {
|
||||
// checkout-only days allow departure but not arrival
|
||||
if (status === 'checkout-only') {
|
||||
this.showMessage('Bitte wähle einen Anreisetag.')
|
||||
return
|
||||
}
|
||||
this.startDate = date
|
||||
this.endDate = null
|
||||
this.updateDisplay()
|
||||
this.showMessage('Bitte wähle das Abreisedatum.')
|
||||
this.disableButton()
|
||||
return
|
||||
}
|
||||
|
||||
if (date === this.startDate) {
|
||||
this.resetSelection()
|
||||
return
|
||||
}
|
||||
|
||||
// Normalize so from < to regardless of which date the user clicked first.
|
||||
const [from, to] = date < this.startDate ? [date, this.startDate] : [this.startDate, date]
|
||||
|
||||
if (this.hasBlockedInRange(from, to)) {
|
||||
this.resetSelection()
|
||||
this.showMessage('Dieser Zeitraum enthält nicht verfügbare Daten.')
|
||||
return
|
||||
}
|
||||
|
||||
const startBtn = this.dayTargets.find(b => b.dataset.date === from)
|
||||
const minNights = parseInt(startBtn?.dataset.minNights) || 0
|
||||
const nights = (new Date(to) - new Date(from)) / 86400000 // ms → days
|
||||
|
||||
// Short-stay warning is advisory only; the booking link is still enabled below.
|
||||
if (minNights > 0 && nights < minNights) {
|
||||
this.showMessage(`Weniger als ${minNights} Nächte nur auf Anfrage.`)
|
||||
} else {
|
||||
this.hideMessage()
|
||||
}
|
||||
|
||||
this.startDate = from
|
||||
this.endDate = to
|
||||
this.updateDisplay()
|
||||
this.enableButton(from, to)
|
||||
this.dispatch('range-select', { detail: { dateFrom: from, dateTo: to } })
|
||||
}
|
||||
|
||||
hasBlockedInRange(from, to) {
|
||||
for (const btn of this.dayTargets) {
|
||||
const d = btn.dataset.date
|
||||
// End date is exclusive: guests may check out of a blocked day, so it is not a hard blocker.
|
||||
if (d >= from && d < to && btn.dataset.status === 'blocked') return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
updateDisplay() {
|
||||
for (const btn of this.dayTargets) {
|
||||
const d = btn.dataset.date
|
||||
btn.classList.remove('pc-day--pending-start', 'pc-day--selected-start', 'pc-day--selected-end', 'pc-day--in-range')
|
||||
if (!this.startDate) continue
|
||||
if (d === this.startDate) {
|
||||
btn.classList.add(this.endDate ? 'pc-day--selected-start' : 'pc-day--pending-start')
|
||||
} else if (this.endDate) {
|
||||
if (d === this.endDate) btn.classList.add('pc-day--selected-end')
|
||||
else if (d > this.startDate && d < this.endDate) btn.classList.add('pc-day--in-range')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetSelection() {
|
||||
this.startDate = null
|
||||
this.endDate = null
|
||||
this.updateDisplay()
|
||||
this.hideMessage()
|
||||
this.disableButton()
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.resetSelection()
|
||||
}
|
||||
|
||||
showButton() {
|
||||
if (this.hasButtonTarget) this.buttonTarget.classList.remove('hidden')
|
||||
}
|
||||
|
||||
enableButton(dateFrom, dateTo) {
|
||||
if (!this.hasButtonTarget) return
|
||||
const params = new URLSearchParams({
|
||||
hotel_code: this.hotelCodeValue,
|
||||
date_from: dateFrom,
|
||||
date_to: dateTo,
|
||||
})
|
||||
this.buttonTarget.href = `${this.groupsBookingUrlValue}?${params}`
|
||||
this.buttonTarget.textContent = `${this.formatDate(dateFrom)}–${this.formatDate(dateTo)} Anfragen / Buchen`
|
||||
this.buttonTarget.classList.remove('opacity-50', 'cursor-not-allowed', 'pointer-events-none')
|
||||
if (this.hasResetButtonTarget) this.resetButtonTarget.classList.remove('hidden')
|
||||
}
|
||||
|
||||
disableButton() {
|
||||
if (!this.hasButtonTarget) return
|
||||
this.buttonTarget.removeAttribute('href')
|
||||
this.buttonTarget.textContent = 'Anfragen / Buchen'
|
||||
this.buttonTarget.classList.add('opacity-50', 'cursor-not-allowed', 'pointer-events-none')
|
||||
if (this.hasResetButtonTarget) this.resetButtonTarget.classList.add('hidden')
|
||||
}
|
||||
|
||||
formatDate(iso) {
|
||||
const [y, m, d] = iso.split('-')
|
||||
return `${d}.${m}.${y.slice(2)}`
|
||||
}
|
||||
|
||||
showMessage(text) {
|
||||
if (this.hasMessageTarget) {
|
||||
this.messageTarget.textContent = text
|
||||
}
|
||||
}
|
||||
|
||||
hideMessage() {
|
||||
if (this.hasMessageTarget) {
|
||||
this.messageTarget.textContent = ''
|
||||
}
|
||||
}
|
||||
|
||||
showError() {
|
||||
if (this.hasErrorTarget) {
|
||||
this.errorTarget.classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-6
@@ -1,5 +1,4 @@
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
import { useDispatch } from 'stimulus-use'
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
@@ -9,10 +8,6 @@ export default class extends Controller {
|
||||
|
||||
static values = { activeTab: String }
|
||||
|
||||
initialize() {
|
||||
useDispatch(this)
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.activeTabValue = this.tabTargets[0].dataset.tab
|
||||
}
|
||||
@@ -20,7 +15,8 @@ export default class extends Controller {
|
||||
select(e) {
|
||||
let selectedTab = e.currentTarget.dataset.tab
|
||||
this.activeTabValue = selectedTab
|
||||
this.dispatch('selected', selectedTab)
|
||||
this.dispatch('selected', { detail: selectedTab })
|
||||
this.dispatch('tab-' + selectedTab)
|
||||
}
|
||||
|
||||
activeTabValueChanged() {
|
||||
|
||||
@@ -44,6 +44,47 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Price calendar (hand-rolled)
|
||||
.pc-loading-indicator {
|
||||
display: none;
|
||||
|
||||
&.htmx-request {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.pc-day {
|
||||
@apply relative w-full text-center py-1 px-0.5 text-sm leading-tight rounded text-white cursor-pointer;
|
||||
|
||||
// Status colors — [data-status] attribute adds specificity (0,2,0)
|
||||
&[data-status="ok"] {
|
||||
@apply bg-emerald-500;
|
||||
}
|
||||
&[data-status="blocked-to-ok"] {
|
||||
background: linear-gradient(to bottom right, theme('colors.gray.400') 50%, theme('colors.emerald.500') 50%);
|
||||
}
|
||||
&[data-status="checkout-only"] {
|
||||
background: linear-gradient(to bottom right, theme('colors.emerald.500') 50%, theme('colors.gray.400') 50%);
|
||||
}
|
||||
&[data-status="blocked"] {
|
||||
@apply bg-gray-400 cursor-default;
|
||||
}
|
||||
&[data-status=""], &:not([data-status]) {
|
||||
@apply text-gray-400 cursor-default;
|
||||
}
|
||||
|
||||
// Compound selector gives (0,2,0) specificity, matching [data-status] rules; source order wins.
|
||||
&.pc-day--pending-start,
|
||||
&.pc-day--selected-start,
|
||||
&.pc-day--selected-end {
|
||||
@apply bg-none outline-none bg-ep-secondary text-gray-700;
|
||||
}
|
||||
|
||||
&.pc-day--in-range {
|
||||
@apply bg-none bg-ep-secondary text-gray-700 opacity-55;
|
||||
}
|
||||
}
|
||||
|
||||
// Maps
|
||||
@import "~leaflet/dist/leaflet.css";
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<html data-namespace-typo3-fluid="true" lang="en"
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers"
|
||||
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers">
|
||||
|
||||
<div data-controller="price-calendar"
|
||||
data-price-calendar-hotel-code-value="{product.calendarHotel.code}"
|
||||
data-price-calendar-groups-booking-url-value="{settings.groupsBookingUrl}"
|
||||
class="relative p-4 bg-zinc-50">
|
||||
<div class="pc-loading-indicator items-center justify-center space-x-4 text-sm p-4"
|
||||
id="pc-loading-{product.calendarHotel.uid}">
|
||||
<f:image class="block w-4 h-4" src="EXT:ep_theme/Resources/Public/images/ajax-loader-white.gif" alt=""/>
|
||||
<span>Preiskalender wird geladen …</span>
|
||||
</div>
|
||||
<div data-price-calendar-target="mount"
|
||||
hx-get="{ep:uri.ajax(action: 'priceCalendar', controller: 'AjaxCalendar', extensionName: 'epproducts', pageUid: settings.defaultAjaxUid, arguments: '{hotel: product.calendarHotel}', format: 'html')}"
|
||||
hx-trigger="load"
|
||||
hx-indicator="#pc-loading-{product.calendarHotel.uid}">
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="h-8 text-sm" data-price-calendar-target="message"></div>
|
||||
<div class="h-8 text-sm text-right">
|
||||
<button type="button"
|
||||
class="hidden underline"
|
||||
data-price-calendar-target="resetButton"
|
||||
data-action="click->price-calendar#reset">
|
||||
Auswahl zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<a class="hidden button bg-button w-full mt-2 opacity-50 cursor-not-allowed pointer-events-none"
|
||||
data-price-calendar-target="button"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Anfragen / Buchen
|
||||
</a>
|
||||
<div class="hidden p-4 text-sm text-red-600" data-price-calendar-target="error">
|
||||
Der Kalender konnte aktuell nicht geladen werden.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</html>
|
||||
+76
@@ -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>← Vorherige</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">Nächste →</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"> </div>
|
||||
</f:else>
|
||||
</f:if>
|
||||
</td>
|
||||
</f:for>
|
||||
</tr>
|
||||
</f:for>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</f:for>
|
||||
</div>
|
||||
</f:else>
|
||||
</f:if>
|
||||
|
||||
</html>
|
||||
+1
@@ -0,0 +1 @@
|
||||
{priceConfig -> f:format.json() ->f:format.raw()}
|
||||
+79
@@ -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-scroll 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>
|
||||
@@ -37,40 +37,20 @@
|
||||
<div id="main" class="w-full lg:w-2/3">
|
||||
<div class="lg:pr-4 flex flex-col space-y-8">
|
||||
<div class="grid grid-cols-3 md:grid-cols-6 gap-px uppercase text-white text-sm md:text-base">
|
||||
<f:if condition="{settings.hotelOnTop}">
|
||||
<f:then>
|
||||
<button type="button"
|
||||
data-action="tabs#select"
|
||||
data-tabs-target="button"
|
||||
data-tab="hotel"
|
||||
class="py-2 uppercase bg-button bg-button--dark">
|
||||
Unterkunft
|
||||
</button>
|
||||
<button type="button"
|
||||
data-action="tabs#select"
|
||||
data-tabs-target="button"
|
||||
data-tab="dates"
|
||||
class="col-span-2 py-2 uppercase bg-button bg-button--dark">
|
||||
Termine/Preise
|
||||
</button>
|
||||
</f:then>
|
||||
<f:else>
|
||||
<button type="button"
|
||||
data-action="tabs#select"
|
||||
data-tabs-target="button"
|
||||
data-tab="dates"
|
||||
class="col-span-2 py-2 uppercase bg-button bg-button--dark">
|
||||
Termine/Preise
|
||||
</button>
|
||||
<button type="button"
|
||||
data-action="tabs#select"
|
||||
data-tabs-target="button"
|
||||
data-tab="hotel"
|
||||
class="py-2 uppercase bg-button bg-button--dark">
|
||||
Unterkunft
|
||||
</button>
|
||||
</f:else>
|
||||
</f:if>
|
||||
<button type="button"
|
||||
data-action="tabs#select"
|
||||
data-tabs-target="button"
|
||||
data-tab="dates"
|
||||
class="col-span-2 py-2 uppercase bg-button bg-button--dark">
|
||||
Termine/Preise
|
||||
</button>
|
||||
<button type="button"
|
||||
data-action="tabs#select"
|
||||
data-tabs-target="button"
|
||||
data-tab="hotel"
|
||||
class="py-2 uppercase bg-button bg-button--dark">
|
||||
Unterkunft
|
||||
</button>
|
||||
<button type="button"
|
||||
data-action="tabs#select"
|
||||
data-tabs-target="button"
|
||||
@@ -95,16 +75,8 @@
|
||||
</button>
|
||||
</f:if>
|
||||
</div>
|
||||
<f:if condition="{settings.hotelOnTop}">
|
||||
<f:then>
|
||||
<f:render section="Hotel" arguments="{_all}"/>
|
||||
<f:render section="Dates" arguments="{_all}"/>
|
||||
</f:then>
|
||||
<f:else>
|
||||
<f:render section="Dates" arguments="{_all}"/>
|
||||
<f:render section="Hotel" arguments="{_all}"/>
|
||||
</f:else>
|
||||
</f:if>
|
||||
<f:render section="Dates" arguments="{_all}"/>
|
||||
<f:render section="Hotel" arguments="{_all}"/>
|
||||
<f:render section="City" arguments="{_all}"/>
|
||||
<f:render section="Region" arguments="{_all}"/>
|
||||
<f:render section="Journey" arguments="{_all}"/>
|
||||
@@ -123,14 +95,32 @@
|
||||
</f:section>
|
||||
|
||||
<f:section name="Dates">
|
||||
<div class="mb-4{f:if(condition: settings.hotelOnTop, then: ' hidden')}"
|
||||
<div class="mb-4"
|
||||
data-tabs-target="tab"
|
||||
data-tab="dates">
|
||||
|
||||
<f:if condition="{hotel.groupsPriceConfigs} && {nonBookableInfo}">
|
||||
<div class="alert alert-info mb-4" data-rte-content>
|
||||
{nonBookableInfo.text -> f:format.html()}
|
||||
</div>
|
||||
</f:if>
|
||||
|
||||
<f:if condition="{product.calendarHotel}">
|
||||
<f:render partial="Product/PriceCalendar.html" arguments="{_all}"/>
|
||||
|
||||
<f:variable name="currentYear" value="{f:format.date(date: 'now', format: 'Y')}"/>
|
||||
<div hx-get="{ep:uri.ajax(action: 'priceTable', controller: 'AjaxCalendar', extensionName: 'epproducts', pageUid: settings.defaultAjaxUid, arguments: '{hotel: product.calendarHotel, year: currentYear}', format: 'html')}"
|
||||
hx-trigger="load">
|
||||
<f:render section="TableLoadingIndicator"/>
|
||||
</div>
|
||||
|
||||
<f:variable name="nextYear" value="{f:format.date(date: '+1 year', format: 'Y')}"/>
|
||||
<div hx-get="{ep:uri.ajax(action: 'priceTable', controller: 'AjaxCalendar', extensionName: 'epproducts', pageUid: settings.defaultAjaxUid, arguments: '{hotel: product.calendarHotel, year: nextYear}', format: 'html')}"
|
||||
hx-trigger="load">
|
||||
<f:render section="TableLoadingIndicator"/>
|
||||
</div>
|
||||
</f:if>
|
||||
|
||||
<f:if condition="{isProductWithExcludedConcept}">
|
||||
<f:if condition="{product.region.season} == 'w'">
|
||||
<div class="bg-ep-primary-light text-white p-4">
|
||||
@@ -146,6 +136,7 @@
|
||||
</div>
|
||||
</f:if>
|
||||
</f:if>
|
||||
|
||||
<f:if condition="{product.daytrip}">
|
||||
<f:then>
|
||||
<f:render section="DaytripDateSelect" arguments="{_all}"/>
|
||||
@@ -164,17 +155,19 @@
|
||||
<div class="hidden" data-product-details-target="priceTableContainer"></div>
|
||||
</f:else>
|
||||
</f:if>
|
||||
|
||||
<div class="lg:hidden">
|
||||
<f:render section="SidebarGallery" arguments="{_all}"/>
|
||||
<f:render section="SidebarButtons" arguments="{_all}"/>
|
||||
</div>
|
||||
|
||||
<f:render partial="Product/Details" arguments="{_all}"/>
|
||||
</div>
|
||||
</f:section>
|
||||
|
||||
<f:section name="Hotel">
|
||||
|
||||
<div class="mb-4{f:if(condition: settings.hotelOnTop, else: ' hidden')}"
|
||||
<div class="mb-4 hidden"
|
||||
data-tabs-target="tab"
|
||||
data-tab="hotel">
|
||||
<f:render partial="Hotel/Details" arguments="{hotel: hotel, bars: 1, showNonBookableHint: 0, currentPageUid: currentPageUid, showInquiryForm: isProductWithGroupInquiryForm}"/>
|
||||
@@ -209,7 +202,6 @@
|
||||
|
||||
<f:section name="Sidebar">
|
||||
<f:render section="SidebarGallery" arguments="{_all}"/>
|
||||
<f:render section="Calendar" arguments="{_all}"/>
|
||||
<f:if condition="{product.tourLink}">
|
||||
<div class="bg-zinc-50 p-4 mb-4"
|
||||
data-controller="lightbox">
|
||||
@@ -468,4 +460,10 @@
|
||||
</f:if>
|
||||
</f:section>
|
||||
|
||||
<f:section name="TableLoadingIndicator">
|
||||
<div class="flex items-center justify-center space-x-4 text-sm p-4">
|
||||
<f:image class="block w-4 h-4" src="EXT:ep_theme/Resources/Public/images/ajax-loader-white.gif" alt=""/>
|
||||
<span>Preistabelle wird geladen …</span>
|
||||
</div>
|
||||
</f:section>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user