Implement daily bookable dates

This commit is contained in:
Björn Fromme
2018-09-28 12:41:04 +02:00
parent 59694bb410
commit 24adcaebd2
29 changed files with 808 additions and 104 deletions
@@ -0,0 +1,195 @@
<template>
<div class="date-select-wrapper">
<div class="ep-headbar">{{ headerLabel }}</div>
<div class="row">
<div class="col-xs-12 col-md-6">
<div class="date-select">
<div class="date-select__picker">
<div class="daterange__panel">
<input ref="field" type="hidden">
</div>
</div>
</div>
</div>
<div class="col-xs-12 col-md-6">
<table class="table table-striped pricetable" v-show="availableRooms.length">
<thead>
<tr>
<th>Zimmerart</th>
<th>Preis</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="entry of availableRooms">
<td>{{entry.label}}</td>
<td>ab {{entry.price * selectedNumberOfDays}} &euro;</td>
<td>
<a :href="entry.bookingUrl" target="_blank"
class="button button--small button--action">Buchen</a>
</td>
</tr>
</tbody>
</table>
<div v-show="loading" class="loader"><p>Loading...</p></div>
<div v-show="message">{{message}}</div>
</div>
</div>
</div>
</template>
<script>
import $ from 'jquery'
import Vue from 'vue'
import flatpickr from 'flatpickr'
import { German } from 'flatpickr/dist/l10n/de'
import dayjs from 'dayjs'
import 'dayjs/locale/de'
import DatesTable from './DatesTable.vue'
export default {
components: {
DatesTable
},
data () {
return {
loading: false,
picker: null,
displayedFrom: dayjs().startOf('month'),
displayedTo: dayjs().endOf('month'),
selectedFrom: null,
selectedTo: null,
availableDates: [],
availableRooms: []
}
},
props: {
hotelName: {
type: String,
required: true
},
hotelUid: {
type: Number,
required: true
},
productUid: {
type: Number,
required: true
},
minDate: {
type: Date,
default () {
return new Date()
}
},
maxDate: {
type: Date,
default () {
return new Date(dayjs().add(1, 'year'))
}
},
contingentEndpointUri: {
type: String,
required: true
},
roomsEndpointUri: {
type: String,
required: true
},
argumentPrefix: {
type: String,
required: true
}
},
computed: {
headerLabel () {
let label = 'Buchungsoptionen';
if (this.hotelName) {
label += ' ' + this.hotelName;
}
if (this.selectedFrom && this.selectedTo) {
label += ' ' + this.selectedFrom.format('DD.MM.YY') + ' - ' + this.selectedTo.format('DD.MM.YY');
label += ' (' + this.selectedNumberOfDays + ' Nächte)';
}
return label;
},
selectedNumberOfDays () {
if (this.selectedFrom === null || this.selectedTo === null) {
return 0;
}
return this.selectedTo.diff(this.selectedFrom, 'days');
},
message () {
if (this.loading) {
return '';
}
if (this.selectedFrom === null || this.selectedTo === null) {
return 'Bitte einen Zeitraum auswählen.';
}
if (this.selectedTo.diff(this.selectedFrom, 'days') < 2) {
return 'Bitte mindestens zwei Nächte auswählen';
}
if (this.availableRooms.length === 0) {
return 'Im gewählten Zeitraum sind leider keine Zimmer verfügbar.';
}
return '';
}
},
methods: {
onDateSelected (date) {
this.selectedDate = dayjs(date);
},
fetchEnabledDates () {
let query = {};
query[this.argumentPrefix + '[hotel]'] = this.hotelUid;
query[this.argumentPrefix + '[product]'] = this.productUid;
this.loading = true;
$.post(this.contingentEndpointUri, query, (json) => {
this.loading = false;
this.availableDates = json;
const enabledDates = [];
for (let entry of this.availableDates) {
enabledDates.push(entry.date);
}
this.picker.set('enable', enabledDates);
}, 'json');
},
fetchAvailableRooms () {
let query = {};
query[this.argumentPrefix + '[hotel]'] = this.hotelUid;
query[this.argumentPrefix + '[product]'] = this.productUid;
query[this.argumentPrefix + '[dateFrom]'] = this.selectedFrom.format('YYYY-MM-DD');
query[this.argumentPrefix + '[dateTo]'] = this.selectedTo.format('YYYY-MM-DD');
this.loading = true;
$.post(this.roomsEndpointUri, query, (json) => {
this.loading = false;
this.availableRooms = json;
}, 'json');
}
},
mounted () {
this.picker = $(this.$refs.field).flatpickr({
mode: 'range',
dateFormat: 'Y-m-d',
locale: German,
inline: true,
minDate: new Date(this.minDate),
maxDate: new Date(this.maxDate),
onChange: (selectedDates) => {
this.availableRooms = [];
if (selectedDates.length === 2) {
this.selectedFrom = dayjs(selectedDates[0]);
this.selectedTo = dayjs(selectedDates[1]);
if (this.selectedTo.diff(this.selectedFrom, 'days') > 1) {
this.fetchAvailableRooms();
}
}
}
});
Vue.nextTick(() => {
this.fetchEnabledDates();
});
}
}
</script>
@@ -1,7 +1,9 @@
<template>
<div class="dates-table-wrapper">
<div v-if="bookable" class="ep-headbar">{{ headerLabel }}</div>
<div v-if="!bookable" class="ep-headbar">Termine &amp; Leistungen</div>
<template v-if="showHeader">
<div v-if="bookable" class="ep-headbar">{{ headerLabel }}</div>
<div v-if="!bookable" class="ep-headbar">Termine &amp; Leistungen</div>
</template>
<div class="dates-table__pricetoggle" v-if="hasSurcharge || hasDiscount">
<a href="#" v-if="hasSurcharge" v-on:click.prevent="toggleBus">
<template v-if="addBus">
@@ -127,6 +129,10 @@
altLabel: {
type: String,
default: null
},
showHeader: {
type: Boolean,
default: true
}
},
data () {
@@ -1,6 +1,6 @@
<template>
<div class="price-table-wrapper">
<div class="row ep-headbar">
<div class="row ep-headbar" v-if="showHeader">
<div class="col-xs-6">Buchungsoptionen</div>
<div class="col-xs-6 text-right">{{dateLabel}}</div>
</div>
@@ -130,6 +130,10 @@
singleDate: {
type: Boolean,
default: false
},
showHeader: {
typoe: Boolean,
default: true
}
},
data () {
@@ -6,6 +6,7 @@
import PriceTable from './PriceTable.vue'
import ServicesIncluded from './ServicesListIncluded.vue'
import ServicesOptional from './ServicesListOptional.vue'
import DateSelect from './DateSelect.vue'
import DaterangeSelect from './DaterangeSelect.vue'
import AjaxContent from './AjaxContent.vue'
import Calendar from './Calendar.vue'
@@ -21,6 +22,7 @@
PriceTable,
ServicesIncluded,
ServicesOptional,
DateSelect,
DaterangeSelect,
AjaxContent,
Calendar,
@@ -47,7 +49,42 @@
singleDate: false
}
},
props: [ 'filterSettings', 'argumentPrefix', 'uris', 'hotelFirst', 'fbPixelData', 'watchlist' ],
props: {
filterSettings: {
type: Object,
default () {
return {}
}
},
argumentPrefix: {
type: String,
required: true
},
uris: {
type: Object,
required: true
},
hotelFirst: {
type: Boolean,
default: false
},
fbPixelData: {
type: Object,
default () {
return {}
}
},
watchlist: {
type: Array,
default () {
return []
}
},
daytrip: {
type: Number,
default: 0
}
},
computed: {
dateLabel () {
return this.dateStart + ' - ' + this.dateEnd;
@@ -79,10 +116,36 @@
this.loading = false;
EventBus.$emit('tableLoaded');
}, 'json');
},
loadSelectableDates () {
this.loading = true;
this.datesShow = true;
this.priceTableShow = false;
this.detailShow = false;
let query = {};
query[this.argumentPrefix + '[product]'] = this.filterSettings;
$.post(this.uris['dateslist'], query, (json) => {
if (json.dates.length === 1) {
this.singleDate = true;
let dateRow = json.dates[0];
this.loadPriceTableView(dateRow.dateUid);
} else {
this.singleDate = false;
this.datesRows = json.dates;
this.bookable = json.bookable;
this.altLabel = json.altLabel;
this.altProductLink = json.altProductLink;
this.loading = false;
}
}, 'json');
},
loadDatesView (scroll = true) {
this.loadDates();
if (this.daytrip) {
this.loading = false;
this.datesShow = true;
} else {
this.loadDates();
}
this.activeSection = 'dates';
if (scroll) {
this.scrollToMain();
@@ -13,13 +13,16 @@
<product-detail
:filter-settings='<v:format.json.encode>{filterSettings}</v:format.json.encode>'
:uris='{
dates: "<f:format.raw>{ep:uri.ajax(action: 'dates', controller: 'AjaxDate', pageUid: settings.defaultAjaxUid, arguments: '{product: product, hotel: hotel}', format: 'json')}</f:format.raw>",
pricetable: "<f:format.raw>{ep:uri.ajax(action: 'pricetable', controller: 'AjaxTable', pageUid: settings.defaultAjaxUid, arguments: '{product: product, hotel: hotel}', format: 'json')}</f:format.raw>"
dates: "<f:format.raw>{ep:uri.ajax(action: 'dates', controller: 'AjaxDate', arguments: '{product: product, hotel: hotel}', format: 'json', noCacheHash: 1)}</f:format.raw>",
contingents: "<f:format.raw>{ep:uri.ajax(action: 'list', controller: 'AjaxContingent', format: 'json', noCacheHash: 1)}</f:format.raw>",
rooms: "<f:format.raw>{ep:uri.ajax(action: 'rooms', controller: 'AjaxContingent', format: 'json', noCacheHash: 1)}</f:format.raw>",
pricetable: "<f:format.raw>{ep:uri.ajax(action: 'pricetable', controller: 'AjaxTable', arguments: '{product: product, hotel: hotel}', format: 'json', noCacheHash: 1)}</f:format.raw>"
}'
argument-prefix='<ep:argumentPrefix pluginName="Ajax" />'
:hotel-first="{f:if(condition: settings.hotelOnTop, then: 'true', else: 'false')}"
:fb-pixel-data="{ productUid: <f:format.raw>{product.uid}</f:format.raw>, nameInternal: '<f:format.raw>{product.nameInternal}</f:format.raw>' }"
:watchlist="watchlist"
:daytrip="{product.daytrip -> v:variable.convert(type: 'int')}"
inline-template>
<div class="product-detail-wrapper">
<div class="row">
@@ -128,23 +131,37 @@
</div>
</f:if>
</f:if>
<template v-if="available">
<dates-table :dates-rows="datesRows"
:alt-product-link="altProductLink"
:alt-label="altLabel"
hotel-name="{hotel.name}"
:bookable="bookable"
></dates-table>
</template>
<f:if condition="{isProductWithNoInfoConcept}">
<f:if condition="{product.daytrip}">
<f:then>
<date-select
hotel-name="{hotel.name}"
:hotel-uid="{hotel.uid}"
:product-uid="{product.uid}"
:contingent-endpoint-uri="uris.contingents"
:rooms-endpoint-uri="uris.rooms"
:argument-prefix="argumentPrefix"
></date-select>
</f:then>
<f:else>
<template v-else>
<div class="alert alert-info">
Die von dir gewählte Reise ist aktuell nicht buchbar, aber wir helfen dir gerne dabei,
eine andere Reise zu finden. Ruf uns an unter <a href="tel:{settings.phoneNumber -> v:format.pregReplace(pattern: '/[\-\s]/', replacement: '')}" title="Service Telefon anrufen" rel="nofollow">{settings.phoneNumber}</a>
oder schreibe an <f:link.email email="[email protected]"/>.
</div>
<template v-if="available">
<dates-table :dates-rows="datesRows"
:alt-product-link="altProductLink"
:alt-label="altLabel"
hotel-name="{hotel.name}"
:bookable="bookable"
></dates-table>
</template>
<f:if condition="{isProductWithNoInfoConcept}">
<f:else>
<template v-else>
<div class="alert alert-info">
Die von dir gewählte Reise ist aktuell nicht buchbar, aber wir helfen dir gerne dabei,
eine andere Reise zu finden. Ruf uns an unter <a href="tel:{settings.phoneNumber -> v:format.pregReplace(pattern: '/[\-\s]/', replacement: '')}" title="Service Telefon anrufen" rel="nofollow">{settings.phoneNumber}</a>
oder schreibe an <f:link.email email="[email protected]"/>.
</div>
</template>
</f:else>
</f:if>
</f:else>
</f:if>
<f:render partial="Product/Details" arguments="{_all}"/>
@@ -152,7 +169,8 @@
<!-- /Dates -->
<!-- Prices -->
<div v-cloak v-show="!loading && priceTableShow">
<price-table :price-table-rows="priceTableRows" :services-included="servicesIncluded" :date-label="dateLabel" :single-date="singleDate"></price-table>
<price-table :price-table-rows="priceTableRows" :services-included="servicesIncluded"
:date-label="dateLabel" :single-date="singleDate"></price-table>
<f:render partial="Product/Details" arguments="{_all}"/>
</div>
<!-- /Prices -->