feat: migrate to new myep app for bookings

This commit is contained in:
Björn Fromme
2026-01-05 16:59:56 +01:00
parent ff457a1d3d
commit 96e7e8005b
38 changed files with 52 additions and 2675 deletions
@@ -168,7 +168,6 @@ class AjaxTableController extends ActionController
'dateTableData' => $dates,
'template' => $this->settings['bpnBookingUrlTemplateCode'],
'paCode' => $paCode,
'isDayTrip' => $product->isDaytrip(),
'showBookingButton' => false === $product->getHideBookingButton(),
'nonBookableDateUids' => $nonBookableDateUids,
]);
@@ -200,7 +200,7 @@ class ProductRepository extends AbstractRepository
'date.uid as dateUid', 'date.date_start as dateStart', 'date.date_end as dateEnd',
'date.bus_pro_id as dateBusProId', 'date.hotel as hotelUid', 'date.hotel_bus_pro_id as hotelBusProId',
'date.bus_included as dateBusIncluded', 'date.skipass_included as dateSkipassIncluded',
'date.season as season',
'date.season as season', 'date.product_detail_page as productDetailPage',
'room.uid as roomUid', 'room.bus_pro_id as roomBusProId', 'room.name as roomName',
'room.price as roomPrice', 'date.discount as roomDiscount', 'date.bus_price as busPrice',
'room.services as roomOptionalServices', 'room.nights as roomNights', 'room.available as roomAvailable'
@@ -259,7 +259,7 @@ class ProductRepository extends AbstractRepository
'date.bus_included as dateBusIncluded', 'date.services_included as dateServicesIncluded',
'date.services_general as dateServicesGeneral', 'date.skipass_included as dateSkipassIncluded',
'date.bus_pro_id as dateBusProId', 'date.hotel_bus_pro_id as hotelBusProId',
'date.available as available'
'date.available as available', 'date.product_detail_page as productDetailPage',
)
->from('tx_epproducts_domain_model_date', 'date')
->innerJoin('date', 'tx_epproducts_domain_model_room', 'room', 'room.date = date.uid')
@@ -89,13 +89,13 @@ class ContingentDataService
$data = $this->contingentRepository->getAvailableRooms($dateFrom, $dateTo, $hotel, $product);
$period = new Period($dateFrom, $dateTo);
$nights = $period->dateInterval()->format('%a');
$rooms = [];
foreach ($data as $row) {
$row['bookingUrl'] = BookingUrlUtility::generateUrl([
'bookingId' => $row['busProId'],
'dateId' => $row['busProId'],
'hotelId' => $row['hotelBusProId'],
'dateEnd' => $dateTo,
'template' => $template,
'theme' => $template,
]);
$row['summer'] = $row['season'] === 's';
@@ -132,7 +132,6 @@ class DateService implements SingletonInterface
'priceTableData' => $this->productRepository->getPricetable($product, $hotel, $filterSettings, $date),
'template' => $template,
'paCode' => $paCode,
'isDayTrip' => $product->isDaytrip(),
'showBookingButton' => false === $product->getHideBookingButton(),
'nonBookableDateUids' => $nonBookableDateUids,
]);
@@ -194,9 +193,8 @@ class DateService implements SingletonInterface
public function preprocessPriceTable(array $options): array
{
$resolver = new OptionsResolver();
$resolver->setDefined(['paCode', 'isDayTrip', 'showBookingButton', 'nonBookableDateUids']);
$resolver->setDefined(['paCode', 'showBookingButton', 'nonBookableDateUids']);
$resolver->setRequired(['priceTableData', 'template']);
$resolver->setDefault('isDayTrip', false);
$resolver->setDefault('showBookingButton', true);
$resolvedOptions = $resolver->resolve($options);
@@ -208,7 +206,6 @@ class DateService implements SingletonInterface
$priceTableData = $resolvedOptions['priceTableData'];
$template = $resolvedOptions['template'];
$paCode = $resolvedOptions['paCode'];
$isDayTrip = $resolvedOptions['isDayTrip'];
$nonBookableDateUids = $resolvedOptions['nonBookableDateUids'];
foreach ($priceTableData as $row) {
@@ -217,12 +214,11 @@ class DateService implements SingletonInterface
$dateStart = new \DateTime($row['dateStart']);
$dateEnd = new \DateTime($row['dateEnd']);
$bookingUrl = BookingUrlUtility::generateUrl([
'bookingId' => $row['dateBusProId'],
'dateId' => $row['dateBusProId'],
'hotelId' => $row['hotelBusProId'],
'dateEnd' => $row['dateEnd'],
'template' => $template,
'theme' => $template,
'paCode' => $paCode,
'isDayTrip' => $isDayTrip,
'returnPageUid' => $row['productDetailPage'] ?? null,
]);
$optionalServices = $this->preprocessOptionalServices(unserialize($row['roomOptionalServices'], ['allowed_classes' => false]));
@@ -284,9 +280,8 @@ class DateService implements SingletonInterface
public function preprocessDatesTable(array $options): array
{
$resolver = new OptionsResolver();
$resolver->setDefined(['paCode', 'isDayTrip', 'showBookingButton', 'nonBookableDateUids']);
$resolver->setDefined(['paCode', 'showBookingButton', 'nonBookableDateUids']);
$resolver->setRequired(['dateTableData', 'template']);
$resolver->setDefault('isDayTrip', false);
$resolver->setDefault('showBookingButton', true);
$resolver->setDefault('nonBookableDateUids', []);
$resolvedOptions = $resolver->resolve($options);
@@ -296,7 +291,6 @@ class DateService implements SingletonInterface
$dateTableData = $resolvedOptions['dateTableData'];
$template = $resolvedOptions['template'];
$paCode = $resolvedOptions['paCode'];
$isDayTrip = $resolvedOptions['isDayTrip'];
$nonBookableDateUids = $resolvedOptions['nonBookableDateUids'];
foreach ($dateTableData as $row) {
@@ -310,12 +304,11 @@ class DateService implements SingletonInterface
$dateStart = new \DateTime($row['dateStart']);
$dateEnd = new \DateTime($row['dateEnd']);
$bookingUrl = BookingUrlUtility::generateUrl([
'bookingId' => $row['dateBusProId'],
'dateId' => $row['dateBusProId'],
'hotelId' =>$row['hotelBusProId'],
'dateEnd' => $row['dateEnd'],
'template' => $template,
'theme' => $template,
'paCode' => $paCode,
'isDayTrip' => $isDayTrip,
'returnPageUid' => $row['productDetailPage'] ?? null,
]);
$dateTable[] = [
'dateUid' => $row['dateUid'],
@@ -386,11 +379,11 @@ class DateService implements SingletonInterface
'hotelTitle' => $row['hotelTitle'],
'hotelDetailPageUid' => $row['hotelMappingDetailPageUid'] ?? $row['hotelDetailPageUid'],
'bookingUrl' => BookingUrlUtility::generateUrl([
'bookingId' => $row['dateBusProId'],
'dateId' => $row['dateBusProId'],
'hotelId' => $row['hotelBusProId'],
'dateEnd' => $row['dateEnd'],
'template' => $template,
'theme' => $template,
'paCode' => $paCode,
'returnPageUid' => $row['productDetailPage'] ?? null,
]),
'rooms' => [],
];
@@ -145,10 +145,9 @@ class ResellerDataService implements SingletonInterface
$data[$row['dateStart']] = $date;
}
$data[$row['dateStart']]['bookingurl'] = BookingUrlUtility::generateResellerUrl([
'bookingId' => $row['dateBusProId'],
$data[$row['dateStart']]['bookingurl'] = BookingUrlUtility::generateUrl([
'dateId' => $row['dateBusProId'],
'hotelId' => $row['hotelBusProId'],
'dateEnd' => $row['dateEnd'],
'paCode' => $paCode,
]);
@@ -2,145 +2,45 @@
namespace EP\EpProducts\Utility;
/***************************************************************
*
* Copyright notice
*
* (c) 2016 Björn Fromme <[email protected]>, dreipunktnull
*
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use Symfony\Component\OptionsResolver\OptionsResolver;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Routing\RouterInterface;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
class BookingUrlUtility
{
const COOKIE_NAME = 'ep_trk';
public static function generateUrl(array $options): string
{
$config = GeneralUtility::makeInstance(ExtensionConfiguration::class)
->get('ep_products');
$resolver = new OptionsResolver();
$resolver->setDefined('paCode');
$resolver->setRequired(['bookingId', 'hotelId', 'dateEnd']);
$resolver->setDefined(['paCode', 'returnPageUid']);
$resolver->setRequired(['dateId', 'hotelId']);
$resolver->setDefaults([
'template' => 'base',
'isDayTrip' => false,
'theme' => 'base',
]);
$resolvedOptions = $resolver->resolve($options);
$bookingId = $resolvedOptions['bookingId'];
$hotelId = $resolvedOptions['hotelId'];
$dateEnd = $resolvedOptions['dateEnd'];
$template = $resolvedOptions['template'];
$paCode = $resolvedOptions['paCode'];
$isDayTrip = $resolvedOptions['isDayTrip'];
$query = static::buildQuery($bookingId, $hotelId, $dateEnd, $template, $isDayTrip);
if ('' === session_id()) {
session_start();
}
$trackingCode = $_SESSION['ep_trk'] ?? null;
if (null !== $paCode) {
$query['agenturcode'] = $paCode;
} elseif (null !== $trackingCode) {
$queryParameter = static::getQueryParameter($trackingCode);
$query[$queryParameter] = $trackingCode;
}
return static::buildUrl($query);
}
public static function generateResellerUrl(array $options): string
{
$resolver = new OptionsResolver();
$resolver->setDefined('paCode');
$resolver->setRequired(['bookingId', 'hotelId', 'dateEnd']);
$resolver->setDefaults([
'template' => 'base',
'isDayTrip' => false,
]);
$resolvedOptions = $resolver->resolve($options);
$bookingId = $resolvedOptions['bookingId'];
$hotelId = $resolvedOptions['hotelId'];
$dateEnd = $resolvedOptions['dateEnd'];
$template = $resolvedOptions['template'];
$paCode = $resolvedOptions['paCode'];
$isDayTrip = $resolvedOptions['isDayTrip'];
$query = static::buildQuery($bookingId, $hotelId, $dateEnd, $template, $isDayTrip);
if ($paCode !== null) {
$query['agenturcode'] = $paCode;
}
return static::buildUrl($query);
}
protected static function buildQuery(string $bookingId, string $hotelId, ?string $dateEnd, string $template = 'base', bool $isDayTrip = false): array
{
$query = [
'id' => $bookingId,
'hotelid' => $hotelId,
'date_id' => $resolvedOptions['dateId'],
'hotel_id' => $resolvedOptions['hotelId'],
'theme' => $resolvedOptions['theme'],
];
if ($dateEnd && !$isDayTrip) {
$query['ruecktermin'] = date('d.m.Y', strtotime($dateEnd));
if (null !== $resolvedOptions['paCode']) {
$query['agency'] = $resolvedOptions['paCode'];
}
if ($template) {
$query['templatezusatz'] = '_' . $template;
if (null !== $resolvedOptions['returnPageUid'] && 0 < (int) $resolvedOptions['returnPageUid']) {
$siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
$site = $siteFinder->getSiteByPageId((int) $resolvedOptions['returnPageUid']);
$query['r'] = (string) $site->getRouter()->generateUri((int) $resolvedOptions['returnPageUid'], [], '', RouterInterface::ABSOLUTE_URL);
}
return $query;
}
protected static function buildUrl(array $query): string
{
/** @var SiteFinder $siteFinder */
$siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
$pageId = $GLOBALS['TSFE']->id;
if (null !== $pageId) {
$site = $siteFinder->getSiteByPageId($pageId);
} else {
$site = $siteFinder->getSiteByIdentifier('ep-reisen');
}
return $site->getBase() . '/buchung/maske_buchung.php?' . http_build_query($query);
}
protected static function getQueryParameter($code)
{
if (!preg_match('/^PA\d{3,4}$/', $code)) {
return 'crminfo';
}
return 'agenturcode';
return $config['myEpUrl'].'/bookings/create?'.http_build_query($query);
}
}
@@ -1,6 +1,9 @@
# cat=General; type=int; label=Searchresult PID
searchPageUid = 0
# cat=MyEp; type=string; label=MyE&P URL
myEpUrl =
# cat=MyEpAPI; type=string; label=MyE&P API base URL (trailing slash!)
myEpApiBaseUrl =
@@ -35,6 +35,10 @@ $boot = function () {
],
];
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptConstants(
'plugin.tx_epproducts.settings.myEpUrl = '.$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['ep_products']['myEpUrl']
);
$iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Imaging\IconRegistry::class);
$iconRegistry->registerIcon(
'ep-logo',
@@ -82,9 +82,9 @@ class BookingViewHelper extends AbstractViewHelper
}
$options = [
'bookingId' => $arguments['bookingId'],
'dateId' => $arguments['bookingId'],
'hotelId' => $arguments['hotelId'],
'template' => $template,
'theme' => $template,
];
return BookingUrlUtility::generateUrl($options);
}
@@ -76,6 +76,10 @@ plugin.tx_eptheme {
globalConceptCode = {$plugin.tx_eptheme.settings.globalConceptCode}
travelAlertPageUid = {$plugin.tx_eptheme.settings.travelAlertPageUid}
myEpPageUid = {$plugin.tx_eptheme.settings.myEpPageUid}
myEpApiEndpointUrl = {$plugin.tx_eptheme.settings.myEpApiEndpointUrl}
myEpApiToken = {$plugin.tx_eptheme.settings.myEpApiToken}
myEpApiTimeout = {$plugin.tx_eptheme.settings.myEpApiTimeout}
myEpUrl = {$plugin.tx_epproducts.settings.myEpUrl}
facebookViewContentValues {
productUid = {$plugin.tx_eptheme.settings.facebookViewContentProductUid}
nameInternal = {$plugin.tx_eptheme.settings.facebookViewContentNameInternal}
@@ -1,16 +0,0 @@
// Initialize vue app
import Vue from 'vue'
import vueCustomElement from 'vue-custom-element'
import dayjs from 'dayjs'
import 'dayjs/locale/de'
import 'document-register-element/build/document-register-element'
import App from './myep/App'
Vue.filter('date', (value) => {
return dayjs(value).format('DD.MM.YY');
})
Vue.use(vueCustomElement)
Vue.config.productionTip = false
Vue.customElement('my-ep', App)
@@ -1,130 +0,0 @@
<template>
<div>
<p class="text-2xl">Adressdaten</p>
<form @submit.prevent="save()">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<form-group label="Gender">
<select v-model="userData.gender" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
<option value="w">W</option>
<option value="m">M</option>
<option value="d">D</option>
</select>
</form-group>
<form-group label="Title">
<input type="text" v-model="userData.title" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
</form-group>
<form-group label="Name" :error="formErrors.lastName">
<input type="text" v-model="userData.lastName" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
</form-group>
<form-group label="Vorname" :error="formErrors.firstName">
<input type="text" v-model="userData.firstName" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
</form-group>
<form-group label="Geburtsdatum">
<div ref="picker"></div>
</form-group>
</div>
<div>
<form-group label="Straße" :error="formErrors.street">
<input type="text" v-model="userData.street" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
</form-group>
<form-group label="PLZ" :error="formErrors.zipCode">
<input type="text" v-model="userData.zipCode" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
</form-group>
<form-group label="Stadt" :error="formErrors.city">
<input type="text" v-model="userData.city" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
</form-group>
<form-group label="Land" :error="formErrors.country">
<select v-model="userData.country" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
<option v-for="country in countries"
:value="country['code']">{{ country['name'] }}</option>
</select>
</form-group>
<form-group label="E-Mail" :error="formErrors.email">
<input type="text" v-model="userData.email" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
</form-group>
<form-group label="Telefon" :error="formErrors.phonePrivate">
<input type="text" v-model="userData.phonePrivate" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
</form-group>
<form-group label="Mobil" :error="formErrors.phoneMobile">
<input type="text" v-model="userData.phoneMobile" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
</form-group>
</div>
<div class="col-span-1 md:col-span2">
<button type="submit" class="button bg-button" :disabled="loading">
Speichern
</button>
</div>
</div>
</form>
</div>
</template>
<script>
import Vue from 'vue'
import { mapState, mapMutations, mapActions } from 'vuex'
import flatpickr from 'flatpickr'
import { German } from 'flatpickr/dist/l10n/de'
import FormGroup from './components/FormGroup'
export default {
components: {
FormGroup
},
data () {
return {
picker: null,
formErrors: {},
}
},
created () {
this.loadAddress()
.then(() => {
this.picker.setDate(new Date(this.userData.dateOfBirth), true);
this.$emit('loaded')
})
.catch(() => {
this.logout()
this.$router.push({name: 'login'})
})
},
mounted () {
Vue.nextTick(() => {
this.picker = flatpickr(this.$refs.picker, {
dateFormat: 'd.m.Y',
locale: German,
inline: true,
onChange: selectedDates => {
this.userData.dateOfBirth = flatpickr.formatDate(selectedDates[0], 'Y-m-d')
}
});
})
},
methods: {
...mapMutations(['logout', 'alert']),
...mapActions(['loadAddress', 'saveAddress']),
save () {
this.formErrors = {}
this.saveAddress(this.userData)
.catch(error => {
this.alert({
message: 'Bitte überprüfe deine Eingaben.',
success: false,
})
const violations = error.violations
if (typeof violations !== 'undefined') {
for (let violation of violations) {
this.$set(this.formErrors, violation.property_path, violation.message)
}
}
})
.finally(() => {
this.$emit('loaded')
})
}
},
computed: {
...mapState(['loading', 'userData', 'countries'])
}
}
</script>
@@ -1,82 +0,0 @@
<template>
<div class="relative mb-16">
<a ref="container" class="block w-0 h-0 pt-24 -mt-24"></a>
<nav class="w-full bg-zinc-50 flex items-center mb-8 relative" v-if="isLoggedIn">
<div class="flex-1 block text-2xl px-4">My E&P</div>
<div class="bg-zinc-50 shadow w-full lg:w-auto absolute lg:relative z-10 lg:z-auto top-full lg:top-auto left-0 lg:shadow-none lg:flex lg:space-x-4"
:class="{ 'hidden': !navVisible }">
<div class="lg:flex lg:items-center m-0">
<router-link @click.native="navVisible = false" :to="{ name: 'address' }" class="block p-4 text-black hover:bg-zinc-400 hover:text-zinc-800" active-class="bg-zinc-400 text-zinc-800">Adressdaten</router-link>
<router-link @click.native="navVisible = false" :to="{ name: 'newsletter' }" class="block p-4 text-black hover:bg-zinc-400 hover:text-zinc-800" active-class="bg-zinc-400 text-zinc-800">Newsletter</router-link>
<router-link @click.native="navVisible = false" :to="{ name: 'profile' }" class="block p-4 text-black hover:bg-zinc-400 hover:text-zinc-800" active-class="bg-zinc-400 text-zinc-800">Mein Profil</router-link>
<router-link @click.native="navVisible = false" :to="{ name: 'events' }" class="block p-4 text-black hover:bg-zinc-400 hover:text-zinc-800" active-class="bg-zinc-400 text-zinc-800">Buchungen</router-link>
</div>
<a class="block px-4 py-4 text-black hover:bg-zinc-400 hover:text-zinc-800" href="#" @click.prevent="performLogout()">Logout</a>
</div>
<a href="#" class="lg:hidden p-4 text-black hover:text-zinc-800" :class="{ 'rotate-90': navVisible }" @click.prevent="navVisible = !navVisible">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
</a>
</nav>
<div class="p-4 mb-8 text-white" :class="{'bg-emerald-600': alert.success, 'bg-red-600': !alert.success}" v-if="alert.message" v-html="alert.message"></div>
<div class="relative">
<router-view @loaded="scrollTop()"></router-view>
<div class="absolute top-0 left-0 inset-0 bg-white/85" v-show="loading">
<div class="absolute top-1/2 left-1/2 transform -translate-xy-1/2 flex items-center space-x-2 h-8">
<img :src="config.loaderUrl" alt="" class="block w-4 h-4">
<span v-text="statusMessage"></span>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapState, mapActions, mapMutations } from 'vuex'
import store from './store'
import router from './router'
export default {
store,
router,
methods: {
performLogout () {
this.logout()
.then(() => {
this.$router.replace({ name: 'login' })
}).catch(error => {});
},
scrollTop() {
this.$refs.container.scrollIntoView({behavior: 'smooth', block: 'start'})
},
...mapMutations([
'initConfig',
]),
...mapActions([
'checkLoginState',
'loadCountries',
'logout',
]),
},
data () {
return {
navVisible: false
}
},
computed: {
...mapState(['config', 'loading', 'alert', 'statusMessage']),
...mapGetters(['isLoggedIn', 'isTeamer'])
},
mounted () {
this.initConfig()
this.checkLoginState()
.then(() => {
if (this.isLoggedIn) {
this.loadCountries()
.then(() => {
this.$router.replace({ name: 'address' })
})
}
})
},
}
</script>
@@ -1,548 +0,0 @@
<template>
<div class="wrapper">
<div class="grid grid-cols-2 gap-x-2 pb-4">
<button type="button" class="button bg-button" :class="{ 'bg-button--secondary': tab === 'data' }" @click="tab = 'data'">
Buchungsübersicht
</button>
<button type="button" class="button bg-button" :class="{ 'bg-button--secondary': tab === 'participants' }" @click="tab = 'participants'">
Buchung ändern
</button>
</div>
<div v-if="initialized" v-show="tab === 'data'">
<h2 class="mb-4">
Reisedaten
</h2>
<div class="grid lg:grid-cols-2 gap-4">
<div>
<panel title="Allgemein">
<ul class="list-none">
<li class="p-2 border-b border-zinc-200 last:border-0">
<strong>Reise:</strong> {{ travelData.productName }}
</li>
<li class="p-2 border-b border-zinc-200 last:border-0">
<strong>Zeitraum:</strong> {{ travelData.dateFrom }} - {{ travelData.dateTo }}
</li>
<li class="p-2 border-b border-zinc-200 last:border-0">
<strong>Inklusivleistungen:</strong> {{ travelData.servicesIncluded.join(', ') }}
</li>
<li class="p-2 border-b border-zinc-200 last:border-0">
<strong>Anmelder:</strong> {{ bookingData.applicant.firstName }} {{ bookingData.applicant.lastName }}
</li>
<li class="p-2 border-b border-zinc-200 last:border-0">
<strong>Teilnehmer:</strong> {{ participantsCount.active }}
</li>
<li class="p-2 border-b border-zinc-200 last:border-0">
<strong>Stornos:</strong> {{ participantsCount.canceled }}
</li>
<li class="p-2 border-b border-zinc-200 last:border-0">
<strong>Gesamtpreis:</strong> Es gelten die in der Rechnung angegebenen Preise
</li>
</ul>
</panel>
<services-overview :services="services.other" :participant-data="participantData"/>
</div>
<div>
<mutable-fields :mutable-fields="mutableFields"></mutable-fields>
<contingents-overview :contingents="bookingData.accomodations"/>
<panel title="Beförderungsart (alle Teilnehmer)" :body="true">
<ul class="list-none">
<li class="p-2 border-b border-zinc-200 last:border-0" v-for="transportation in bookingData.transportations" :key="transportation.id">
{{ transportation.name }}: {{ transportation.count }}
</li>
</ul>
</panel>
</div>
</div>
<div class="py-4 lg:flex lg:justify-between space-y-1 lg:space-y-0" v-show="!loading" v-if="initialized">
<button class="button bg-button" :disabled="!dirty" @click.prevent="storeBookingData()">
Buchung aktualisieren
</button>
<router-link :to="{ name: 'events' }" class="button bg-button">
zur Übersicht
</router-link>
</div>
</div>
<div id="participants" class="py-4" v-if="initialized" v-show="tab === 'participants'">
<h2 class="mb-4">
Teilnehmer
</h2>
<div class="py-4 lg:flex lg:justify-between space-y-1 lg:space-y-0" v-show="!loading" v-if="initialized && participantData.length > 5">
<button class="button bg-button" :disabled="!dirty" @click.prevent="storeBookingData()">
Buchung aktualisieren
</button>
<router-link :to="{ name: 'events' }" class="button bg-button">
zur Übersicht
</router-link>
</div>
<collapsible-panel :title="'#' + (index + 1) + ' ' + participant.data.lastName + ', ' + participant.data.firstName"
:class="invalidParticipantData[participant.data.id] ? 'panel-danger' : 'panel-default'" parent="#participant-list"
v-for="(participant, index) in participantData" :key="participant.data.id">
<template v-if="participant.data.canceled">
Storniert am {{ participant.data.cancellationDate }}, Stornokosten {{ participant.data.cancellationFee.toFixed(2) }}
</template>
<template v-else>
<div class="p-2 border-b border-zinc-200">
<participant-data-form :participant="participant"
:mutable="mutableFields.participantData.mutable"
:countries="countries"
v-on:updated="onParticipantDataUpdated($event, index)"
/>
</div>
<div class="flex flex-col space-y-1 md:flex-row md:space-y-0 md:space-x-1 py-4 border-b border-zinc-200 p-2">
<participant-address-form :participant="participant"
:countries="countries"
:mutable="mutableFields.participantData.mutable"
v-on:updated="onParticipantDataUpdated($event, index)"
/>
<accommodation-select :participant="participant"
:contingents="availableContingents"
:mutable="mutableFields.accommodation.mutable && isGroupManager"
v-on:selected="onAccommodationSelected($event, index)"
/>
<service-select v-for="subType in servicesSubTypes"
:key="subType.label"
:sub-type="subType"
:services="services.other"
:participant="participant"
:mutable="mutableFields.services.mutable"
v-on:updated="onServiceUpdated($event, index)"
/>
<participant-sizes-form :services="services.other"
:participant="participant"
:mutable="mutableFields.services.mutable"
v-on:updated="onParticipantDataUpdated($event, index)"/>
<transportation-select :services="services.transportation"
:participant="participant"
:mutable="mutableFields.transportation.mutable"
v-on:updated="onTransportationUpdated($event)"
/>
<pickup-select
:pickups="services.pickups"
:participant="participant"
:mutable="mutableFields.transportation.mutable"
v-on:selected="onPickupSelected($event, index)"
v-if="services.pickups"
/>
</div>
<div class="grid lg:grid-cols-2 gap-4 p-2">
<table class="text-sm mb-0">
<tbody>
<tr>
<th class="w-1/5">Zimmer</th>
<td>{{ getAccommodationLabel(participant) }}</td>
</tr>
<tr>
<th>Zusatzleistungen</th>
<td>{{ getSelectedServicesLabels(participant.serviceIds, services.other) }}</td>
</tr>
<tr>
<th>Beförderung</th>
<td>{{ getSelectedServicesLabels(participant.transportationIds, services.transportation) }}</td>
</tr>
<tr v-if="participant.surcharges.length > 0">
<th>Zuschläge</th>
<td>
<ul class="list-none p-0 mb-0">
<li v-for="surcharge in participant.surcharges">
{{ surcharge.priceSingle.toFixed(2) }} <span v-show="surcharge.name">({{ surcharge.name }})</span>
</li>
</ul>
</td>
</tr>
</tbody>
</table>
</div>
</template>
</collapsible-panel>
<div class="py-4 lg:flex lg:justify-between space-y-1 lg:space-y-0" v-show="!loading" v-if="initialized">
<button class="button bg-button" :disabled="!dirty" @click.prevent="storeBookingData()">
Buchung aktualisieren
</button>
<router-link :to="{ name: 'events' }" class="button bg-button">
zur Übersicht
</router-link>
</div>
</div>
</div>
</template>
<script>
import AccommodationSelect from './components/AccommodationSelect'
import ContingentsOverview from './components/ContingentsOverview'
import MutableFields from './components/MutableFields'
import Panel from './components/Panel'
import CollapsiblePanel from './components/CollapsiblePanel'
import ParticipantAddressForm from './components/ParticipantAddressForm'
import ParticipantDataForm from './components/ParticipantDataForm'
import ParticipantSizesForm from './components/ParticipantSizesForm'
import PickupSelect from './components/PickupSelect'
import ServiceSelect from './components/ServiceSelect'
import ServicesOverview from './components/ServicesOverview'
import TransportationSelect from './components/TransportationSelect'
import { mapGetters, mapMutations, mapState } from 'vuex'
import {checkStatus, defaultOptions} from './api'
export default {
name: 'BookingEdit',
components: {
Panel,
CollapsiblePanel,
MutableFields,
TransportationSelect,
ServicesOverview,
ContingentsOverview,
AccommodationSelect,
ParticipantDataForm,
ParticipantAddressForm,
ParticipantSizesForm,
PickupSelect,
ServiceSelect
},
data () {
return {
tab: 'data',
bookingId: null,
initialized: false,
dirty: false,
processing: false,
bookingData: null,
participantData: null,
invalidParticipantData: {},
eventData: null,
travelData: null,
mutableFields: null,
contingents: null,
services: null,
servicesSubTypes: [
{ 'codes': ['KUR'], 'label': 'Kurse' },
{ 'codes': ['SON'], 'label': 'Zusatzleistungen' },
{ 'codes': ['SPA'], 'label': 'Skipass' },
{ 'codes': ['VPF'], 'label': 'Verpflegung' },
{ 'codes': ['VER', 'VE2', 'VE3', 'VE4', 'VE5', 'VE6', 'VE7', 'VE8'], 'label': 'Verleih' },
{ 'codes': ['EIN'], 'label': '???' }
],
modal: {
title: 'Hinweis',
message: ''
}
}
},
methods: {
...mapMutations(['alert', 'beginLoading', 'endLoading']),
loadBookingData () {
this.beginLoading('Lade Reise- und Buchungsdaten...')
const options = {
...defaultOptions
}
return fetch(`${this.config.myEpApiBaseUrl}/api/booking/${this.bookingId}`, options)
.then(checkStatus)
.then(response => response.json())
.then(data => {
this.bookingData = data.bookingData
this.participantData = data.participantData
this.travelData = data.travelData
})
.finally(() => {
this.endLoading()
})
},
loadEventData () {
this.beginLoading('Lade Vorgangsdaten...')
const options = {
...defaultOptions
}
return fetch(`${this.config.myEpApiBaseUrl}/api/event/${this.bookingData.bookingNumber}`, options)
.then(checkStatus)
.then(response => response.json())
.then(data => {
this.eventData = data
})
.catch(error => {
throw new Error(error.message)
})
.finally(() => {
this.endLoading()
})
},
loadMutableFields () {
this.beginLoading('Lade mögliche Änderungen...')
const options = {
...defaultOptions
}
return fetch(`${this.config.myEpApiBaseUrl}/api/mutable-fields/${this.bookingData.travelId}`, options)
.then(checkStatus)
.then(response => response.json())
.then(data => {
this.mutableFields = data
})
.catch(() => {
throw new Error('Mögliche Änderungen konnten nicht geladen werden.');
})
.finally(() => {
this.endLoading()
})
},
loadContingents () {
this.beginLoading('Lade Kontingente...')
const options = {
...defaultOptions
}
return fetch(`${this.config.myEpApiBaseUrl}/api/contingents/${this.bookingData.travelId}/${this.bookingData.partnerId}`, options)
.then(checkStatus)
.then(response => response.json())
.then(data => {
this.contingents = data
})
.catch(() => {
throw new Error('Kontingente konnten nicht geladen werden.');
})
.finally(() => {
this.endLoading()
})
},
loadServices () {
this.beginLoading('Lade Leistungen...')
const options = {
...defaultOptions
}
return fetch(`${this.config.myEpApiBaseUrl}/api/services/${this.bookingData.travelId}`, options)
.then(checkStatus)
.then(response => response.json())
.then(data => {
this.services = data
})
.catch(() => {
throw new Error('Leistungen konnten nicht geladen werden.');
})
.finally(() => {
this.endLoading()
})
},
getAccommodationLabel (participant) {
if (participant.hasOwnProperty('accommodationId') && participant.accommodationId in this.contingents) {
return this.contingents[participant.accommodationId].name
}
return '-';
},
getSelectedServicesLabels (serviceIds, services) {
const labels = [];
for (let serviceId of serviceIds) {
if (services[serviceId]) {
labels.push(services[serviceId].name);
}
}
return labels.join(', ');
},
calculateSinglePrice (participant) {
let singlePrice = 0;
if (participant.hasOwnProperty('accommodationId') && participant.accommodationId in this.contingents) {
let accommodation = this.contingents[participant.accommodationId];
singlePrice += accommodation.price;
}
for (let serviceId of participant.serviceIds) {
if (this.services.other[serviceId]) {
singlePrice += this.services.other[serviceId].price;
}
}
for (let serviceId of participant.transportationIds) {
if (this.services.transportation[serviceId]) {
singlePrice += this.services.transportation[serviceId].price;
}
}
if ('S' === participant.data.status && participant.data.cancellationFee) {
singlePrice += participant.data.cancellationFee;
}
for (let surcharge of participant.surcharges) {
singlePrice += surcharge.priceSingle
}
return singlePrice;
},
onAccommodationSelected (accommodation, index) {
const participant = this.participantData[index];
if (participant.hasOwnProperty('accommodationId')) {
const currentAccommodationId = this.participantData[index].accommodationId;
if (currentAccommodationId in this.contingents) {
this.contingents[currentAccommodationId].available++;
}
}
const newAccommodationId = accommodation.id;
if (this.participantData[index].accommodationId === newAccommodationId) {
return;
}
this.participantData[index].accommodationId = newAccommodationId;
this.contingents[newAccommodationId].available--;
this.dirty = true;
},
onPickupSelected (pickupId, index) {
this.participantData[index].pickupId = pickupId;
this.dirty = true;
},
onParticipantDataUpdated (participant, index) {
this.participantData[index] = participant;
this.dirty = true;
},
onServiceUpdated (event, index) {
let delta = event.action === 'select' ? -1 : 1;
this.services.other[event.service.id].available += delta;
this.participantData[index] = event.participant;
this.dirty = true;
},
onTransportationUpdated (event) {
let services = this.services.transportation
if (event.action === 'select') {
services[event.service.id].available--;
} else {
services[event.service.id].available++;
}
this.services.transportation = services
this.dirty = true;
},
hasSelectedRentals (participant) {
let result = false;
for (let id of participant.serviceIds) {
if (typeof this.services.other[id] === 'undefined') {
continue;
}
if ('VER' === this.services.other[id].subType) {
result = true;
}
}
return result
},
validateInput () {
let messages = [];
this.invalidParticipantData = {};
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
for (let participant of this.participantData) {
let participantId = participant.data.id;
if (!emailPattern.test(participant.data.email)) {
this.invalidParticipantData[participantId] = {
invalid: 'E-Mail',
};
messages.push('Teilnehmer #'+participantId+': ungültige E-Mail-Adresse')
}
// if (!participant.data.nationality) {
// this.invalidParticipantData[participantId] = {
// invalid: 'Nationalität',
// };
// messages.push('Teilnehmer #'+participantId+': Nationalität fehlt')
// }
if (!this.hasSelectedRentals(participant)) {
continue;
}
if (!participant.data.height) {
this.invalidParticipantData[participantId] = {
invalid: 'Körpergröße',
};
messages.push('Teilnehmer #'+participantId+': Körpergröße fehlt')
}
if (!participant.data.shoeSize) {
this.invalidParticipantData[participantId] = {
invalid: 'Schuhgröße',
};
messages.push('Teilnehmer #'+participantId+': Schuhgröße fehlt')
}
}
return messages
},
storeBookingData() {
let messages = this.validateInput();
if (false === this.dataValid) {
this.alert({
message: messages.join('<br>'),
success: false,
});
return;
}
this.beginLoading()
const options = {
...defaultOptions,
method: 'PUT',
body: JSON.stringify({
participantData: this.participantData,
remarks: this.bookingData.remarks,
})
}
fetch(`${this.config.myEpApiBaseUrl}/api/booking/${this.bookingId}`, options)
.then(checkStatus)
.then(response => response.json())
.then(data => {
this.alert({
message: data.message,
success: true,
})
this.dirty = false
})
.catch(error => {
this.alert({
message: error.message,
success: false,
})
})
.finally(() => {
this.endLoading()
this.$emit('loaded')
})
}
},
computed: {
...mapState(['config', 'loading', 'countries']),
...mapGetters(['isGroupManager']),
availableContingents () {
let filteredContingents = {};
for (let id of Object.keys(this.contingents)) {
if (this.contingents[id].available > 0) {
filteredContingents[id] = this.contingents[id];
}
}
return filteredContingents;
},
totalPrice () {
let totalPrice = 0;
this.participantData.forEach(participant => {
totalPrice += this.calculateSinglePrice(participant);
});
return totalPrice;
},
participantsCount () {
let active = 0;
let canceled = 0;
for (const participant of this.participantData) {
if (participant.data.canceled) {
canceled++;
} else {
active++;
}
}
return {active: active, canceled: canceled};
},
dataValid () {
return Object.keys(this.invalidParticipantData).length === 0;
}
},
mounted () {
this.bookingId = this.$route.params.id
this.loadBookingData()
.then(() => this.loadEventData())
.then(() => this.loadMutableFields())
.then(() => this.loadContingents())
.then(() => this.loadServices())
.then(() => {
this.initialized = true;
})
.catch(error => {
this.alert({
message: 'Es ist ein Fehler aufgetreten: ' + error.message,
success: false,
})
})
.finally(() => {
this.$emit('loaded')
})
}
}
</script>
@@ -1,138 +0,0 @@
<template>
<div>
<h1 class="mb-4">
Mein Profil
</h1>
<form @submit.prevent="save()">
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 mb-4">
<div>
<template v-for="selectionGroup in crmData.selectionGroups"
v-if="hasVisibleSelections(selectionGroup)">
<h3 class="mb-2">
{{ selectionGroup.name}}
</h3>
<div class="mb-2"
v-for="selection in selectionGroup.selections"
v-if="selection.changeable === true && isSelectionVisible(selection)">
<label class="inline-flex items-center" :class="{ 'opacity-50': !selection.changeable}">
<input type="checkbox"
class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 text-ep-secondary sbw:text-sbw-secondary"
v-model="selection.value"
:disabled="!selection.changeable"/>
<span class="ml-2">
{{ selection.name }}
</span>
</label>
</div>
</template>
</div>
<div>
<div class="mb-2"
v-for="action in crmData.actions"
v-if="action.changeable === true">
<label class="inline-flex items-center" :class="{ 'opacity-50': !action.changeable}">
<input type="checkbox"
class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 text-ep-secondary sbw:text-sbw-secondary"
v-model="action.value"
:disabled="!action.changeable"/>
<span class="ml-2">
{{ action.name }}
</span>
</label>
</div>
</div>
</div>
<button type="submit" class="button bg-button" :disabled="loading">
Speichern
</button>
</form>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import { defaultOptions, checkStatus } from './api'
export default {
data () {
return {
crmData: [],
hiddenSelectionGroups: [ 10 ],
hiddenSelections: [ 1070 ]
}
},
created () {
this.beginLoading()
const options = {
...defaultOptions
}
return fetch(`${this.config.myEpApiBaseUrl}/api/crm-selections`, options)
.then(checkStatus)
.then(response => response.json())
.then(data => {
this.crmData = data
})
.catch(() => {
this.alert({
message: 'Es ist ein Fehler aufgetreten :(',
success: false,
})
})
.finally(() => {
this.endLoading()
this.$emit('loaded')
})
},
methods: {
...mapMutations(['beginLoading', 'endLoading', 'alert']),
save () {
this.beginLoading()
const options = {
...defaultOptions,
method: 'PUT',
body: JSON.stringify(this.crmData)
}
return fetch(`${this.config.myEpApiBaseUrl}/api/crm-selections`, options)
.then(checkStatus)
.then(response => response.json())
.then(() => {
this.alert({
message: 'Die Änderungen wurden gespeichert',
success: true,
})
})
.catch(() => {
this.alert({
message: 'Es ist ein Fehler aufgetreten :(',
success: false,
})
})
.finally(() => {
this.endLoading()
this.$emit('loaded')
})
},
hasVisibleSelections (group) {
if (this.hiddenSelectionGroups.indexOf(group.id) !== -1) {
return false
}
for (let selection of group.selections) {
if (this.hiddenSelections.indexOf(selection.id) === -1 && selection.changeable === true) {
return true
}
}
return false
},
isSelectionVisible (selection) {
return this.hiddenSelections.indexOf(selection.id) === -1
}
},
computed: {
...mapState(['config', 'loading'])
}
}
</script>
@@ -1,157 +0,0 @@
<template>
<div>
<h1 class="mb-4">
Meine Buchungen
</h1>
<div class="w-full max-w-full overflow-x-scroll md:overflow-x-auto">
<table class="min-w-full border border-zinc-200 border-collapse mb-8 text-sm">
<thead>
<tr>
<th class="px-2 py-1 border border-zinc-200 text-left">
Buchungsdatum
</th>
<th class="px-2 py-1 border border-zinc-200 text-left">
Reisedatum
</th>
<th class="px-2 py-1 border border-zinc-200 text-left">
Reise
</th>
<th class="px-2 py-1 border border-zinc-200 text-left">
Vorgang
</th>
<th class="px-2 py-1 border border-zinc-200 text-center">
Preis
</th>
<th class="px-2 py-1 border border-zinc-200 text-center">
offen
</th>
<th class="px-2 py-1 border border-zinc-200 text-left">
Status
</th>
<th class="px-2 py-1 border border-zinc-200"></th>
</tr>
</thead>
<tbody>
<tr v-for="event of events" class="even:bg-zinc-50">
<td class="px-2 py-1 border border-zinc-200">
{{ formatBookingDate(event.dateBooking) }}
</td>
<td class="px-2 py-1 border border-zinc-200">
{{ event.dateTravel | date }}
</td>
<td class="px-2 py-1 border border-zinc-200">
{{ event.product }}
</td>
<td class="px-2 py-1 border border-zinc-200">
{{ event.bookingNumber }}
</td>
<td class="px-2 py-1 border border-zinc-200 whitespace-nowrap">
{{ formatCurrency(event.price) }}
</td>
<td class="px-2 py-1 border border-zinc-200 whitespace-nowrap" :class="{ 'text-red-500': hasOutstandingPayment(event) }">
{{ calculateBalance(event) }}
</td>
<td class="px-2 py-1 border border-zinc-200">
{{ status[event.status] }}
</td>
<td class="px-2 py-1 border border-zinc-200">
<div class="grid grid-cols-1 xl:grid-cols-3 gap-2 w-32 xl:w-auto">
<a :href="getDownloadUrl(event, 'confirmation')" target="_blank" class="button bg-button button--small">
Bestätigung
</a>
<router-link :to="{ name: 'bookingEdit', params: { id: event.id } }" v-if="event.editable" class="button bg-button button--small">
bearbeiten
</router-link>
<a :href="event.travelInfoUrl"
target="_blank"
class="button bg-button bg-button--secondary button--small"
v-if="event.travelInfoUrl">
Reiseinfos
</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
import { mapState, mapMutations, mapGetters } from 'vuex'
import dayjs from 'dayjs'
import 'dayjs/locale/de'
import { checkStatus, defaultOptions } from './api'
export default {
data () {
return {
status: {
'F': 'Festbuchung',
'S': 'Stornierung',
'F/S': 'Festbuchung',
'O': 'Option',
'U': 'Umbuchung',
},
events: []
}
},
created () {
this.beginLoading();
const options = {
...defaultOptions
}
return fetch(`${this.config.myEpApiBaseUrl}/api/events`, options)
.then(checkStatus)
.then(response => response.json())
.then(data => {
this.events = data
})
.catch(() => {
this.alert({
message: 'Es ist ein Fehler aufgetreten :(',
success: false,
})
})
.finally(() => {
this.endLoading()
this.$emit('loaded')
})
},
methods: {
...mapMutations(['beginLoading', 'endLoading', 'alert']),
formatBookingDate (date) {
if (!date) {
return '-'
}
return dayjs(date).format('DD.MM.YY')
},
getDownloadUrl (event, type) {
return `${this.config.myEpApiBaseUrl}/api/events/${event.id}/${type}`
},
calculateBalance (event) {
if (!event.price) {
return '-';
}
const balance = event.price - event.payment;
if (balance > 0) {
return this.formatCurrency(balance);
}
return '-';
},
hasOutstandingPayment (event) {
return (event.price - event.payment) > 0;
},
formatCurrency (value) {
return value.toFixed(2).replace('.', ',') + ' €';
}
},
computed: {
...mapState(['config', 'loading']),
...mapGetters(['isGroupManager'])
}
}
</script>
@@ -1,152 +0,0 @@
<template>
<div id="myep-main">
<h1 class="mb-4">
Login MyE&amp;P
</h1>
<form @submit.prevent="submit()">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<form-group label="E-Mail" :error="formErrors.email">
<input type="email" v-model="email" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
</form-group>
<form-group label="Passwort" v-if="loginMode">
<input type="password" v-model="password" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
</form-group>
<div class="flex flex-col lg:flex-row space-y-2 lg:space-y-0 lg:space-x-2 mt-4">
<button type="submit" class="button bg-button" :disabled="loading">
{{ loginMode ? 'Login' : 'Passwort erneuern' }}
</button>
<button class="button bg-button bg-button--secondary" @click.prevent="toggleMode()">
{{ loginMode ? 'Passwort vergessen?' : 'Zum Login' }}
</button>
<router-link to="register" class="button bg-button bg-button--secondary">Registrieren</router-link>
</div>
</div>
<div>
<h3 class="mb-2">
Neu!
</h3>
<p class="mb-4">
Melde dich jetzt bei MyEP an und du kannst jederzeit auf deine aktuellen Buchungen zugreifen und
diese abändern. Egal ob du einen Kurs oder eine Skipasserweiterung hinzufügen/entfernen möchtest,
oder das vegetarische Essen auswählen willst. Im neuen Portal ist das nun kostenlos bis 7 Tage vor
Reisebeginn möglich!
</p>
<h3 class="mb-2">
Noch keine Zugangsdaten?
</h3>
<p class="mb-2">
Dann gib deine E-Mail ein, mit der Du gebucht hast und klicke auf "Passwort vergessen"! Und wir
senden dir ein E-Mail mit einem Passwort-Link!
</p>
<p>
Ein LogIn ist nur möglich wenn Eure Daten schon durch eine Buchung oder Anfrage bei uns
angelegt wurden. Wenn Ihr auch Kunde werden möchtet, dann schickt uns Eure Interessen über unser
<a href="https://www.ep-reisen.de/skireisen/unternehmen/kontakt/kontaktformular/" title="Kontaktformular">Kontaktformular</a>.
</p>
</div>
</div>
</form>
</div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
import FormGroup from './components/FormGroup'
export default {
components: {FormGroup},
data () {
return {
mode: 'login',
email: '',
password: '',
formErrors: {}
}
},
methods: {
...mapMutations(['alert']),
...mapActions(['login', 'resetPassword', 'loadCountries']),
toggleMode () {
this.mode = this.mode === 'login' ? 'reset' : 'login';
this.password = ''
},
submit () {
this.message = '';
if (this.mode === 'login') {
this.performLogin()
} else {
this.performPasswordReset()
}
},
performLogin () {
this.formErrors = {};
if (!this.email || !this.password) {
this.alert({
message: 'Bitte E-Mail und Passwort eingeben',
success: false,
});
return
}
this.login({
email: this.email,
password: this.password
}).then(() => {
this.loadCountries()
.then(() => {
this.$router.push({ name: 'address' })
})
}).catch(error => {
}).finally(() => {
this.email = ''
this.password = ''
})
},
performPasswordReset () {
this.formErrors = {};
if (!this.email) {
this.alert({
message: 'Bitte die E-Mail Adresse angeben',
success: false,
});
return;
}
this.resetPassword(this.email)
.then(response => {
this.email = '';
if (response.success) {
this.alert({
message: 'Du erhältst in Kürze eine E-Mail mit einem Link zum (Zurück)setzen deines Passworts.',
success: true,
})
} else {
this.alert({
message: 'Diese E-Mail Adresse konnte nicht gefunden werden.',
success: false,
})
}
}).catch(error => {
this.alert({
message: 'Bitte überprüfe deine Eingaben.',
success: false,
})
const violations = error.response.data.violations;
if (typeof violations !== 'undefined') {
for (let violation of violations) {
this.$set(this.formErrors, violation.property_path, violation.message);
}
}
}).finally(() => {
this.$emit('loaded')
})
}
},
computed: {
...mapState(['loading']),
...mapGetters(['isLoggedIn']),
loginMode () {
return this.mode === 'login';
}
}
}
</script>
@@ -1,71 +0,0 @@
<template>
<div>
<h1 class="mb-4">
Newsletter
</h1>
<p class="mb-8" v-html="statusMessage"></p>
<button type="submit" class="button bg-button" :disabled="loading" @click.prevent="toggleReqistration()">
{{ userData.newsletter ? 'Abmelden' : 'Anmelden' }}
</button>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import { checkStatus, defaultOptions } from './api'
export default {
methods: {
...mapMutations(['beginLoading', 'endLoading', 'alert']),
toggleReqistration () {
this.userData.newsletter = !this.userData.newsletter;
const data = {
id: this.userData.id,
email: this.userData.email,
registration: this.userData.newsletter
};
this.beginLoading();
const options = {
...defaultOptions,
method: 'PUT',
body: JSON.stringify(data)
}
return fetch(`${this.config.myEpApiBaseUrl}/api/newsletter`, options)
.then(checkStatus)
.then(response => response.json())
.then(() => {
this.alert({
message: 'Die Änderungen wurden gespeichert',
success: true,
})
})
.catch(() => {
this.alert({
message: 'Es ist ein Fehler aufgetreten :(',
success: false,
})
})
.finally(() => {
this.endLoading()
this.$emit('loaded')
})
}
},
computed: {
...mapState(['config', 'loading', 'userData']),
statusMessage () {
let message = 'Du bist aktuell';
message += this.userData.newsletter ? ' ' : ' <strong>nicht</strong> ';
message += 'zum Newsletter angemeldet.';
return message;
}
},
created() {
this.$emit('loaded')
}
}
</script>
@@ -1,94 +0,0 @@
<template>
<div>
<h1 class="mb-4">
Registrierung MyE&amp;P
</h1>
<form @submit.prevent="performRegistration()">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 mb-4">
<form-group label="Vorname" :error="formErrors.firstName">
<input type="text" v-model="userData.firstName" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
</form-group>
<form-group label="Name" :error="formErrors.lastName">
<input type="text" v-model="userData.lastName" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
</form-group>
<form-group label="E-Mail" :error="formErrors.email">
<input type="text" v-model="userData.email" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
</form-group>
<form-group label="Gender" :error="formErrors.gender">
<select v-model="userData.gender" class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full">
<option value="M">M</option>
<option value="W">W</option>
<option value="D">D</option>
</select>
</form-group>
</div>
<div class="flex flex-col lg:flex-row space-y-2 lg:space-y-0 lg:space-x-2">
<button type="submit" class="button bg-button" :disabled="loading">
Registrieren
</button>
<router-link to="login" class="button bg-button bg-button--secondary">
Zum Login
</router-link>
</div>
</form>
</div>
</template>
w
<script>
import { mapState, mapActions, mapMutations } from 'vuex'
import FormGroup from './components/FormGroup'
export default {
components: {
FormGroup
},
data () {
return {
userData: {
lastName: '',
firstName: '',
email: '',
gender: 'M'
},
formErrors: {}
}
},
methods: {
...mapMutations(['alert']),
...mapActions(['register']),
performRegistration () {
this.message = '';
this.formErrors = {};
this.register(this.userData)
.then(response => {
if (response.success) {
this.alert({
message: 'Du erhältst in Kürze eine E-Mail mit einem Link zum (Zurück)setzen deines Passworts.',
success: true,
});
this.userData = {
lastName: '',
firstName: '',
email: '',
gender: 'M'
};
this.formErrors = {};
} else {
this.alert({
message: 'Du bist bereits registriert. Bitte nutze die Passwort vergessen Funktion.',
success: false,
});
}
}).catch(error => {
const violations = error.response.violations;
for (let violation of violations) {
this.$set(this.formErrors, violation.property_path, violation.message);
}
})
}
},
computed: {
...mapState(['loading'])
}
}
</script>
@@ -1,17 +0,0 @@
<template>
<div>
<p class="text-2xl pb-4">Teamer</p>
<div class="p-4 mb-8 text-white bg-ep-primary">
Ab sofort nutzen wir zur Teameinteilung ein neues Tool. Den Zugang erhältst du hier: <a class="underline" href="https://myep-team.ep-reisen.de/" target="_blank">https://myep-team.ep-reisen.de/</a>
<br>
Du kannst dich mit deinen bisherigen Log-In Daten anmelden und schonmal ein wenig zurechtfinden.
<br>
Ein detailliertes Tutorial folgt noch.
<br>
Wir freuen uns, dich in der neuen Saison einteilen zu dürfen 😊
</div>
</div>
</template>
<script>
</script>
@@ -1,24 +0,0 @@
const defaultOptions = {
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
}
function checkStatus(response) {
if (response.ok) {
return response
} else {
return response
.json()
.then(error => {
const responseError = new Error(error.message)
if (error.hasOwnProperty('violations')) {
responseError.violations = error.violations
}
throw responseError
})
}
}
export { checkStatus, defaultOptions }
@@ -1,61 +0,0 @@
<template>
<div class="relative" v-if="mutable && selectableAccommodations.length > 0">
<button class="button bg-button button--small w-full"
type="button"
@click.prevent="visible = !visible">
<span>
Zimmer
</span>
<svg class="w-4 h-4" fill="none" :class="{ 'rotate-180': visible }" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</button>
<ul class="absolute top-100 left-0 z-50 mt-4 border shadow-md p-4 bg-white w-screen max-w-xs divide-y" v-show="visible">
<li v-for="accommodation in selectableAccommodations" :key="accommodation.id" class="py-1">
<a href="#" @click.prevent="selectAccommodation(accommodation)">
{{ accommodation.name}}
</a>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'AccommodationSelect',
props: {
participant: {
type: Object,
required: true
},
contingents: {
type: Object,
required: true
},
mutable: {
type: Boolean,
default: false
}
},
data() {
return {
visible: false,
}
},
methods: {
selectAccommodation (accommodation) {
this.$emit('selected', accommodation);
}
},
computed: {
selectableAccommodations () {
let accommodations = []
for (let accommodationId of Object.keys(this.contingents)) {
if (this.participant.accommodationId !== parseInt(accommodationId)) {
accommodations.push(this.contingents[accommodationId])
}
}
this.visible = false
return accommodations
}
}
}
</script>
@@ -1,41 +0,0 @@
<template>
<div class="border border-zinc-200 rounded mb-4">
<div class="bg-zinc-50 p-2">
<h3 class="text-base font-bold">
<a role="button" href="#" class="flex items-center justify-between"
@click.prevent="expanded = !expanded">
<span>{{ title }}</span>
<svg class="w-6 h-6" fill="none" :class="{ 'rotate-180': expanded }" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</a>
</h3>
</div>
<div :class="{ 'hidden': !expanded }">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'CollapsiblePanel',
data() {
return {
id: null,
expanded: false,
}
},
props: {
parent: {
type: String,
required: true,
},
title: {
type: String,
required: true,
}
},
mounted() {
this.id = this._uid
}
}
</script>
@@ -1,26 +0,0 @@
<template>
<panel title="Gebuchte Kontingente">
<ul class="list-none">
<li class="p-2 border-b border-zinc-200 last:border-0" v-for="contingent in contingents" :key="contingent.id">
{{ contingent.room }}: {{ contingent.count }}
</li>
</ul>
</panel>
</template>
<script>
import Panel from './Panel'
export default {
name: 'ContingentsOverview',
components: {
Panel
},
props: {
contingents: {
type: Array,
required: true
}
}
}
</script>
@@ -1,27 +0,0 @@
<template>
<div class="mb-4">
<label class="block mb-1" :class="{ 'text-red-500': error }">
{{ label }}:
</label>
<slot></slot>
<span class="block text-sm mt-1 text-red-500" v-show="error">
{{ error }}
</span>
</div>
</template>
<script>
export default {
name: 'FormGroup',
props: {
label: {
type: String,
required: true
},
error: {
type: String,
default: null
}
}
}
</script>
@@ -1,42 +0,0 @@
<template>
<panel title="Änderungen möglich bis">
<ul class="list-none">
<li class="p-2 border-b border-zinc-200 last:border-0" v-for="field in fields" :key="field.prop">
<strong>
{{ field.label }}:
</strong>
<span class="px-2 py-1 text-white" :class="mutableFields[field.prop].mutable ? 'bg-emerald-500' : 'bg-red-500'">
{{ mutableFields[field.prop].mutable ? mutableFields[field.prop].until : 'keine Änderung möglich' }}
</span>
</li>
</ul>
</panel>
</template>
<script>
import Panel from './Panel';
export default {
name: 'MutableFields',
data() {
return {
fields: [
{ prop: 'accommodation', label: 'Unterkunft' },
{ prop: 'participantData', label: 'Teilnehmer' },
{ prop: 'services', label: 'Leistungen' },
{ prop: 'transportation', label: 'Beförderung' },
{ prop: 'pickup', label: 'Zustiege' },
]
}
},
components: {
Panel
},
props: {
mutableFields: {
type: Object,
required: true
}
}
}
</script>
@@ -1,33 +0,0 @@
<template>
<div class="border border-zinc-50 rounded mb-4">
<div class="bg-zinc-50 p-2">
<h3 class="text-base font-bold">
{{ title }}
</h3>
</div>
<template v-if="body">
<div class="bg-white p-2">
<slot></slot>
</div>
</template>
<template v-else>
<slot></slot>
</template>
</div>
</template>
<script>
export default {
name: 'Panel',
props: {
title: {
type: String,
required: true
},
body: {
type: Boolean,
default: false
}
}
}
</script>
@@ -1,53 +0,0 @@
<template>
<div class="relative" ref="target">
<button class="button bg-button button--small w-full"
type="button"
@click.prevent="visible = !visible">
<span>
Telefon
</span>
<svg class="w-4 h-4" fill="none" :class="{ 'rotate-180': visible }" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</button>
<div class="absolute top-100 left-0 z-50 mt-4 border shadow-md p-4 bg-white w-screen max-w-xs space-y-2" v-show="visible">
<input class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full"
:class="{'cursor-not-allowed': !mutable }"
type="text" placeholder="Mobil"
v-model="participant.data.phoneMobile" :disabled="!mutable"
@change="$emit('updated', participant)"
>
</div>
</div>
</template>
<script>
export default {
name: 'ParticipantAddressForm',
props: {
participant: {
type: Object,
required: true
},
countries: {
type: Array,
required: true
},
mutable: {
type: Boolean,
default: false
}
},
data () {
return {
visible: false,
}
},
mounted () {
let self = this
document.addEventListener('click', e => {
if (self.$refs.target !== undefined && self.$refs.target.contains(e.target) === false) {
self.visible = false
}
})
}
}
</script>
@@ -1,66 +0,0 @@
<template>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-1">
<input class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full"
:class="{'cursor-not-allowed': !mutable }"
type="text" placeholder="Nachname"
v-model="participant.data.lastName" :disabled="!mutable"
@change="$emit('updated', participant)"
>
<input class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full"
:class="{'cursor-not-allowed': !mutable }"
type="text" placeholder="Vorname"
v-model="participant.data.firstName" :disabled="!mutable"
@change="$emit('updated', participant)"
>
<input class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full"
:class="{'cursor-not-allowed': !mutable }"
type="email" placeholder="E-Mail"
v-model="participant.data.email" :disabled="!mutable"
@change="$emit('updated', participant)"
>
<select class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full"
:class="{'cursor-not-allowed': !mutable }"
v-model="participant.data.gender" :disabled="!mutable"
@change="$emit('updated', participant)"
>
<option value="W">weiblich</option>
<option value="M">männlich</option>
<option value="D">divers</option>
</select>
<select class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full"
:class="{'cursor-not-allowed': !mutable }"
v-model="participant.data.nationality" :disabled="!mutable"
@change="$emit('updated', participant)"
>
<option value="">Nationalität...</option>
<option v-for="country in countries" :key="country.code" :value="country.code">{{ country.adjective }}</option>
</select>
<input type="date"
class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 mt-1 w-full"
:class="{'cursor-not-allowed': !mutable }"
placeholder="Geburtsdatum"
v-model="participant.data.dateOfBirth" :disabled="!mutable"
@change="$emit('updated', participant)"
>
</div>
</template>
<script>
export default {
name: 'ParticipantDataForm',
props: {
participant: {
type: Object,
required: true
},
countries: {
type: Array,
required: true
},
mutable: {
type: Boolean,
default: false
}
}
}
</script>
@@ -1,87 +0,0 @@
<template>
<div class="relative" v-if="hasSelectedRentals" ref="target">
<button class="button bg-button button--small" @click.prevent="visible = !visible">
<span>
Größen
</span>
<svg class="w-4 h-4" fill="none" :class="{ 'rotate-180': visible }" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</button>
<div class="absolute top-100 left-0 z-50 mt-4 border shadow-md p-4 bg-white w-screen max-w-xs space-y-2" v-show="visible">
<input class="form-input w-full" type="number" placeholder="Körpergröße (150-210cm)"
v-model.number="participant.data.height" :disabled="!mutable"
@change="validate(participant.data.height, 150, 210, 'Körpergröße')"
>
<input class="form-input w-full" type="number" placeholder="Schuhgröße (36-49, EU-Größe)"
v-model.number="participant.data.shoeSize" :disabled="!mutable"
@change="validate(participant.data.shoeSize, 36, 49, 'Schuhgröße')"
>
<input class="form-input w-full" type="number" placeholder="Gewicht (40 - 130kg)"
v-model.number="participant.data.weight" :disabled="!mutable"
@change="validate(participant.data.weight, 40, 130, 'Gewicht')"
>
</div>
</div>
</template>
<script>
export default {
name: 'ParticipantSizesForm',
props: {
participant: {
type: Object,
required: true
},
services: {
type: Object,
required: true
},
subTypes: {
type: Array,
default() {
return ['VER', 'VE2', 'VE3', 'VE4', 'VE5', 'VE6', 'VE7', 'VE8']
}
},
mutable: {
type: Boolean,
default: false
}
},
data () {
return {
visible: false,
}
},
methods: {
validate (value, min, max, label) {
let number = parseInt(value)
if (isNaN(number) || number < min || number > max) {
alert(`Bitte einen Wert zwischen ${min} und ${max} für ${label} eingeben`)
}
this.$emit('updated', this.participant)
},
},
computed: {
hasSelectedRentals () {
let result = false;
for (let id of this.participant.serviceIds) {
if (typeof this.services[id] === 'undefined') {
continue;
}
if (this.subTypes.includes(this.services[id].subType)) {
result = true;
}
}
return result
}
},
mounted () {
let self = this
document.addEventListener('click', e => {
if (self.$refs.target !== undefined && self.$refs.target.contains(e.target) === false) {
self.visible = false
}
})
}
}
</script>
@@ -1,66 +0,0 @@
<template>
<div class="relative" ref="target" v-show="hasSelectedBus">
<button class="button bg-button button--small" @click.prevent="visible = !visible">
<span>
Zustieg
</span>
<svg class="w-4 h-4" fill="none" :class="{ 'rotate-180': visible }" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</button>
<div class="absolute top-100 left-0 z-50 mt-4 border shadow-md p-4 bg-white w-screen max-w-xs" v-show="visible">
<div class="py-1" v-for="pickup in pickups" :key="pickup.id">
<label class="inline-flex items-center text-sm">
<input type="radio"
class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 text-ep-secondary sbw:text-sbw-secondary"
:name="'pickup-' + participant.data.id"
v-model="pickupId"
:value="pickup.busproId"
:disabled="!mutable"
@change="$emit('selected', pickupId)"
>
<span class="ml-2">{{ pickup.city }} ({{ pickup.street }})</span>
</label>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'PickupSelect',
props: {
pickups: {
type: Object,
required: true
},
participant: {
type: Object,
required: true
},
mutable: {
type: Boolean,
default: false
}
},
data () {
return {
visible: false,
pickupId: this.participant.pickupId
}
},
computed: {
hasSelectedBus () {
return this.participant.transportations.some((transportation) => {
return 'BUS' === transportation.subType && 'HIN' === transportation.direction
})
},
},
mounted () {
let self = this
document.addEventListener('click', e => {
if (self.$refs.target !== undefined && self.$refs.target.contains(e.target) === false) {
self.visible = false
}
})
}
}
</script>
@@ -1,98 +0,0 @@
<template>
<div class="relative" v-if="hasServices" ref="target">
<button class="button bg-button button--small" type="button" @click.prevent="visible = !visible">
<span>
{{ subType.label }}
</span>
<svg class="w-4 h-4" fill="none" :class="{ 'rotate-180': visible }" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</button>
<div class="absolute top-100 left-0 z-50 mt-1 border shadow-md p-4 bg-white w-screen max-w-xs" v-show="visible">
<div class="text-right">
<button type="button" @click="visible = false">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
<div class="py-1" v-for="service in availableServices" :key="service.id">
<label class="inline-flex items-center text-sm">
<input type="checkbox"
class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 text-ep-secondary sbw:text-sbw-secondary"
v-model="participant.serviceIds"
:value="service.id"
:disabled="false === isServiceSelectable(service)"
@change="selectService(service, $event)"
>
<span class="ml-2">{{ service.name }} ({{ service.price }})</span>
</label>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ServiceSelect',
props: {
subType: {
type: Object,
required: true
},
services: {
type: Object,
required: true
},
participant: {
type: Object,
required: true
},
mutable: {
type: Boolean,
default: false
}
},
data () {
return {
visible: false,
}
},
methods: {
selectService(service, event) {
const action = event.target.checked ? 'selected' : 'unselected';
this.$emit('updated', { action, participant: this.participant, service });
},
isServiceSelectable(service) {
// Service is mandatory so check current selection state
if (service.mandatory) {
return this.participant.serviceIds.indexOf(service.id) === -1
}
// Service is mutable and available
return true === this.mutable && service.available > 0;
}
},
computed: {
availableServices () {
let filteredServices = {};
for (let id of Object.keys(this.services)) {
if (this.services[id].subType === undefined) {
continue;
}
if (!this.subType.codes.includes(this.services[id].subType)) {
continue;
}
filteredServices[id] = this.services[id];
}
return filteredServices;
},
hasServices () {
return Object.keys(this.availableServices).length > 0;
}
},
mounted () {
let self = this
document.addEventListener('click', e => {
if (self.$refs.target !== undefined && self.$refs.target.contains(e.target) === false) {
self.visible = false
}
})
}
}
</script>
@@ -1,57 +0,0 @@
<template>
<panel title="Gebuchte Zusatzleistungen">
<ul class="list-none" v-show="hasServiceAssignments">
<li class="p-2 border-b border-zinc-200 last:border-0" v-for="(assignment, serviceId) in serviceAssignments">
{{ services[serviceId].name }}: {{ assignment.length }}
</li>
</ul>
<div class="bg-white p-2" v-show="!hasServiceAssignments">
<span>keine</span>
</div>
</panel>
</template>
<script>
import Panel from './Panel'
export default {
name: 'ServicesOverview',
components: {
Panel
},
props: {
services: {
type: Object,
required: true
},
participantData: {
type: Array,
required: true
}
},
computed: {
serviceAssignments () {
let assignments = {};
for (let participant of this.participantData) {
for (let id of participant.serviceIds) {
if (typeof this.services[id] === 'undefined') {
continue;
}
if (!assignments.hasOwnProperty(id)) {
assignments[id] = [];
}
assignments[id].push(participant.data.id);
}
}
return assignments;
},
hasServiceAssignments() {
return Object.keys(this.serviceAssignments).length > 0 && this.serviceAssignments.constructor === Object
}
}
}
</script>
<style scoped>
</style>
@@ -1,76 +0,0 @@
<template>
<div class="relative" ref="target">
<button class="button bg-button button--small" @click.prevent="visible = !visible">
<span>
Anreise
</span>
<svg class="w-4 h-4" fill="none" :class="{ 'rotate-180': visible }" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div class="absolute top-100 left-0 z-50 mt-4 border shadow-md p-4 bg-white w-screen max-w-xs" v-show="visible">
<div class="flex flex-col space-y-2" v-if="hasServices">
<label class="flex items-center"
v-for="service in services" :key="service.id">
<input type="checkbox"
class="border-zinc-400 focus:border-zinc-800 ring-0 focus:ring-0 text-ep-secondary sbw:text-sbw-secondary"
v-model="selectedTransportationIds"
:value="service.id"
:disabled="isServiceUnavailable(service) || !mutable"
@change="selectService(service, $event)"/>
<span class="ml-2">{{ service.name }}</span>
</label>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'TransportationSelect',
props: {
services: {
type: Object,
required: true
},
participant: {
type: Object,
required: true,
},
mutable: {
type: Boolean,
default: false
}
},
data () {
return {
visible: false,
selectedTransportationIds: this.participant.transportations.map((transportation) => transportation.busproId),
}
},
methods: {
selectService(service, event) {
// uncheck transportation ids with same direction to make them unique
let direction = service.direction
let serviceIdsToUncheck = Object.entries(this.services).filter(item => direction === item[1].direction && item[1].id !== service.id).map(item => item[1].id)
this.selectedTransportationIds = this.selectedTransportationIds.filter(id => !serviceIdsToUncheck.includes(id))
this.participant.transportations = this.selectedTransportations
let action = event.target.checked ? 'select' : 'unselect';
this.$emit('updated', { action, service });
},
isServiceUnavailable(service) {
return service.available === 0
}
},
computed: {
hasServices() {
return Object.keys(this.services).length > 0;
},
selectedTransportations() {
return this.selectedTransportationIds.map((id) => this.services[id])
},
}
}
</script>
@@ -1,99 +0,0 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import store from '../store';
Vue.use(VueRouter);
const guard = (to, from, next) => {
if (store.getters.isLoggedIn === false) {
next({ name: 'login' });
} else if (to.meta.requiresTeamer && store.getters.isTeamer === false) {
next(false);
} else {
next();
}
};
import Address from '../Address'
import Events from '../Events'
import Login from '../Login'
import CrmData from '../CrmData'
import Newsletter from '../Newsletter'
import Registration from '../Registration'
// Lazy load some components when they become required
const Teamer = () => import('../Teamer')
const BookingEdit = () => import('../BookingEdit')
export default new VueRouter({
routes: [
{
name: 'login',
path: '/',
component: Login,
beforeEnter (from, to, next) {
if (store.getters.isLoggedIn === false) {
next();
} else {
next({ name: 'address' });
}
}
},
{
name: 'register',
path: '/register',
component: Registration,
beforeEnter (from, to, next) {
if (store.getters.isLoggedIn === false) {
next();
} else {
next({ name: 'address' });
}
}
},
{
name: 'address',
path: '/adressdaten',
component: Address,
beforeEnter: guard
},
{
name: 'newsletter',
path: '/newsletter',
component: Newsletter,
beforeEnter: guard
},
{
name: 'profile',
path: '/profil',
component: CrmData,
beforeEnter: guard
},
{
name: 'events',
path: '/vorgaenge',
component: Events,
beforeEnter: guard,
},
{
name: 'bookingEdit',
path: '/buchung/:id',
component: BookingEdit,
beforeEnter: guard
},
{
name: 'teamer',
path: '/teamer',
component: Teamer,
meta: { requiresTeamer: true },
beforeEnter: guard
},
{
path: '*',
redirect: { name: 'login' }
}
],
scrollBehavior (to, from, savedPosition) {
return { selector: '#myep', offset: { x: 0 , y: 160 } }
}
});
@@ -1,263 +0,0 @@
import Vue from 'vue'
import Vuex from 'vuex'
import { checkStatus, defaultOptions } from '../api'
const configElement = document.getElementById('appconfig')
const config = JSON.parse(configElement.innerHTML)
Vue.use(Vuex)
const defaultUserData = {
isTeamer: false,
isGroupManager: false,
email: '',
firstName: '',
lastName: '',
gender: '',
dateOfBirth: null,
street: '',
zipCode: '',
city: '',
phoneMobile: '',
phonePrivate: '',
newsletter: false,
};
export default new Vuex.Store({
state: {
config: {},
loading: false,
statusMessage: '',
authenticated: false,
userData: defaultUserData,
countries: [],
alert: {
message: '',
success: true,
}
},
mutations: {
initConfig(state) {
state.config = config
},
setUserData(state, data) {
if (state.userData.watchList) {
data.watchList = [...state.userData.watchList, ...data.watchList]
}
state.userData = data
},
login(state) {
state.authenticated = true
},
logout(state) {
state.userData = defaultUserData;
state.authenticated = false
},
alert(state, payload) {
state.alert = payload;
},
beginLoading(state, statusMessage) {
state.loading = true
state.statusMessage = statusMessage
state.alert = {message: '', success: true}
},
endLoading(state) {
state.loading = false
state.statusMessage = ''
},
setCountries(state, countries) {
state.countries = countries;
}
},
actions: {
checkLoginState({commit, state}) {
commit('beginLoading')
const options = {
...defaultOptions
}
return fetch(`${state.config.myEpApiBaseUrl}/api/ping`, options)
.then(checkStatus)
.then(response => response.json())
.then(() => {
commit('login')
})
.catch(() => {})
.finally(() => {
commit('endLoading')
})
},
loadCountries({commit, state}) {
commit('beginLoading')
const options = {
...defaultOptions
}
return fetch(`${state.config.myEpApiBaseUrl}/api/countries`, options)
.then(checkStatus)
.then(response => response.json())
.then(data => {
commit('setCountries', data)
})
.catch(error => {
commit('alert', {
message: 'Es ist ein Fehler aufgetreten: ' + error.message,
success: false,
})
})
.finally(() => {
commit('endLoading')
})
},
login({commit}, credentials) {
commit('beginLoading')
const options = {
...defaultOptions,
method: 'POST',
body: JSON.stringify({
username: credentials.email,
password: credentials.password
})
}
return fetch(`${config.myEpApiBaseUrl}/api/login`, options)
.then(checkStatus)
.then(response => response.json())
.then(data => {
commit('login', data)
})
.catch(error => {
commit('logout')
commit('alert', {
message: 'Der Login ist fehlgeschlagen :(',
success: false,
});
throw error
})
.finally(() => {
commit('endLoading')
})
},
logout({commit}) {
commit('beginLoading')
const options = {
...defaultOptions
}
return fetch(`${config.myEpApiBaseUrl}/api/logout`, options)
.then(checkStatus)
.catch(() => {})
.finally(() => {
commit('logout')
commit('endLoading')
})
},
resetPassword({commit}, email) {
commit('beginLoading')
const options = {
...defaultOptions,
method: 'POST',
body: JSON.stringify({
email
})
}
return fetch(`${config.myEpApiBaseUrl}/api/reset-password`, options)
.then(checkStatus)
.then(response => response.json())
.then(response => response)
.catch(error => {
commit('alert', {
message: 'Es ist ein Fehler aufgetreten: ' + error.message,
success: false,
})
})
.finally(() => {
commit('endLoading')
})
},
register({commit}, userData) {
commit('beginLoading')
const options = {
...defaultOptions,
method: 'POST',
body: JSON.stringify(userData)
}
return fetch(`${config.myEpApiBaseUrl}/api/register`, options)
.then(checkStatus)
.then(response => response.json())
.then(data => data)
.catch(error => {
commit('alert', {
message: 'Es ist ein Fehler aufgetreten: ' + error.message,
success: false,
})
throw error
})
.finally(() => {
commit('endLoading')
})
},
loadAddress({commit}) {
commit('beginLoading')
const options = {
...defaultOptions,
}
return fetch(`${config.myEpApiBaseUrl}/api/address`, options)
.then(checkStatus)
.then(response => response.json())
.then(data => {
commit('setUserData', data)
if (data.watchlist) {
commit('setWatchlist', data.watchlist)
}
})
.catch(error => {
commit('alert', {
message: 'Es ist ein Fehler aufgetreten: ' + error.message,
success: false,
})
})
.finally(() => {
commit('endLoading')
})
},
saveAddress({commit, state}, data) {
commit('beginLoading')
const options = {
...defaultOptions,
method: 'PUT',
body: JSON.stringify(data)
}
return fetch(`${config.myEpApiBaseUrl}/api/address`, options)
.then(checkStatus)
.then(response => response.json())
.then(() => {
commit('alert', {
message: 'Die Änderungen wurden gespeichert',
success: true,
})
})
.finally(() => {
commit('endLoading')
})
}
},
getters: {
onWatchlist: state => uid => state.userData.watchList.indexOf(uid) !== -1,
isLoggedIn: state => !!state.authenticated,
isTeamer: state => !!state.userData.isTeamer,
isGroupManager: state => !!state.userData.isGroupManager,
}
});
@@ -6,7 +6,7 @@
<a href="#main-content" class="hidden lg:block fixed left-0 top-0 bg-ep-primary-dark text-white py-1 px-4 z-50 transform focus:translate-y-12 transition">
zum Inhalt springen
</a>
<f:if condition="{settings.hideAreaNavigation}">
<f:else>
<div class="topnav">
@@ -37,7 +37,7 @@
</div>
</f:if>
<f:if condition="{settings.myEpPageUid}">
<f:link.typolink parameter="{settings.myEpPageUid}" class="topnav__item" title="My E&amp;P">
<f:link.typolink parameter="{settings.myEpUrl}" class="topnav__item" title="My E&amp;P">
{ep:icon(icon: 'user', class: 'w-5 h-5')}
</f:link.typolink>
</f:if>
-1
View File
@@ -9,7 +9,6 @@ Encore
.setOutputPath('./public/typo3conf/ext/ep_theme/Resources/Public/')
.setPublicPath('/typo3conf/ext/ep_theme/Resources/Public/')
.addEntry('main', './public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/main.js')
.addEntry('myep', './public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/myep.js')
.addEntry('groups-calculator', './public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/groups-calculator.js')
.addStyleEntry('rte', './public/typo3conf/ext/ep_theme/Resources/Private/Assets/scss/rte.scss')
.splitEntryChunks()