working location settings (incl saving)

This commit is contained in:
2023-07-24 15:44:24 +02:00
parent 5e5e7fee49
commit 84c0017e2a
9 changed files with 167 additions and 121 deletions

View File

@@ -22,7 +22,7 @@ module.exports = ( app ) => {
getHandler.handleCall( req.params.call, req.query ).then( data => { getHandler.handleCall( req.params.call, req.query ).then( data => {
res.send( data ); res.send( data );
} ).catch( error => { } ).catch( error => {
res.status( 500 ).send( error ); res.status( error.code ).send( error.error );
} ); } );
} else { } else {
res.status( 403 ).sendFile( path.join( __dirname + '/../ui/' + ( req.query.lang ?? 'en' ) + '/errors/403.html' ) ); res.status( 403 ).sendFile( path.join( __dirname + '/../ui/' + ( req.query.lang ?? 'en' ) + '/errors/403.html' ) );
@@ -34,7 +34,7 @@ module.exports = ( app ) => {
postHandler.handleCall( req.params.call, req.body, req.query.lang ).then( data => { postHandler.handleCall( req.params.call, req.body, req.query.lang ).then( data => {
res.send( data ); res.send( data );
} ).catch( error => { } ).catch( error => {
res.status( 500 ).send( error ); res.status( error.code ).send( error.error );
} ); } );
} else { } else {
res.status( 403 ).sendFile( path.join( __dirname + '/../ui/' + ( req.query.lang ?? 'en' ) + '/errors/403.html' ) ); res.status( 403 ).sendFile( path.join( __dirname + '/../ui/' + ( req.query.lang ?? 'en' ) + '/errors/403.html' ) );

View File

@@ -21,10 +21,10 @@ class GETHandler {
if ( Object.keys( data ).length > 0 ) { if ( Object.keys( data ).length > 0 ) {
resolve( data[ 'save' ] ); resolve( data[ 'save' ] );
} else { } else {
reject( 'No data found for this location' ); reject( { 'code': 400, 'error': 'No data found for this location' } );
} }
} ).catch( error => { } ).catch( error => {
reject( error ); reject( { 'code': 500, 'error': error } );
} ); } );
} else if ( call === 'getSeatplanDraft' ) { } else if ( call === 'getSeatplanDraft' ) {
db.getJSONDataSimple( 'seatplan', query.location ).then( data => { db.getJSONDataSimple( 'seatplan', query.location ).then( data => {
@@ -35,11 +35,19 @@ class GETHandler {
resolve( data[ 'save' ] ); resolve( data[ 'save' ] );
} }
} else { } else {
reject( 'No data found for this location' ); reject( { 'code': 400, 'error': 'No data found for this location' } );
} }
} ).catch( error => { } ).catch( error => {
reject( error ); reject( error );
} ); } );
} else if ( call === 'getLocations' ) {
db.getJSONData( 'locations' ).then( data => {
resolve( data );
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} else {
reject( { 'code': 404, 'error': 'Route not found' } );
} }
} ); } );
} }

View File

@@ -24,15 +24,38 @@ class POSTHandler {
db.writeJSONDataSimple( 'seatplan', data.location, dat ).then( resp => { db.writeJSONDataSimple( 'seatplan', data.location, dat ).then( resp => {
resolve( resp ); resolve( resp );
} ).catch( error => { } ).catch( error => {
reject( error ); reject( { 'code': 500, 'error': error } );
} ); } );
} ); } );
} else if ( call === 'saveSeatplan' ) { } else if ( call === 'saveSeatplan' ) {
db.writeJSONDataSimple( 'seatplan', data.location, { 'draft': {}, 'save': data.data } ).then( resp => { db.writeJSONDataSimple( 'seatplan', data.location, { 'draft': {}, 'save': data.data } ).then( resp => {
resolve( resp ); resolve( resp );
} ).catch( error => { } ).catch( error => {
reject( error ); reject( { 'code': 500, 'error': error } );
} ); } );
} else if ( call === 'saveLocations' ) {
db.getJSONData( 'seatplan' ).then( res => {
let dat = res;
for ( let loc in data.updated ) {
if ( res[ loc ] ) {
dat[ data.updated[ loc ] ] = res[ loc ];
delete dat[ loc ];
}
}
db.writeJSONData( 'seatplan', dat ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
db.writeJSONData( 'locations', data.data ).then( resp => {
resolve( resp );
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} else {
reject( { 'code': 404, 'error': 'Route not found' } );
} }
} ); } );
} }

View File

@@ -0,0 +1 @@
{"test2":{"locationID":"test2","name":"TestLocation2","seatplan-enabled":true},"test":{"locationID":"test","name":"TestLocation","seatplan-enabled":true}}

View File

@@ -15,7 +15,7 @@
<h3>{{ data.message }}</h3> <h3>{{ data.message }}</h3>
<settings v-model:settings="data.options"></settings> <settings v-model:settings="data.options"></settings>
<div class="button-wrapper"> <div class="button-wrapper">
<button @click="closePopup( 'ok' )" title="Save changes">Save</button> <button @click="submitSettings( 'ok' )" title="Save changes">Save</button>
<button @click="closePopup( 'cancel' )" title="Cancel changes">Cancel</button> <button @click="closePopup( 'cancel' )" title="Cancel changes">Cancel</button>
</div> </div>
</div> </div>
@@ -121,6 +121,17 @@
this.data[ 'selected' ] = data; this.data[ 'selected' ] = data;
this.closePopup( message ); this.closePopup( message );
}, },
submitSettings () {
$( '#popup-backdrop' ).fadeOut( 300 );
const dat = this.data.options;
let ret = {};
for ( let setting in dat ) {
if ( dat[ setting ][ 'type' ] !== 'link' ) {
ret[ setting ] = dat[ setting ][ 'value' ];
}
}
this.$emit( 'data', { 'data': ret, 'status': 'settings' } );
},
openPopup ( message, options, dataType, selected ) { openPopup ( message, options, dataType, selected ) {
this.data = { 'message': message ?? 'No message defined on method call!!', 'options': options ?? { '1': { 'value': 'undefined', 'displayName': 'No options specified in call' } }, 'selected': selected ?? '' }; this.data = { 'message': message ?? 'No message defined on method call!!', 'options': options ?? { '1': { 'value': 'undefined', 'displayName': 'No options specified in call' } }, 'selected': selected ?? '' };
this.contentType = dataType ?? 'string'; this.contentType = dataType ?? 'string';

View File

@@ -79,7 +79,7 @@
data() { data() {
return { return {
draggables: { 1: { 'x': 100, 'y':100, 'h': 100, 'w': 250, 'active': false, 'draggable': true, 'resizable': true, 'id': 1, 'origin': 1, 'shape':'rectangular', 'type': 'seat', 'startingRow': 1, 'seatCountingStartingPoint': 1, 'sector': 'A', 'text': { 'text': 'TestText', 'textSize': 20, 'colour': '#20FFFF' }, 'ticketCount': 1, 'category': 1 } }, draggables: { 1: { 'x': 100, 'y':100, 'h': 100, 'w': 250, 'active': false, 'draggable': true, 'resizable': true, 'id': 1, 'origin': 1, 'shape':'rectangular', 'type': 'seat', 'startingRow': 1, 'seatCountingStartingPoint': 1, 'sector': 'A', 'text': { 'text': 'TestText', 'textSize': 20, 'colour': '#20FFFF' }, 'ticketCount': 1, 'category': 1 } },
event: { 'name': 'TestEvent2', 'location': 'TestLocation2', 'date': '2023-07-15', 'RoomName': 'TestRoom2', 'currency': 'CHF', 'categories': { '1': { 'price': { '1':25, '2':35 }, 'bg': 'black', 'fg': 'white', 'name': 'Category 1' }, '2': { 'price': { '1':15, '2':20 }, 'bg': 'green', 'fg': 'white', 'name': 'Category 2' } }, 'ageGroups': { '1':{ 'id': 1, 'name':'Child', 'age':'0 - 15.99' }, '2':{ 'id': 2, 'name': 'Adult' } }, 'maxTickets': 2 }, event: { 'name': 'TestEvent2', 'location': 'TestLocation2', 'date': '2023-07-15', 'currency': 'CHF', 'categories': { '1': { 'price': { '1':25, '2':35 }, 'bg': 'black', 'fg': 'white', 'name': 'Category 1' }, '2': { 'price': { '1':15, '2':20 }, 'bg': 'green', 'fg': 'white', 'name': 'Category 2' } }, 'ageGroups': { '1':{ 'id': 1, 'name':'Child', 'age':'0 - 15.99' }, '2':{ 'id': 2, 'name': 'Adult' } }, 'maxTickets': 2 },
available: { 'redo': false, 'undo': false }, available: { 'redo': false, 'undo': false },
scaleFactor: 1, scaleFactor: 1,
sizePoll: null, sizePoll: null,

View File

@@ -15,9 +15,9 @@
<div class="location-app" v-if="Object.keys( locations ).length"> <div class="location-app" v-if="Object.keys( locations ).length">
<ul> <ul>
<li v-for="location in locations"> <li v-for="location in locations">
<div class="location" @click="selectLocation( location.locationID );" title="Edit this location"> <div class="location" @click="selectLocation( location.locationID );" title="Edit this location" @contextmenu="( e ) => { e.preventDefault(); openRightClickMenu( location.locationID, e ); }">
<div class="location-name"> <div class="location-name">
<h3>{{ location.name }}</h3> <h3>{{ location.locationID }} ({{ location.name }})</h3>
<p v-if="location['seatplan-enabled']">This location has a seatplan.</p> <p v-if="location['seatplan-enabled']">This location has a seatplan.</p>
<p v-else>This location has NO seatplan.</p> <p v-else>This location has NO seatplan.</p>
</div> </div>
@@ -28,39 +28,54 @@
<div v-else class="no-location-hint"> <div v-else class="no-location-hint">
No locations configured, please <b @click="addLocation();" style="cursor: pointer;">add</b> one No locations configured, please <b @click="addLocation();" style="cursor: pointer;">add</b> one
</div> </div>
<popups ref="popup" size="big"></popups> <popups ref="popup" size="big" @data="( data ) => {
<popups ref="popup2" size="huge"></popups> handleData( data );
}"></popups>
<rightClickMenu ref="rclk" @command="( command ) => { executeCommand( command ) }"></rightClickMenu>
</div> </div>
</template> </template>
<script> <script>
import popups from '@/components/notifications/popups.vue'; import popups from '@/components/notifications/popups.vue';
import rightClickMenu from '@/components/settings/rightClickMenu.vue';
export default { export default {
data () { data () {
return { return {
locations: { 'test':{ 'name':'TestLocation', 'locationID':'test', 'seatplan-enabled': true, 'seatplan': {} } }, locations: { 'test':{ 'name':'TestLocation', 'locationID':'test', 'seatplan-enabled': true, 'seatplan': {} } },
currentlyOpenMenu: '',
currentPopup: '',
updatedLocations: {}
} }
}, },
components: { components: {
popups, popups,
rightClickMenu,
}, },
methods: { methods: {
selectLocation ( locationID ) { selectLocation ( locationID ) {
sessionStorage.setItem( 'locationID', locationID ); sessionStorage.setItem( 'locationID', locationID );
this.currentlyOpenMenu = locationID;
this.$refs.popup.openPopup( 'Settings for ' + this.locations[ locationID ][ 'name' ], { this.$refs.popup.openPopup( 'Settings for ' + this.locations[ locationID ][ 'name' ], {
'locationName': { 'locationID': {
'display': 'Location name', 'display': 'Internal location name',
'id': 'locationName', 'id': 'locationID',
'tooltip':'Give the location the event takes place a name. This name will also be shown to customers', 'tooltip':'Give the location where the event takes place a name. This name will not be shown to the customers and is used for the backend and admin portal. Has to be unique',
'value': '', 'value': locationID,
'type': 'text', 'type': 'text',
}, },
'usesSeatplan': { 'name': {
'display': 'Public location name',
'id': 'name',
'tooltip':'The name of the location that is shown to the customers.',
'value': this.locations[ locationID ][ 'name' ],
'type': 'text',
},
'seatplan-enabled': {
'display': 'Use seat plan?', 'display': 'Use seat plan?',
'id': 'usesSeatplan', 'id': 'seatplan-enabled',
'tooltip':'With this toggle you may specify whether or not this location has a seat plan or not.', 'tooltip':'With this toggle you may specify whether or not this location has a seat plan or not.',
'value': true, 'value': this.locations[ locationID ][ 'seatplan-enabled' ],
'type': 'toggle', 'type': 'toggle',
}, },
'seatplanEditor': { 'seatplanEditor': {
@@ -78,16 +93,23 @@
}, },
addLocation () { addLocation () {
this.$refs.popup.openPopup( 'Add a new location', { this.$refs.popup.openPopup( 'Add a new location', {
'locationName': { 'locationID': {
'display': 'Location name', 'display': 'Internal location name',
'id': 'locationName', 'id': 'locationID',
'tooltip':'Give the location the event takes place a name. This name will also be shown to customers', 'tooltip':'Give the location where the event takes place a name. This name will not be shown to the customers and is used for the backend and admin portal. Has to be unique',
'value': '', 'value': '',
'type': 'text', 'type': 'text',
}, },
'usesSeatplan': { 'name': {
'display': 'Public location name',
'id': 'name',
'tooltip':'The name of the location that is shown to the customers.',
'value': '',
'type': 'text',
},
'seatplan-enabled': {
'display': 'Use seat plan?', 'display': 'Use seat plan?',
'id': 'usesSeatplan', 'id': 'seatplan-enabled',
'tooltip':'With this toggle you may specify whether or not this location has a seat plan or not.', 'tooltip':'With this toggle you may specify whether or not this location has a seat plan or not.',
'value': true, 'value': true,
'type': 'toggle', 'type': 'toggle',
@@ -105,6 +127,62 @@
} }
, 'settings' ); , 'settings' );
}, },
openRightClickMenu( id, event ) {
this.$refs.rclk.openRightClickMenu( event, { 'edit': { 'command': 'editLocation', 'symbol': 'edit', 'display': 'Edit location' }, 'editor': { 'command': 'openEditor', 'symbol': 'tune', 'display': 'Edit seatplan' }, 'delete': { 'command': 'deleteLocation', 'symbol': 'delete', 'display': 'Delete location' } } )
this.currentlyOpenMenu = id;
},
executeCommand( command ) {
if ( command === 'editLocation' ) {
this.selectLocation( this.currentlyOpenMenu );
} else if ( command === 'deleteLocation' ) {
this.$refs.popup.openPopup( 'Do you really want to delete the location ' + this.currentlyOpenMenu + '?', {}, 'confirm' );
this.currentPopup = 'delete';
} else if ( command === 'openEditor' ) {
sessionStorage.setItem( 'locationID', this.currentlyOpenMenu );
this.$router.push( '/admin/seatplan' );
}
},
handleData ( data ) {
if ( this.currentPopup === 'delete' ) {
this.currentPopup = '';
if ( data.status === 'ok' ) {
delete this.locations[ this.currentlyOpenMenu ];
}
} else {
if ( data.status === 'settings' ) {
if ( data.data.locationID !== this.currentlyOpenMenu && this.currentlyOpenMenu !== '' ) {
delete this.locations[ this.currentlyOpenMenu ];
this.updatedLocations[ this.currentlyOpenMenu ] = data.data.locationID;
}
this.locations[ data.data.locationID ] = data.data;
this.currentlyOpenMenu = '';
const options = {
method: 'post',
body: JSON.stringify( { 'updated': this.updatedLocations, 'data': this.locations } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/admin/api/saveLocations', options ).then( res => {
if ( res.status === 200 ) {
res.text().then( text => {
console.log( text );
} );
}
} );
}
}
},
},
created () {
fetch( localStorage.getItem( 'url' ) + '/admin/getAPI/getLocations' ).then( res => {
res.json().then( data => {
this.locations = data;
} ).catch( error => {
console.error( error );
} );
} );
} }
}; };
</script> </script>
@@ -149,4 +227,8 @@
margin-right: auto; margin-right: auto;
max-width: 35%; max-width: 35%;
} }
.no-location-hint {
margin-top: 5%;
}
</style> </style>

View File

@@ -16,11 +16,12 @@
<table class="category"> <table class="category">
<tr> <tr>
<td>Event location</td> <td>Event location</td>
<td> <td v-if="Object.keys( locations ).length > 0">
<select v-model="event.location" class="small-text"> <select v-model="event.location" class="small-text">
<option value="TestLocation">TestLocation</option> <option v-for="location in locations" value="location.locationID">{{ location.locationID }} ({{ location.name }})</option>
</select> </select>
</td> </td>
<td v-else>No locations configured yet. Configure one <router-link to="/admin/locations">here</router-link></td>
</tr> </tr>
<tr> <tr>
<td>Event date</td> <td>Event date</td>
@@ -38,12 +39,12 @@
<div class="category-wrapper"> <div class="category-wrapper">
<table class="category" v-for="category in event.categories"> <table class="category" v-for="category in event.categories">
{{ category.name }} {{ category.name }}
<tr v-for="price in category.price"> <tr v-for="ageGroup in event.ageGroups">
<td> <td>
<div class="category-details">{{ price.name }}:</div> <div class="category-details">{{ ageGroup.name }}<div style="display: inline;" v-if="ageGroup.age"> ({{ ageGroup.age }})</div>:</div>
</td> </td>
<td> <td>
<input type="number" v-model="price.price"> <input type="number" v-model="category.price[ ageGroup.id ]">
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -118,10 +119,18 @@
this.$router.push( '/admin/events' ); this.$router.push( '/admin/events' );
} }
this.eventID = sessionStorage.getItem( 'selectedTicket' ); this.eventID = sessionStorage.getItem( 'selectedTicket' );
fetch( localStorage.getItem( 'url' ) + '/admin/getAPI/getLocations' ).then( res => {
res.json().then( data => {
this.locations = data;
} ).catch( error => {
console.error( error );
} );
} );
}, },
data() { data() {
return { return {
event: { 'name': 'TestEvent', 'description': 'This is a description for the TestEvent to test multiline support and proper positioning of the Fields', 'freeSeats': 2, 'maxSeats': 2, 'date':'TestDate', 'startingPrice':15, 'location': 'TestLocation', 'eventID': 'test', 'currency': 'CHF', 'logo': 'logo.png', 'categories': { '1': { 'price': { '1': { 'price':25, 'name':'Child (0-15.99 years)'}, '2': { 'price':35, 'name':'Adult'} }, 'bg': 'black', 'fg': 'white', 'name': 'Category 1' }, '2': { 'price': { '1': { 'price':25, 'name':'Child (0-15.99 years)' }, '2': { 'price':35, 'name':'Adult'} }, 'bg': 'green', 'fg': 'white', 'name': 'Category 2' } } }, locations: {},
event: { 'name': 'TestEvent', 'description': 'This is a description for the TestEvent to test multiline support and proper positioning of the Fields', 'location': 'test', 'date': '2023-07-15', 'currency': 'CHF', 'categories': { '1': { 'price': { '1':25, '2':35 }, 'bg': 'black', 'fg': 'white', 'name': 'Category 1' }, '2': { 'price': { '1':15, '2':20 }, 'bg': 'green', 'fg': 'white', 'name': 'Category 2' } }, 'ageGroups': { '1':{ 'id': 1, 'name':'Child', 'age':'0 - 15.99' }, '2':{ 'id': 2, 'name': 'Adult' } }, 'maxTickets': 2 },
specialSettings: { specialSettings: {
'currency': { 'currency': {
'display': 'Currency', 'display': 'Currency',

View File

@@ -1,88 +0,0 @@
<!--
* libreevent - TicketsDetailsView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="details">
<h2>{{ event.name }}</h2>
<div class="category-wrapper">
<table class="category">
<tr>
<td>Location name</td>
<td>
<input type="text" v-model="event.name">
</td>
</tr>
<tr>
<td>Seat plan editor</td>
<router-link to="/admin/seatplan">Edit seat plan</router-link>
</tr>
</table>
</div>
<button @click="openPopup()">Open Popup</button>
<popups ref="popup" @data="( event ) => { popupReturnHandling( event ) }"></popups>
<notifications ref="notification" location="topright"></notifications>
</div>
</template>
<style scoped>
.details {
flex-grow: 1;
}
.ticket-settings {
width: 100%;
}
.category-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
flex-direction: column;
}
.category {
width: 50%;
text-align: justify;
}
.category-details {
margin-left: 7%;;
}
</style>
<script>
import settings from '@/components/settings/settings.vue';
import notifications from '@/components/notifications/notifications.vue';
import popups from '@/components/notifications/popups.vue';
export default {
name: 'TicketsDetailsView',
components: {
settings,
notifications,
popups,
},
data() {
return {
event: { 'name': 'TestEvent', 'description': 'This is a description for the TestEvent to test multiline support and proper positioning of the Fields', 'freeSeats': 2, 'maxSeats': 2, 'date':'TestDate', 'startingPrice':15, 'location': 'TestLocation', 'eventID': 'test', 'currency': 'CHF', 'logo': 'logo.png', 'categories': { '1': { 'price': { '1': { 'price':25, 'name':'Child (0-15.99 years)'}, '2': { 'price':35, 'name':'Adult'} }, 'bg': 'black', 'fg': 'white', 'name': 'Category 1' }, '2': { 'price': { '1': { 'price':25, 'name':'Child (0-15.99 years)' }, '2': { 'price':35, 'name':'Adult'} }, 'bg': 'green', 'fg': 'white', 'name': 'Category 2' } } },
}
},
methods: {
openPopup() {
console.log( 'opening' );
this.$refs.popup.openPopup();
},
popupReturnHandling( event ) {
console.log( event );
}
}
};
</script>