Integrate booking edit into myep spa

This commit is contained in:
Björn Fromme
2020-09-22 17:21:00 +02:00
committed by Björn Fromme
parent 213b75b863
commit dbe4336550
23 changed files with 4785 additions and 2877 deletions
+3422 -2750
View File
File diff suppressed because it is too large Load Diff
+15 -13
View File
@@ -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
View File
@@ -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">&times;</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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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 {
+33
View File
@@ -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,
}
}