Integrate booking edit into myep spa
This commit is contained in:
committed by
Björn Fromme
parent
213b75b863
commit
dbe4336550
Generated
+3422
-2750
File diff suppressed because it is too large
Load Diff
+15
-13
@@ -7,41 +7,43 @@
|
||||
"repository": "https://gitlab.com/dreipunktnull/ep_theme.git",
|
||||
"scripts": {
|
||||
"dev": "./node_modules/.bin/encore dev --watch",
|
||||
"build": "./node_modules/.bin/encore production"
|
||||
"build": "NODE_ENV=production ./node_modules/.bin/encore production"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fancyapps/fancybox": "^3.5.7",
|
||||
"@tailwindcss/custom-forms": "^0.2.1",
|
||||
"axios": "^0.18.0",
|
||||
"axios-auth-refresh": "^1.0.7",
|
||||
"bootstrap-sass": "^3.4.1",
|
||||
"bourbon": "^4.3.2",
|
||||
"cookieconsent": "^3.1.0",
|
||||
"dayjs": "^1.8.12",
|
||||
"flatpickr": "^4.5.7",
|
||||
"dayjs": "^1.8.36",
|
||||
"flatpickr": "^4.6.6",
|
||||
"font-awesome": "^4.7.0",
|
||||
"jquery": "^1.12.4",
|
||||
"lazysizes": "^4.1.8",
|
||||
"leaflet": "^1.4.0",
|
||||
"leaflet": "^1.7.1",
|
||||
"normalize.css": "^7.0.0",
|
||||
"picturefill": "^3.0.3",
|
||||
"sass-mq": "^3.3.2",
|
||||
"slick-carousel": "^1.8.0",
|
||||
"sticky-js": "^1.2.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue-router": "^3.0.6",
|
||||
"sticky-js": "^1.3.0",
|
||||
"tailwindcss": "^1.8.10",
|
||||
"vue": "^2.6.12",
|
||||
"vue-router": "^3.4.3",
|
||||
"vue-session": "^1.0.0",
|
||||
"vuex": "^3.1.0"
|
||||
"vuex": "^3.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@symfony/webpack-encore": "^0.28.2",
|
||||
"@symfony/webpack-encore": "^0.28.3",
|
||||
"autoprefixer": "^8.6.5",
|
||||
"node-sass": "^4.11.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"sass-loader": "^7.3.1",
|
||||
"vue-loader": "^15.8.3",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
"vue-loader": "^15.9.3",
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
parser: false,
|
||||
plugins: {
|
||||
'autoprefixer': {},
|
||||
}
|
||||
plugins: [
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div id="myep" class="relative">
|
||||
<nav class="navbar navbar-default" v-if="isLoggedIn">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed"
|
||||
@@ -16,7 +16,7 @@
|
||||
<router-link :to="{ name: 'address' }" tag="li" active-class="active" :class="{ disabled: loading }"><a>Adressdaten</a></router-link>
|
||||
<router-link :to="{ name: 'newsletter' }" tag="li" active-class="active" :class="{ disabled: loading }"><a>Newsletter</a></router-link>
|
||||
<router-link :to="{ name: 'profile' }" tag="li" active-class="active" :class="{ disabled: loading }"><a>Mein Profil</a></router-link>
|
||||
<router-link :to="{ name: 'events' }" tag="li" active-class="active" :class="{ disabled: loading }"><a>Reise-Historie</a></router-link>
|
||||
<router-link :to="{ name: 'events' }" tag="li" active-class="active" :class="{ disabled: loading }"><a>Buchungen</a></router-link>
|
||||
<router-link :to="{ name: 'teamer' }" tag="li" active-class="active" :class="{ disabled: loading }" v-if="isTeamer"><a>Teamer</a></router-link>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
@@ -34,8 +34,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loader" v-show="loading">
|
||||
<img :src="config.loaderUrl" alt="">
|
||||
<div class="absolute top-0 left-0 w-full h-full z-100 bg-overlay-white" v-show="loading">
|
||||
<img :src="config.loaderUrl" alt="" class="absolute top-1/2 left-1/2 transform -translate-xy-1/2">
|
||||
</div>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
@@ -53,7 +53,7 @@
|
||||
logout () {
|
||||
this.$store.dispatch('logout')
|
||||
.then(() => {
|
||||
this.$router.push({ name: 'login' })
|
||||
this.$router.replace({ name: 'login' })
|
||||
}).catch(error => {});
|
||||
}
|
||||
},
|
||||
@@ -63,30 +63,9 @@
|
||||
},
|
||||
mounted () {
|
||||
if (this.isLoggedIn) {
|
||||
this.$router.push({ name: 'address' })
|
||||
this.$router.replace({ name: 'address' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#app {
|
||||
position: relative;
|
||||
}
|
||||
.loader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.75);
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<div class="grid lg:grid-cols-2 gap-4" v-if="initialized">
|
||||
<div>
|
||||
<panel title="Reisedaten">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item"><strong>Reise:</strong> {{ travelData.productName }}</li>
|
||||
<li class="list-group-item"><strong>Zeitraum:</strong> {{ travelData.dateFrom }} - {{ travelData.dateTo }}</li>
|
||||
<li class="list-group-item"><strong>Inklusivleistungen:</strong> {{ travelData.servicesIncluded.join(', ') }}</li>
|
||||
<li class="list-group-item"><strong>Anmelder:</strong> {{ bookingData.applicant.firstName }} {{ bookingData.applicant.lastName }}</li>
|
||||
<li class="list-group-item"><strong>Gesamtpreis:</strong> {{ totalPrice.toFixed(2) }}€</li>
|
||||
<li class="list-group-item"><strong>Teilnehmer:</strong> {{ participantsCount.active }}</li>
|
||||
<li class="list-group-item"><strong>Stornos:</strong> {{ participantsCount.canceled }}</li>
|
||||
</ul>
|
||||
</panel>
|
||||
<services-overview :services="services.other" :participant-data="participantData"/>
|
||||
</div>
|
||||
<div>
|
||||
<mutable-fields :mutable-fields="mutableFields"></mutable-fields>
|
||||
<contingents-overview :contingents="contingents"/>
|
||||
<panel title="Beförderungsart (alle Teilnehmer)" :body="true">
|
||||
<transportation-select :services="services.transportation"
|
||||
:initial-transportation-ids="bookingData.selectedTransportationIds"
|
||||
:mutable="mutableFields.transportation.mutable"
|
||||
v-on:updated="onTransportationUpdated($event)"
|
||||
/>
|
||||
</panel>
|
||||
<panel title="Anmerkungen" :body="true">
|
||||
<textarea class="form-textarea block w-full" rows="2" v-model="bookingData.remarks" @change="dirty = true"/>
|
||||
</panel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-4 lg:flex lg:justify-between space-y-1 lg:space-y-0" v-show="!loading">
|
||||
<button class="btn btn-primary" :disabled="!dirty" @click.prevent="storeBookingData()" v-if="initialized">
|
||||
Buchung aktualisieren
|
||||
</button>
|
||||
<router-link :to="{ name: 'events' }" class="btn btn-primary">
|
||||
zur Übersicht
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="py-4" v-if="initialized">
|
||||
<h2>Teilnehmer</h2>
|
||||
<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="panel-body-section">
|
||||
<participant-data-form :participant="participant"
|
||||
:mutable="mutableFields.participantData.mutable"
|
||||
:countries="countries"
|
||||
v-on:updated="onParticipantDataUpdated($event, index)"
|
||||
/>
|
||||
</div>
|
||||
<div class="lg:flex lg:space-x-1 space-y-1 lg:space-y-0 py-4">
|
||||
<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"
|
||||
v-on:selected="onAccommodationSelected($event, index)"
|
||||
/>
|
||||
<service-select v-for="subType in servicesSubTypes"
|
||||
:key="subType.code"
|
||||
: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)]"
|
||||
/>
|
||||
<pickup-select
|
||||
:pickups="services.pickups"
|
||||
:participant="participant"
|
||||
:mutable="mutableFields.transportation.mutable"
|
||||
v-on:selected="onPickupSelected($event, index)"
|
||||
v-show="showPickups"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid lg:grid-cols-2 gap-4">
|
||||
<table class="table table-condensed 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>
|
||||
<th>Gesamtpreis</th>
|
||||
<td>{{ calculateSinglePrice(participant).toFixed(2) }} €</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<textarea rows="2" class="form-textarea block w-full"
|
||||
v-model="participant.data.accommodationRequest"
|
||||
@change="dirty = true"
|
||||
placeholder="Anmerkungen"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</collapsible-panel>
|
||||
</div>
|
||||
<div class="py-4 lg:flex lg:justify-between space-y-1 lg:space-y-0" v-show="!loading">
|
||||
<button class="btn btn-primary" :disabled="!dirty" @click.prevent="storeBookingData()" v-if="initialized">
|
||||
Buchung aktualisieren
|
||||
</button>
|
||||
<router-link :to="{ name: 'events' }" class="btn btn-primary">
|
||||
zur Übersicht
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="py-4 text-center" v-show="statusMessage" v-text="statusMessage" />
|
||||
<div class="modal fade" tabindex="-1" role="dialog" ref="modal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{ modal.title }}</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p v-html="modal.message"/>
|
||||
</div>
|
||||
</div>
|
||||
</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 authenticatedClient from "../api";
|
||||
import {mapMutations, mapState} from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'BookingEdit',
|
||||
components: {
|
||||
Panel,
|
||||
CollapsiblePanel,
|
||||
MutableFields,
|
||||
TransportationSelect,
|
||||
ServicesOverview,
|
||||
ContingentsOverview,
|
||||
AccommodationSelect,
|
||||
ParticipantDataForm,
|
||||
ParticipantAddressForm,
|
||||
ParticipantSizesForm,
|
||||
PickupSelect,
|
||||
ServiceSelect
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
bookingId: null,
|
||||
initialized: false,
|
||||
dirty: false,
|
||||
processing: false,
|
||||
statusMessage: '',
|
||||
bookingData: null,
|
||||
participantData: null,
|
||||
invalidParticipantData: {},
|
||||
eventData: null,
|
||||
travelData: null,
|
||||
mutableFields: null,
|
||||
contingents: null,
|
||||
services: null,
|
||||
servicesSubTypes: [
|
||||
{ 'code': 'KUR', 'label': 'Kurse' },
|
||||
{ 'code': 'SON', 'label': 'Zusatzleistungen' },
|
||||
{ 'code': 'SPA', 'label': 'Skipass' },
|
||||
{ 'code': 'VPF', 'label': 'Verpflegung' },
|
||||
{ 'code': 'VER', 'label': 'Verleih' },
|
||||
{ 'code': 'EIN', 'label': '???' }
|
||||
],
|
||||
modal: {
|
||||
title: 'Hinweis',
|
||||
message: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['alert', 'beginLoading', 'endLoading']),
|
||||
loadFormOptions () {
|
||||
this.beginLoading();
|
||||
return authenticatedClient({
|
||||
url: '/api/countries',
|
||||
}).then(response => {
|
||||
this.countries = response.data
|
||||
}).catch(error => {
|
||||
}).finally(() => {
|
||||
this.endLoading()
|
||||
});
|
||||
},
|
||||
showModal (message, title = 'Hinweis') {
|
||||
this.modal = {
|
||||
message: message,
|
||||
title: title
|
||||
};
|
||||
$(this.$refs.modal).modal('show');
|
||||
},
|
||||
loadBookingData () {
|
||||
this.beginLoading()
|
||||
this.statusMessage = 'Lade Reise- und Buchungsdaten';
|
||||
return authenticatedClient.get(`/api/booking/${this.bookingId}`)
|
||||
.then(response => {
|
||||
this.bookingData = response.data.bookingData;
|
||||
this.participantData = response.data.participantData;
|
||||
this.travelData = response.data.travelData;
|
||||
})
|
||||
.catch(() => {
|
||||
throw new Error('Reise- und Buchungsdaten konnten nicht geladen werden.');
|
||||
})
|
||||
.finally(() => {
|
||||
this.statusMessage = '';
|
||||
this.endLoading()
|
||||
})
|
||||
},
|
||||
loadEventData () {
|
||||
this.beginLoading()
|
||||
this.statusMessage = 'Lade Vorgangsdaten';
|
||||
return authenticatedClient.get(`/api/event/${this.bookingData.bookingNumber}`)
|
||||
.then(response => {
|
||||
this.eventData = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
throw new Error(error.response.data.message);
|
||||
})
|
||||
.finally(() => {
|
||||
this.statusMessage = '';
|
||||
this.endLoading()
|
||||
})
|
||||
},
|
||||
loadMutableFields () {
|
||||
this.beginLoading()
|
||||
this.statusMessage = 'Lade mögliche Änderungen';
|
||||
return authenticatedClient.get(`/api/mutable-fields/${this.travelData.busproId}`)
|
||||
.then(response => {
|
||||
this.mutableFields = response.data;
|
||||
})
|
||||
.catch(() => {
|
||||
throw new Error('Mögliche Änderungen konnten nicht geladen werden.');
|
||||
})
|
||||
.finally(() => {
|
||||
this.statusMessage = '';
|
||||
this.endLoading()
|
||||
})
|
||||
},
|
||||
loadContingents () {
|
||||
this.beginLoading()
|
||||
this.statusMessage = 'Lade Kontingente';
|
||||
return authenticatedClient.get(`/api/contingents/${this.travelData.busproId}`)
|
||||
.then(response => {
|
||||
this.contingents = response.data;
|
||||
})
|
||||
.catch(() => {
|
||||
throw new Error('Kontingente konnten nicht geladen werden.');
|
||||
})
|
||||
.finally(() => {
|
||||
this.statusMessage = '';
|
||||
this.endLoading()
|
||||
})
|
||||
},
|
||||
loadServices () {
|
||||
this.beginLoading()
|
||||
this.statusMessage = 'Lade Leistungen';
|
||||
return authenticatedClient.get(`/api/services/${this.travelData.busproId}`)
|
||||
.then(response => {
|
||||
this.services = response.data;
|
||||
})
|
||||
.catch(() => {
|
||||
throw new Error('Leistungen konnten nicht geladen werden.');
|
||||
})
|
||||
.finally(() => {
|
||||
this.statusMessage = '';
|
||||
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;
|
||||
}
|
||||
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 factor = event.action === 'select' ? -1 : 1;
|
||||
this.services.transportation[event.service.id].available += this.participantData.length * factor;
|
||||
this.bookingData.selectedTransportationIds = event.selectedTransportationIds;
|
||||
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 = {};
|
||||
for (let participant of this.participantData) {
|
||||
let participantId = participant.data.id;
|
||||
if (!participant.data.nationality) {
|
||||
this.invalidParticipantData[participantId] = {
|
||||
invalid: 'Nationalität',
|
||||
};
|
||||
messages.push('Teilnehmer #'+participantId+': Nationalität')
|
||||
}
|
||||
if (!this.hasSelectedRentals(participant)) {
|
||||
continue;
|
||||
}
|
||||
if (!participant.data.height) {
|
||||
this.invalidParticipantData[participantId] = {
|
||||
invalid: 'Körpergröße',
|
||||
};
|
||||
messages.push('Teilnehmer #'+participantId+': Körpergröße')
|
||||
}
|
||||
if (!participant.data.shoeSize) {
|
||||
this.invalidParticipantData[participantId] = {
|
||||
invalid: 'Schuhgröße',
|
||||
};
|
||||
messages.push('Teilnehmer #'+participantId+': Schuhgröße')
|
||||
}
|
||||
}
|
||||
return messages
|
||||
},
|
||||
storeBookingData() {
|
||||
let messages = this.validateInput();
|
||||
if (false === this.dataValid) {
|
||||
this.showModal(messages.join('<br>'), 'Daten unvollständig');
|
||||
return;
|
||||
}
|
||||
this.beginLoading()
|
||||
authenticatedClient.put(`/api/booking/${this.bookingId}`, {
|
||||
participantData: this.participantData,
|
||||
selectedTransportations: this.selectedTransportations,
|
||||
remarks: this.bookingData.remarks
|
||||
}).then(response => {
|
||||
this.showModal(response.data.message);
|
||||
this.dirty = false;
|
||||
}).catch(error => {
|
||||
this.showModal(error.response.data.message)
|
||||
}).finally(() => {
|
||||
this.endLoading()
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['loading']),
|
||||
availableContingents () {
|
||||
let filteredContingents = {};
|
||||
for (let id of Object.keys(this.contingents)) {
|
||||
if (this.contingents[id].available > 0) {
|
||||
filteredContingents[id] = this.contingents[id];
|
||||
}
|
||||
}
|
||||
return filteredContingents;
|
||||
},
|
||||
selectedTransportations () {
|
||||
let selectedTransportations = [];
|
||||
for (let id of this.bookingData.selectedTransportationIds) {
|
||||
selectedTransportations.push(this.services.transportation[id]);
|
||||
}
|
||||
return selectedTransportations;
|
||||
},
|
||||
showPickups () {
|
||||
for (let id of this.bookingData.selectedTransportationIds) {
|
||||
if ('BUS' === this.services.transportation[id].subType) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
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.loadFormOptions()
|
||||
.then(() => this.loadBookingData())
|
||||
.then(() => this.loadEventData())
|
||||
.then(() => this.loadMutableFields())
|
||||
.then(() => this.loadContingents())
|
||||
.then(() => this.loadServices())
|
||||
.then(() => {
|
||||
this.initialized = true;
|
||||
})
|
||||
.catch(error => {
|
||||
this.showModal(error.message, 'Fehler');
|
||||
})
|
||||
.finally(() => {
|
||||
this.statusMessage = '';
|
||||
this.endLoading()
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,18 +1,17 @@
|
||||
<template>
|
||||
<div id="myep-main" v-show="!loading">
|
||||
<h1>Reise-Historie</h1>
|
||||
<h1>Meine Buchungen</h1>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Buchungsdatum</th>
|
||||
<th>Reisedatum</th>
|
||||
<th>Reise</th>
|
||||
<th>Teilnehmer</th>
|
||||
<th class="w-1/4">Reise</th>
|
||||
<th>Preis</th>
|
||||
<th>offen</th>
|
||||
<th>Status</th>
|
||||
<th>Dokumente</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -20,19 +19,19 @@
|
||||
<td>{{ formatBookingDate(event.dateBooking) }}</td>
|
||||
<td>{{ event.dateTravel | date }}</td>
|
||||
<td>{{ event.product }}</td>
|
||||
<td>
|
||||
<ul class="list-unstyled">
|
||||
<li v-for="customer in event.customers">{{ customer.lastName }}, {{ customer.firstName }}</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td style="white-space: nowrap">{{ event.price }} €</td>
|
||||
<td style="white-space: nowrap">{{ calculateBalance(event) }}</td>
|
||||
<td>{{ status[event.status] }}</td>
|
||||
<td>
|
||||
<a :href="getDownloadUrl(event, 'confirmation')" target="_blank">Bestätigung</a>
|
||||
<template v-if="event.documents">
|
||||
<a :href="getDownloadUrl(event, 'documents')" target="_blank">Reiseinfos</a>
|
||||
</template>
|
||||
<td class="flex space-x-1">
|
||||
<a :href="getDownloadUrl(event, 'confirmation')" target="_blank" class="btn btn-primary btn-sm">
|
||||
Bestätigung
|
||||
</a>
|
||||
<a :href="getDownloadUrl(event, 'documents')" target="_blank" class="btn btn-primary btn-sm" v-if="event.documents">
|
||||
Reiseinfos
|
||||
</a>
|
||||
<router-link :to="{ name: 'bookingEdit', params: { id: event.id } }" v-if="isGroupManager && event.editable" class="btn btn-primary btn-sm">
|
||||
bearbeiten
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -42,7 +41,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import {mapState, mapMutations, mapGetters} from 'vuex'
|
||||
import authenticatedClient from "../api";
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/de'
|
||||
@@ -103,7 +102,8 @@
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['config', 'loading'])
|
||||
...mapState(['config', 'loading']),
|
||||
...mapGetters(['isGroupManager'])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="relative" v-if="mutable">
|
||||
<button class="btn btn-primary btn-sm w-full lg:w-auto" type="button" @click.prevent="visible = !visible">
|
||||
Zimmer <span class="glyphicon" :class="{ 'glyphicon-chevron-up': visible, 'glyphicon-chevron-down': !visible }"></span>
|
||||
</button>
|
||||
<ul class="absolute top-100 left-0 z-10 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>
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="panel">
|
||||
<div class="panel-heading" role="tab" :id="'heading-' + id">
|
||||
<h3 class="panel-title">
|
||||
<a role="button" data-toggle="collapse" :data-parent="parent" :href="'#collapse-' + id"
|
||||
aria-expanded="false" :aria-controls="'collapse-' + id">
|
||||
{{ title }}
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div :id="'collapse-' + id" class="panel-collapse collapse" role="tabpanel" :aria-labelledby="'headine-' + id">
|
||||
<div class="panel-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CollapsiblePanel',
|
||||
data () {
|
||||
return {
|
||||
id: null,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
parent: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.id = this._uid
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<panel title="Kontingente" :body="true">
|
||||
<table class="table table-condensed">
|
||||
<colgroup>
|
||||
<col style="width: 75%">
|
||||
<col style="width: 25%">
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr v-for="contingent in contingents" :key="contingent.id">
|
||||
<th>{{ contingent.name }}</th>
|
||||
<td><span class="label" :class="contingent.available > 0 ? 'label-success' : 'label-danger'">{{ contingent.available }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</panel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Panel from './Panel'
|
||||
|
||||
export default {
|
||||
name: 'ContingentsOverview',
|
||||
components: {
|
||||
Panel
|
||||
},
|
||||
props: {
|
||||
contingents: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<panel title="Änderungen möglich bis" :body="true">
|
||||
<table class="table table-condensed">
|
||||
<colgroup>
|
||||
<col style="width: 75%">
|
||||
<col style="width: 25%">
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Unterkunft</th>
|
||||
<td>
|
||||
<span class="label" :class="mutableFields.accommodation.mutable ? 'label-success' : 'label-danger'">
|
||||
{{ mutableFields.accommodation.mutable ? mutableFields.accommodation.until : 'keine Änderung möglich' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Teilnehmer</th>
|
||||
<td>
|
||||
<span class="label" :class="mutableFields.participantData.mutable ? 'label-success' : 'label-danger'">
|
||||
{{ mutableFields.participantData.mutable ? mutableFields.participantData.until : 'keine Änderung möglich' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Leistungen</th>
|
||||
<td>
|
||||
<span class="label" :class="mutableFields.services.mutable ? 'label-success' : 'label-danger'">
|
||||
{{ mutableFields.services.until ? mutableFields.services.until : 'keine Änderung möglich' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Beförderung</th>
|
||||
<td>
|
||||
<span class="label" :class="mutableFields.transportation.mutable ? 'label-success' : 'label-danger'">
|
||||
{{ mutableFields.transportation.until ? mutableFields.transportation.until : 'keine Änderung möglich' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Zustiege</th>
|
||||
<td>
|
||||
<span class="label" :class="mutableFields.pickup.mutable ? 'label-success' : 'label-danger'">
|
||||
{{ mutableFields.pickup.until ? mutableFields.pickup.until : 'keine Änderung möglich' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</panel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Panel from './Panel';
|
||||
|
||||
export default {
|
||||
name: 'MutableFields',
|
||||
components: {
|
||||
Panel
|
||||
},
|
||||
props: {
|
||||
mutableFields: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table-condensed {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{{ title }}</h3>
|
||||
</div>
|
||||
<template v-if="body">
|
||||
<div class="panel-body">
|
||||
<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>
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<button class="btn btn-primary btn-sm w-full lg:w-auto" type="button" @click.prevent="visible = !visible">
|
||||
Adresse/Telefon <span class="glyphicon" :class="{ 'glyphicon-chevron-up': visible, 'glyphicon-chevron-down': !visible }"></span>
|
||||
</button>
|
||||
<div class="absolute top-100 left-0 z-10 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="text" placeholder="Straße"
|
||||
v-model="participant.data.street" :disabled="!mutable"
|
||||
@change="$emit('updated', participant)"
|
||||
>
|
||||
<input class="form-input w-full" type="text" placeholder="PLZ"
|
||||
v-model="participant.data.zipCode" :disabled="!mutable"
|
||||
@change="$emit('updated', participant)"
|
||||
>
|
||||
<input class="form-input w-full" type="text" placeholder="Ort"
|
||||
v-model="participant.data.city" :disabled="!mutable"
|
||||
@change="$emit('updated', participant)"
|
||||
>
|
||||
<select class="form-select w-full"
|
||||
v-model="participant.data.country" :disabled="!mutable"
|
||||
@change="$emit('updated', participant)"
|
||||
>
|
||||
<option value="">Land...</option>
|
||||
<option v-for="country in countries" :key="country.code" :value="country.code">{{ country.name }}</option>
|
||||
</select>
|
||||
<input class="form-input w-full" type="text" placeholder="Telefon"
|
||||
v-model="participant.data.phonePrivate" :disabled="!mutable"
|
||||
@change="$emit('updated', participant)"
|
||||
>
|
||||
<input class="form-input w-full" 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="lg:flex lg:space-x-1 space-y-1 lg:space-y-0">
|
||||
<input class="form-input w-full lg:w-1/6" type="text" placeholder="Nachname"
|
||||
v-model="participant.data.lastName" :disabled="!mutable"
|
||||
@change="$emit('updated', participant)"
|
||||
>
|
||||
<input class="form-input w-full lg:w-1/6" type="text" placeholder="Vorname"
|
||||
v-model="participant.data.firstName" :disabled="!mutable"
|
||||
@change="$emit('updated', participant)"
|
||||
>
|
||||
<input class="form-input w-full lg:w-1/6" type="text" placeholder="E-Mail"
|
||||
v-model="participant.data.email" :disabled="!mutable"
|
||||
@change="$emit('updated', participant)"
|
||||
>
|
||||
<select class="form-select w-full lg:w-1/6"
|
||||
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="form-select w-full lg:w-1/6"
|
||||
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="form-input w-full lg:w-1/6" 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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-control {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.dropdown-menu {
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="relative" v-if="hasSelectedRentals">
|
||||
<button class="btn btn-secondary btn-sm w-full lg:w-auto" @click.prevent="visible = !visible">
|
||||
Größen <span class="glyphicon" :class="{ 'glyphicon-chevron-up': visible, 'glyphicon-chevron-down': !visible }"></span>
|
||||
</button>
|
||||
<div class="absolute top-100 left-0 z-10 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="text" placeholder="Körpergröße"
|
||||
v-model="participant.data.height" :disabled="!mutable"
|
||||
@change="$emit('updated', participant)"
|
||||
>
|
||||
<input class="form-input w-full" type="text" placeholder="Schuhgröße"
|
||||
v-model="participant.data.shoeSize" :disabled="!mutable"
|
||||
@change="$emit('updated', participant)"
|
||||
>
|
||||
<input class="form-input w-full" type="text" placeholder="Gewicht"
|
||||
v-model="participant.data.weight" :disabled="!mutable"
|
||||
@change="$emit('updated', participant)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ParticipantSizesForm',
|
||||
props: {
|
||||
participant: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
services: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
mutable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
visible: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasSelectedRentals () {
|
||||
let result = false;
|
||||
for (let id of this.participant.serviceIds) {
|
||||
if (typeof this.services[id] === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
if ('VER' === this.services[id].subType) {
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dropdown {
|
||||
margin: 0 2px 2px 0;
|
||||
}
|
||||
.dropdown-menu {
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<button class="btn btn-primary btn-sm w-full lg:w-auto" @click.prevent="visible = !visible">
|
||||
Zustieg <span class="glyphicon" :class="{ 'glyphicon-chevron-up': visible, 'glyphicon-chevron-down': !visible }"></span>
|
||||
</button>
|
||||
<div class="absolute top-100 left-0 z-10 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="form-radio"
|
||||
: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>
|
||||
import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown'
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown {
|
||||
margin: 0 2px 2px 0;
|
||||
}
|
||||
.dropdown-menu {
|
||||
padding: 5px;
|
||||
min-width: 300px;
|
||||
}
|
||||
</style>
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="relative" v-if="hasServices">
|
||||
<button class="btn btn-primary btn-sm w-full lg:w-auto" type="button" @click.prevent="visible = !visible">
|
||||
{{ subType.label }} <span class="glyphicon" :class="{ 'glyphicon-chevron-up': visible, 'glyphicon-chevron-down': !visible }"></span>
|
||||
</button>
|
||||
<div class="absolute top-100 left-0 z-10 mt-4 border shadow-md p-4 bg-white w-screen max-w-xs" v-show="visible">
|
||||
<div class="py-1" v-for="service in availableServices" :key="service.id">
|
||||
<label class="inline-flex items-center text-sm">
|
||||
<input type="checkbox" class="form-checkbox"
|
||||
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 && this.services[id].subType !== this.subType.code) {
|
||||
continue;
|
||||
}
|
||||
filteredServices[id] = this.services[id];
|
||||
}
|
||||
return filteredServices;
|
||||
},
|
||||
hasServices () {
|
||||
return Object.keys(this.availableServices).length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<panel title="Gebuchte Zusatzleistungen">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" v-for="(assignment, serviceId) in serviceAssignments">
|
||||
{{ services[serviceId].name }}: {{ assignment.join(', ')}}
|
||||
</li>
|
||||
</ul>
|
||||
</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;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div v-if="hasServices">
|
||||
<label v-for="service in services" :key="service.id" class="flex items-center mb-1">
|
||||
<input type="checkbox" class="form-checkbox"
|
||||
v-model="selectedTransportationIds"
|
||||
:value="service.id"
|
||||
:disabled="isServiceUnavailable(service) || !mutable"
|
||||
@change="selectService(service, $event)"
|
||||
>
|
||||
<span class="ml-2">{{ service.name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TransportationSelect',
|
||||
props: {
|
||||
services: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
initialTransportationIds: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
mutable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
selectedTransportationIds: this.initialTransportationIds
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectService(service, event) {
|
||||
let action = event.target.checked ? 'select' : 'unselect';
|
||||
this.$emit('updated', { action, service, selectedTransportationIds: this.selectedTransportationIds });
|
||||
},
|
||||
isServiceUnavailable(service) {
|
||||
return service.available === 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasServices () {
|
||||
return Object.keys(this.services).length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown {
|
||||
margin: 0 2px 2px 0;
|
||||
}
|
||||
.form-check-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dropdown-menu {
|
||||
padding: 5px;
|
||||
min-width: 300px;
|
||||
}
|
||||
</style>
|
||||
@@ -5,12 +5,12 @@ import store from '../store';
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const guard = (to, from, next) => {
|
||||
if (store.state.loading) {
|
||||
next(false);
|
||||
} else if (store.getters.isLoggedIn === false) {
|
||||
if (store.getters.isLoggedIn === false) {
|
||||
next({ name: 'login' });
|
||||
} else if (to.meta.requiresTeamer && store.getters.isTeamer === false) {
|
||||
next(false);
|
||||
} else if (to.meta.requiresGroupManager && store.getters.isGroupManager === false) {
|
||||
next(false);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
@@ -23,8 +23,9 @@ import CrmData from '../myep/CrmData'
|
||||
import Newsletter from '../myep/Newsletter'
|
||||
import Registration from '../myep/Registration'
|
||||
|
||||
// Lazy load teamer component when required
|
||||
const Teamer = () => import('../myep/Teamer');
|
||||
// Lazy load some components when they become required
|
||||
const Teamer = () => import('../myep/Teamer')
|
||||
const BookingEdit = () => import('../myep/BookingEdit')
|
||||
|
||||
export default new VueRouter({
|
||||
routes: [
|
||||
@@ -74,6 +75,13 @@ export default new VueRouter({
|
||||
name: 'events',
|
||||
path: '/vorgaenge',
|
||||
component: Events,
|
||||
beforeEnter: guard,
|
||||
},
|
||||
{
|
||||
name: 'bookingEdit',
|
||||
path: '/buchung/:id',
|
||||
component: BookingEdit,
|
||||
meta: { requiresGroupManager: true },
|
||||
beforeEnter: guard
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ Vue.use(Vuex);
|
||||
|
||||
const defaultUserData = {
|
||||
isTeamer: false,
|
||||
isGroupManager: false,
|
||||
email: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
@@ -210,6 +211,7 @@ export default new Vuex.Store({
|
||||
watchlistCount: state => state.userData.watchList.length,
|
||||
onWatchlist: state => uid => state.userData.watchList.indexOf(uid) !== -1,
|
||||
isLoggedIn: state => !!state.authToken,
|
||||
isTeamer: state => !!state.userData.isTeamer
|
||||
isTeamer: state => !!state.userData.isTeamer,
|
||||
isGroupManager: state => !!state.userData.isGroupManager,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,67 +1,85 @@
|
||||
@import "variables";
|
||||
@import "color";
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import "remix/variables";
|
||||
@import "remix/colors";
|
||||
@layer base {
|
||||
@import "variables";
|
||||
@import "color";
|
||||
|
||||
// vendor styles
|
||||
$icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap";
|
||||
@import "remix/variables";
|
||||
@import "remix/colors";
|
||||
|
||||
@import "mixins";
|
||||
// vendor styles
|
||||
$icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap";
|
||||
|
||||
// remix
|
||||
$output-bourbon-deprecation-warnings: false;
|
||||
@import "~bourbon/app/assets/stylesheets/bourbon";
|
||||
@import "remix/breakpoints";
|
||||
@import "remix/mixins";
|
||||
@import "remix/base";
|
||||
@import "remix/overrides";
|
||||
@import "remix/badges";
|
||||
@import "remix/buttons";
|
||||
@import "remix/area-nav";
|
||||
@import "remix/pagehead";
|
||||
@import "remix/slider";
|
||||
@import "remix/header";
|
||||
@import "remix/herounit";
|
||||
@import "remix/calendar";
|
||||
@import "remix/partnerlogos";
|
||||
@import "remix/osm-map";
|
||||
@import "remix/video";
|
||||
@import "remix/newsletterform";
|
||||
@import "remix/layout";
|
||||
@import "remix/teaserbox";
|
||||
@import "remix/searchresult";
|
||||
@import "remix/tables";
|
||||
@import "remix/product";
|
||||
@import "remix/socialmedia";
|
||||
@import "remix/datepicker";
|
||||
@import "remix/sidebar";
|
||||
@import "remix/blog";
|
||||
@import "remix/forms";
|
||||
@import "remix/contactnav";
|
||||
@import "remix/faq";
|
||||
@import "remix/destination-pulldown";
|
||||
@import "remix/watchlist";
|
||||
@import "remix/calculator";
|
||||
@import "mixins";
|
||||
|
||||
// core config
|
||||
@import "fonts";
|
||||
@import "base";
|
||||
@import "btn";
|
||||
@import "input";
|
||||
@import "breadcrumb";
|
||||
@import "modal";
|
||||
// remix
|
||||
$output-bourbon-deprecation-warnings: false;
|
||||
@import "~bourbon/app/assets/stylesheets/bourbon";
|
||||
@import "remix/breakpoints";
|
||||
@import "remix/mixins";
|
||||
@import "remix/base";
|
||||
@import "remix/overrides";
|
||||
@import "remix/badges";
|
||||
@import "remix/buttons";
|
||||
@import "remix/area-nav";
|
||||
@import "remix/pagehead";
|
||||
@import "remix/slider";
|
||||
@import "remix/header";
|
||||
@import "remix/herounit";
|
||||
@import "remix/calendar";
|
||||
@import "remix/partnerlogos";
|
||||
@import "remix/osm-map";
|
||||
@import "remix/video";
|
||||
@import "remix/newsletterform";
|
||||
@import "remix/layout";
|
||||
@import "remix/teaserbox";
|
||||
@import "remix/searchresult";
|
||||
@import "remix/tables";
|
||||
@import "remix/product";
|
||||
@import "remix/socialmedia";
|
||||
@import "remix/datepicker";
|
||||
@import "remix/sidebar";
|
||||
@import "remix/blog";
|
||||
@import "remix/forms";
|
||||
@import "remix/contactnav";
|
||||
@import "remix/faq";
|
||||
@import "remix/destination-pulldown";
|
||||
@import "remix/watchlist";
|
||||
@import "remix/calculator";
|
||||
|
||||
//components
|
||||
@import "mainnavi";
|
||||
@import "footer";
|
||||
@import "searchbar";
|
||||
@import "searchbox";
|
||||
@import "newsletter";
|
||||
@import "disturber";
|
||||
@import "product";
|
||||
@import "sidebar";
|
||||
@import "subpage";
|
||||
@import "searchpage";
|
||||
@import "employee";
|
||||
// core config
|
||||
@import "fonts";
|
||||
@import "base";
|
||||
@import "btn";
|
||||
@import "input";
|
||||
@import "breadcrumb";
|
||||
@import "modal";
|
||||
|
||||
//components
|
||||
@import "mainnavi";
|
||||
@import "footer";
|
||||
@import "searchbar";
|
||||
@import "searchbox";
|
||||
@import "newsletter";
|
||||
@import "disturber";
|
||||
@import "product";
|
||||
@import "sidebar";
|
||||
@import "subpage";
|
||||
@import "searchpage";
|
||||
@import "employee";
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.top-1\/2 {
|
||||
top: 50%;
|
||||
}
|
||||
.left-1\/2 {
|
||||
left: 50%;
|
||||
}
|
||||
.-translate-xy-1\/2 {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ html {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
*, *::after, *::before {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
module.exports = {
|
||||
future: {
|
||||
removeDeprecatedGapUtilities: true,
|
||||
purgeLayersByDefault: true,
|
||||
},
|
||||
purge: {
|
||||
mode: 'layers',
|
||||
layers: ['components', 'utilities'],
|
||||
content: [
|
||||
'./public/typo3conf/ext/ep_theme/Resources/Private/Layouts/**/*.html',
|
||||
'./public/typo3conf/ext/ep_theme/Resources/Private/Partials/**/*.html',
|
||||
'./public/typo3conf/ext/ep_theme/Resources/Private/Templates/**/*.html',
|
||||
'./public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/**/*.js',
|
||||
'./public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/**/*.vue',
|
||||
],
|
||||
},
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
overlay: {
|
||||
'white': 'rgba(255, 255, 255, 0.85)'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
variants: {},
|
||||
plugins: [
|
||||
require('@tailwindcss/custom-forms'),
|
||||
],
|
||||
corePlugins: {
|
||||
container: false,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user