diff --git a/api.http b/api.http index 624ef1d5..6b225a1d 100644 --- a/api.http +++ b/api.http @@ -6,7 +6,7 @@ GET https://ep-reisen.ddev.site/api/product ### GET hotel data for myep GET https://ep-reisen.ddev.site/api/hotel - ?hotel=SSTGR + ?hotel=DPWGRP &key=abcabc123 ### diff --git a/package-lock.json b/package-lock.json index 10350cf0..5952d178 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9197,22 +9197,6 @@ "node": ">=8.10.0" } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "extraneous": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/tapable": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", diff --git a/public/typo3conf/ext/ep_products/Classes/Controller/AjaxCalendarController.php b/public/typo3conf/ext/ep_products/Classes/Controller/AjaxCalendarController.php index fb60965b..3513d290 100644 --- a/public/typo3conf/ext/ep_products/Classes/Controller/AjaxCalendarController.php +++ b/public/typo3conf/ext/ep_products/Classes/Controller/AjaxCalendarController.php @@ -27,8 +27,10 @@ namespace EP\EpProducts\Controller; * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ -use EP\EpProducts\BpnConnect\ApiClient; +use EP\EpProducts\BpnConnect\ApiClient as BpnConnectClient; +use EP\EpProducts\MyEP\ApiClient as MyEpClient; use EP\EpProducts\Domain\Model\Hotel; +use EP\EpProducts\MyEP\ApiException; use EP\EpProducts\Utility\DateUtility; use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; @@ -37,26 +39,147 @@ class AjaxCalendarController extends ActionController { private const CACHE_TTL = 3600; - /** - * @var ApiClient - */ - protected $apiClient; + protected BpnConnectClient $bpnConnectClient; + protected MyEpClient $myEpClient; + protected FrontendInterface $cache; - /** - * @var FrontendInterface - */ - protected $cache; - - /** - * @param ApiClient $apiClient - * @param FrontendInterface $cache - */ - public function __construct(ApiClient $apiClient, FrontendInterface $cache) + public function __construct(BpnConnectClient $bpnConnectClient, MyEpClient $myEpClient, FrontendInterface $cache) { - $this->apiClient = $apiClient; + $this->bpnConnectClient = $bpnConnectClient; + $this->myEpClient = $myEpClient; $this->cache = $cache; } + public function priceCalendarAction(Hotel $hotel, int $months = 12): void + { + $hotelCode = trim($hotel->getCode() ?? ''); + $dateRange = DateUtility::getDateRange(null, null, $months); + $today = new \DateTimeImmutable('today'); + + $priceDataByDate = []; + $error = false; + + if ($hotelCode !== '') { + try { + $cacheIdentifier = 'myep_price_calendar_' . sha1(implode('|', [ + $hotelCode, + $dateRange['period']->getStartDate()->format('Y-m-d'), + $dateRange['period']->getEndDate()->format('Y-m-d'), + ])); + $cachedData = $this->cache->get($cacheIdentifier); + + if (false !== $cachedData) { + $priceDataByDate = $cachedData; + } else { + $rawData = $this->myEpClient->getPriceConfigData( + $hotelCode, + $dateRange['period']->getStartDate(), + $dateRange['period']->getEndDate() + ); + + $availableDates = []; + foreach ($rawData as $item) { + if (($item['status'] ?? '') === 'OK') { + $availableDates[$item['date']] = true; + } + } + + foreach ($rawData as $item) { + $date = $item['date']; + $dt = \DateTimeImmutable::createFromFormat('Y-m-d', $date); + if ($dt < $today) { + continue; + } + + $prevDate = $dt->modify('-1 day')->format('Y-m-d'); + $isAvailable = ($item['status'] ?? '') === 'OK'; + $prevAvailable = isset($availableDates[$prevDate]); + + if ($isAvailable) { + $dayStatus = $prevAvailable ? 'ok' : 'blocked-to-ok'; + } elseif ($prevAvailable) { + $dayStatus = 'checkout-only'; + } else { + $dayStatus = 'blocked'; + } + + $priceDataByDate[$date] = [ + 'status' => $dayStatus, + 'available' => $isAvailable ? 1 : 0, + 'tooltip' => $this->buildTooltip($dayStatus, $item), + 'pricePerNight' => $item['pricePerNight'] ?? null, + 'defaultPricePerNight' => $item['defaultPricePerNight'] ?? null, + 'type' => $item['type'] ?? null, + 'includedPax' => $item['includedPax'] ?? null, + 'minNights' => (int)($item['minNights'] ?? 0), + ]; + } + + $this->cache->set($cacheIdentifier, $priceDataByDate, [], self::CACHE_TTL); + } + } catch (ApiException $e) { + $error = true; + } + } else { + $error = true; + } + + $this->view->assignMultiple([ + 'priceDataByDate' => $priceDataByDate, + 'calendarMonths' => $this->buildCalendarMonths($dateRange['period']), + 'error' => $error, + ]); + } + + public function priceConfigAction(Hotel $hotel, string $year = null, string $month = null, int $months = 12) + { + $dateRange = DateUtility::getDateRange($year, $month, $months); + + $apiDateFrom = $dateRange['period']->getStartDate(); + $apiDateTo = $dateRange['period']->getEndDate(); + $hotelCode = trim($hotel->getCode() ?? ''); + + $priceConfig = [ + 'data' => [], + 'success' => true, + 'error' => null, + ]; + + try { + $priceConfig['data'] = $this->myEpClient->getPriceConfigData($hotelCode, $apiDateFrom, $apiDateTo); + } catch (ApiException $e) { + $priceConfig['success'] = false; + $priceConfig['error'] = $e->getMessage(); + } + + $this->view->assign('priceConfig', $priceConfig); + } + + public function priceTableAction(Hotel $hotel, string $year = null): void + { + $year = $year ?? (new \DateTimeImmutable())->format('Y'); + $hotelCode = trim($hotel->getCode() ?? ''); + + $priceTable = [ + 'data' => [], + 'success' => true, + 'error' => null, + ]; + + try { + $priceTable['data'] = $this->myEpClient->getPriceTableData($hotelCode, $year); + } catch (ApiException $e) { + $priceTable['success'] = false; + $priceTable['error'] = $e->getMessage(); + } + + $this->view->assignMultiple([ + 'hotel' => $hotel, + 'year' => $year, + 'priceTable' => $priceTable, + ]); + } + /** * @param Hotel $hotel * @param string $year @@ -91,7 +214,7 @@ class AjaxCalendarController extends ActionController if (false !== $cachedCalendar) { $calendar = $cachedCalendar; } else { - $calendar = $this->apiClient->getCalendar( + $calendar = $this->bpnConnectClient->getCalendar( $hotelCode, new \DateTimeImmutable($apiDateFrom), new \DateTimeImmutable($apiDateTo), @@ -112,6 +235,36 @@ class AjaxCalendarController extends ActionController ]); } + private function buildTooltip(string $status, array $item): string + { + if ($status === 'blocked') { + return 'Nicht verfügbar'; + } + if ($status === 'checkout-only') { + return 'Nur Abreise'; + } + return $this->buildPriceTooltip($item); + } + + private function buildPriceTooltip(array $item): string + { + $price = $item['pricePerNight'] ?? null; + + if ($price !== null) { + $priceStr = (($item['type'] ?? null) === 'discount') + ? $price . ' € (Angebot)' + : 'ab ' . $price . ' €'; + } else { + $priceStr = 'Anfrage'; + } + + $minNights = (int)($item['minNights'] ?? 0); + + return $minNights > 1 + ? $priceStr . ' · min. ' . $minNights . ' Nächte' + : $priceStr; + } + /** * @return array> */ diff --git a/public/typo3conf/ext/ep_products/Classes/MyEP/ApiClient.php b/public/typo3conf/ext/ep_products/Classes/MyEP/ApiClient.php index 6a10d1ed..3c3152de 100644 --- a/public/typo3conf/ext/ep_products/Classes/MyEP/ApiClient.php +++ b/public/typo3conf/ext/ep_products/Classes/MyEP/ApiClient.php @@ -111,6 +111,33 @@ class ApiClient implements LoggerAwareInterface return $this->requestJson('GET', 'pickups-planning/' . $travelCode); } + /** + * @throws ApiException + */ + public function getPriceConfigData(string $hotelCode, \DateTimeImmutable $dateFrom, \DateTimeImmutable $dateTo): array + { + return $this->requestJson('GET', 'contingents/calendar', [ + 'query' => [ + 'hotelCode' => $hotelCode, + 'dateFrom' => $dateFrom->format('Y-m-d'), + 'dateTo' => $dateTo->format('Y-m-d'), + ], + ]); + } + + /** + * @throws ApiException + */ + public function getPriceTableData(string $hotelCode, string $year): array + { + return $this->requestJson('GET', 'contingents/prices', [ + 'query' => [ + 'hotelCode' => $hotelCode, + 'year' => $year, + ], + ]); + } + /** * @throws ApiException */ diff --git a/public/typo3conf/ext/ep_products/Configuration/Services.yaml b/public/typo3conf/ext/ep_products/Configuration/Services.yaml index 2d886792..e16f6cfa 100644 --- a/public/typo3conf/ext/ep_products/Configuration/Services.yaml +++ b/public/typo3conf/ext/ep_products/Configuration/Services.yaml @@ -28,6 +28,10 @@ services: arguments: $cache: '@cache.ep_cache' + EP\EpProducts\Service\MyEpImageService: + arguments: + $cache: '@cache.ep_cache' + EP\EpProducts\Controller\AjaxCalendarController: arguments: $cache: '@cache.ep_cache' diff --git a/public/typo3conf/ext/ep_products/Configuration/TypoScript/constants.typoscript b/public/typo3conf/ext/ep_products/Configuration/TypoScript/constants.typoscript index f263f449..679c1286 100644 --- a/public/typo3conf/ext/ep_products/Configuration/TypoScript/constants.typoscript +++ b/public/typo3conf/ext/ep_products/Configuration/TypoScript/constants.typoscript @@ -48,6 +48,8 @@ plugin.tx_epproducts { groupsPriceToEmail = gruppen@ep-reisen.de # cat=epproducts/130/120; type=string; label=Email recipient for groups price inquiries groupsPriceToName = EP Reisen + # cat=epproducts/130/130; type=string; label=Email recipient for groups price inquiries + groupsBookingUrl = https://myep.ddev.site/groups/booking # cat=epproducts/140/100; type=string; label=Room types for pricetables (CSV) priceTableRoomTypes = 1,2,3,4,5,6,7,8,9,10 # cat=epproducts/140/110; type=string; label=Ignore following IPs in searchlog diff --git a/public/typo3conf/ext/ep_products/Configuration/TypoScript/setup.typoscript b/public/typo3conf/ext/ep_products/Configuration/TypoScript/setup.typoscript index 0c65137a..a7002221 100644 --- a/public/typo3conf/ext/ep_products/Configuration/TypoScript/setup.typoscript +++ b/public/typo3conf/ext/ep_products/Configuration/TypoScript/setup.typoscript @@ -54,6 +54,7 @@ plugin.tx_epproducts { groupsPriceToEmail = {$plugin.tx_epproducts.settings.groupsPriceToEmail} groupsPriceToName = {$plugin.tx_epproducts.settings.groupsPriceToName} groupInquiryFormConceptUids = {$plugin.tx_epproducts.settings.groupInquiryFormConceptUids} + groupsBookingUrl = {$plugin.tx_epproducts.settings.groupsBookingUrl} logoImage = {$plugin.tx_eptheme.settings.logoImage} themekey = {$plugin.tx_eptheme.settings.themekey} globalConceptCode = {$plugin.tx_eptheme.settings.globalConceptCode} @@ -173,6 +174,9 @@ tx_epproducts_ajax_json { } AjaxCalendar { 1 = range + 2 = priceConfig + 3 = priceTable + 4 = priceCalendar } AjaxContingent { 1 = list diff --git a/public/typo3conf/ext/ep_products/ext_localconf.php b/public/typo3conf/ext/ep_products/ext_localconf.php index 0687171e..d38b95f1 100644 --- a/public/typo3conf/ext/ep_products/ext_localconf.php +++ b/public/typo3conf/ext/ep_products/ext_localconf.php @@ -276,7 +276,7 @@ $boot = function () { 'AjaxDate' => 'dates', 'AjaxContingent' => 'list,rooms', 'AjaxTable' => 'datestable,pricetable,pricetableHtml,eventPricetable,daytripRooms', - 'AjaxCalendar' => 'range,contingents,availableRooms', + 'AjaxCalendar' => 'range,priceConfig,priceCalendar,priceTable', 'AjaxWatchlist' => 'list', 'AjaxGroupsPrice' => 'processForm', 'AjaxTeaserPopup' => 'concept', @@ -287,7 +287,7 @@ $boot = function () { 'AjaxDate' => 'dates', 'AjaxContingent' => 'list,rooms', 'AjaxTable' => 'datestable,pricetable,pricetableHtml,eventPricetable,daytripRooms', - 'AjaxCalendar' => 'range,contingents,availableRooms', + 'AjaxCalendar' => 'range,priceConfig,priceCalendar,priceTable', 'AjaxWatchlist' => 'list', 'AjaxGroupsPrice' => 'processForm', 'AjaxTeaserPopup' => 'concept', diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/price_calendar_controller.js b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/price_calendar_controller.js new file mode 100644 index 00000000..1252fa2c --- /dev/null +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/price_calendar_controller.js @@ -0,0 +1,178 @@ +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.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() + } + + 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') + } + } +} diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/tabs_controller.js b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/tabs_controller.js index b5ca2849..0c620f37 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/tabs_controller.js +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/tabs_controller.js @@ -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() { diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/scss/_vendor.scss b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/scss/_vendor.scss index 7ad98519..7cab209a 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/scss/_vendor.scss +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/scss/_vendor.scss @@ -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"; diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Product/PriceCalendar.html b/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Product/PriceCalendar.html new file mode 100644 index 00000000..2ba2b095 --- /dev/null +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Product/PriceCalendar.html @@ -0,0 +1,42 @@ + + +
+
+ + Preiskalender wird geladen … +
+
+
+
+
+
+ +
+
+ + Anfragen / Buchen + + +
+ + diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxCalendar/PriceCalendar.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxCalendar/PriceCalendar.html new file mode 100644 index 00000000..4fade31e --- /dev/null +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxCalendar/PriceCalendar.html @@ -0,0 +1,70 @@ + + + + +
+ + +
+
+ +
+
+ {month.date} +
+ + + + + + + + + + + + + + + + + + + + + +
MoDiMiDoFrSaSo
+ + + + + + + +
 
+
+
+
+
+
+
+
+
+ + diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxCalendar/PriceConfig.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxCalendar/PriceConfig.html new file mode 100644 index 00000000..5bd064e4 --- /dev/null +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxCalendar/PriceConfig.html @@ -0,0 +1 @@ +{priceConfig -> f:format.json() ->f:format.raw()} diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxCalendar/PriceTable.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxCalendar/PriceTable.html new file mode 100644 index 00000000..15fd510e --- /dev/null +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxCalendar/PriceTable.html @@ -0,0 +1,76 @@ + + + +
+

+ Preise {year} +

+ + + + + + + + + + + + + + + + + + + + + + + +
+ von + + bis + + Saison + + Pax bis + + Mindestmiete pro Nacht + + weitere Personen pro Nacht +
+ {row.dateFrom -> f:format.date(format: 'd.m.Y')} + + {row.dateTo -> f:format.date(format: 'd.m.Y')} + + + VNS + NS + HS + {row.season} + + + {row.includedPax} + + + + + {row.defaultPricePerNight -> f:format.currency(currencySign: row.currency)} + + {row.pricePerNight -> f:format.currency(currencySign: row.currency)} + + + {row.pricePerNight -> f:format.currency(currencySign: row.currency)} + + + + {row.priceAdditionalPerson -> f:format.currency(currencySign: row.currency)} +
+
+
+ + diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Product/Detail.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Product/Detail.html index 88c27adb..43f59f65 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Product/Detail.html +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Product/Detail.html @@ -126,11 +126,29 @@
+
{nonBookableInfo.text -> f:format.html()}
+ + + + + +
+ +
+ + +
+ +
+
+
@@ -146,6 +164,7 @@
+ @@ -164,10 +183,12 @@ +
+