event settings almost done

This commit is contained in:
2023-08-11 15:37:04 +02:00
parent d2c0257b8f
commit d5c581170f
15 changed files with 576 additions and 67 deletions

View File

@@ -52,6 +52,26 @@ class GETHandler {
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} else if ( call === 'getEvent' ) {
db.getJSONDataSimple( 'eventDrafts', query.event ).then( data => {
if ( Object.keys( data ).length > 1 ) {
resolve( data );
} else {
reject( { 'code': 404, 'error': 'EventNotFound' } );
}
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} else if ( call === 'getAllEvents' ) {
db.getJSONData( 'eventDrafts' ).then( data => {
db.getJSONData( 'events' ).then( dat => {
resolve( { 'live': dat ?? {}, 'drafts': data ?? {} } );
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} else {
reject( { 'code': 404, 'error': 'Route not found' } );
}

View File

@@ -54,6 +54,52 @@ class POSTHandler {
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} else if ( call === 'deleteLocation' ) {
db.deleteJSONDataSimple( 'locations', data.location ).then( () => {
resolve( 'ok' );
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} else if ( call === 'createEvent' ) {
db.getJSONDataSimple( 'eventDrafts', data.event ).then( dat => {
if ( Object.keys( dat ).length < 1 ) {
db.writeJSONDataSimple( 'eventDrafts', data.event, { 'name': 'Unnamed event', 'description': '', 'location': '', 'date': '', 'categories': {}, 'ageGroups': { '1': { 'id': 1, 'name': 'Child', 'age': '0 - 15.99' }, '2': { 'id': 2, 'name': 'Adult' } }, 'maxTickets': 2, 'eventID': data.event } ).then( () => {
resolve( 'ok' );
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} else {
reject( { 'code': 409, 'error': 'ExistsAlready' } );
}
} );
} else if ( call === 'saveEvent' ) {
db.writeJSONDataSimple( 'eventDrafts', data.event, data.eventData ).then( () => {
resolve( 'ok' );
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} else if ( call === 'deployEvent' ) {
db.writeJSONDataSimple( 'events', data.event, data.eventData ).then( () => {
resolve( 'ok' );
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} else if ( call === 'deleteEvent' ) {
db.deleteJSONDataSimple( 'eventDrafts', data.event ).then( () => {
db.deleteJSONDataSimple( 'events', data.event ).then( () => {
resolve( 'ok' );
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} else if ( call === 'undeployEvent' ) {
db.deleteJSONDataSimple( 'events', data.event ).then( () => {
resolve( 'ok' );
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} else if ( call === 'saveTickets' ) {
db.writeJSONDataSimple( 'tickets', data.location, data.data ).then( resp => {
resolve( resp );

View File

@@ -36,6 +36,16 @@ class GETHandler {
} else {
reject( { 'code': 400, 'message': 'Bad request, missing event query' } );
}
} else if ( call === 'getEvent' ) {
db.getJSONDataSimple( 'events', query.event ).then( data => {
if ( Object.keys( data ) ) {
resolve( data );
} else {
reject( { 'code': 404, 'error': 'EventNotFound' } );
}
} ).catch( error => {
reject( { 'code': 500, 'error': error } );
} );
} else if ( call === 'getName' ) {
resolve( { 'name': settings.name } );
} else {

View File

@@ -17,7 +17,8 @@ class POSTHandler {
db.getJSONData( 'booked' ).then( dat => {
this.allSelectedSeats = dat;
} );
// TODO: Load from event db
// TODO: Load from event db subtract all occupied seats from the ordered db from it.
// TODO: When loading event data, also add currency to it from settings
this.ticketTotals = { 'test2': { 'ticket1': 5, 'ticket2': 5 } };
this.settings = JSON.parse( fs.readFileSync( path.join( __dirname + '/../../config/settings.config.json' ) ) );
}

View File

@@ -0,0 +1 @@
{"test3":{"name":"TestEvent3","description":"Test event to see if it works to save data to backend","location":"test2","date":"2023-08-25","categories":{"1":{"price":{"1":20,"2":30},"bg":"#FFFFFF","fg":"#000000","name":"Category 1","id":"1","ticketCount":1}},"ageGroups":{"1":{"id":1,"name":"Child","age":"0 - 15.99"},"2":{"id":2,"name":"Adult"}},"maxTickets":0,"eventID":"test3","time":"10:00","currency":"USD","isDraft":true}}

View File

@@ -152,3 +152,25 @@ module.exports.writeJSONData = ( db, data ) => {
} );
} );
};
module.exports.deleteJSONDataSimple = ( db, identifier ) => {
return new Promise( ( resolve, reject ) => {
fs.readFile( path.join( __dirname + '/data/' + db + '.json' ), ( error, data ) => {
if ( error ) {
reject( 'Error occurred: Error trace: ' + error );
} else {
let dat = {};
if ( data.byteLength > 0 ) {
dat = JSON.parse( data ) ?? {};
}
delete dat[ identifier ];
fs.writeFile( path.join( __dirname + '/data/' + db + '.json' ), JSON.stringify( dat ), ( error ) => {
if ( error ) {
reject( 'Error occurred: Error trace: ' + error );
}
resolve( true );
} );
}
} );
} );
};

File diff suppressed because one or more lines are too long

View File

@@ -1,29 +1,25 @@
# Account view:
- Maybe add multi-language support
- make pricing groups changeable in UI (event categories)
- create function that parses DB every 15 minutes and clears out junk --> Also update data in db when user goes to purchase to prevent clearing during purchase
- Require user to confirm email before purchasing
- Load all orders of customer from db when selecting tickets and save to memory to check if ticket count has been exceeded or not.
- Guest purchase in the future (maybe remove from matura)
- Create password changing endpoint (to reset forgotten pwd)
- Add Admin profile (page to change account settings per person like changing pwd)
- Create function that updates currency for every event when updating currency globally.
- Fix text field overflow (text too big for box)
- Other optimisation for seat plan editor
- Other optimization for seat plan editor
- Implement Permission system
- Seat numbering!!
- Add localization for date
- add webpack (or any other minifying tool) to project website to decrease file size (OPTIONAL)
- FUTURE: Add Admin profile (page to change account settings per person like changing pwd)
- FUTURE add multi-language support
- FUTURE: Guest purchase
- FUTURE: add webpack (or any other minifying tool) to project website to decrease file size (OPTIONAL)

View File

@@ -11,6 +11,22 @@
</div>
</div>
<div v-else-if="contentType === 'html'" v-html="data.message" class="options"></div>
<div v-else-if="contentType === 'text'" class="options">
<h3>{{ data.message }}</h3>
<input type="text" v-model="data.selected">
<div class="button-wrapper">
<button @click="closePopup( 'ok' )" title="Save changes">Save</button>
<button @click="closePopup( 'cancel' )" title="Cancel changes">Cancel</button>
</div>
</div>
<div v-else-if="contentType === 'number'" class="options">
<h3>{{ data.message }}</h3>
<input type="number" v-model="data.selected">
<div class="button-wrapper">
<button @click="closePopup( 'ok' )" title="Save changes">Save</button>
<button @click="closePopup( 'cancel' )" title="Cancel changes">Cancel</button>
</div>
</div>
<div v-else-if="contentType === 'settings'" class="options">
<h3>{{ data.message }}</h3>
<settings v-model:settings="data.options"></settings>

View File

@@ -10,7 +10,7 @@
import { defineStore } from "pinia";
export const useBackendStore = defineStore ( 'backend', {
state: () => ( { 'guestPurchase': false, 'guestPurchaseAllowed': true } ),
state: () => ( { 'guestPurchase': false, 'guestPurchaseAllowed': false } ),
getters: {
getVisitedSetupPages: ( state ) => state.visitedSetupPages,
getIsGuestPurchase: ( state ) => state.guestPurchase,

View File

@@ -17,10 +17,10 @@
<div class="side-nav-wrapper">
<img src="@/assets/logo.png" alt="libreevent logo" style="width: 80%; margin-left: 10%; margin-bottom: 5%;">
<router-link to="/admin" class="admin-menu" @click="navMenu( 'hide' )" title="The home page of the admin panel">Home</router-link>
<router-link to="/admin/pages" class="admin-menu" @click="navMenu( 'hide' )" title="Modify your landing page, your terms of service, etc.">Pages</router-link>
<router-link to="/admin/events" class="admin-menu" @click="navMenu( 'hide' )" title="Change, view and analyse everything about your events">Events</router-link>
<router-link to="/admin/pages" class="admin-menu" @click="navMenu( 'hide' )" title="Modify your landing page">Pages</router-link>
<router-link to="/admin/events" class="admin-menu" @click="navMenu( 'hide' )" title="Change and view everything about your events">Events</router-link>
<router-link to="/admin/locations" class="admin-menu" @click="navMenu( 'hide' )" title="Change settings about your event locations">Locations</router-link>
<router-link to="/admin/plugins" class="admin-menu" @click="navMenu( 'hide' )" title="Install, Uninstall and manage plugins">Plugins</router-link>
<router-link to="/admin/plugins" class="admin-menu" @click="navMenu( 'hide' )" title="Manage plugins">Plugins</router-link>
<router-link to="/admin/settings" class="admin-menu" @click="navMenu( 'hide' )" title="Change global settings for libreevent">Settings</router-link>
<button to="/admin/login" class="admin-menu" @click="logout()" title="Log out of the admin panel">Logout</button>
</div>

View File

@@ -10,11 +10,12 @@
<template>
<div class="order">
<h2>Events</h2>
<button @click="addEvent()">Create new event</button>
<div class="order-app" v-if="events">
<ul v-for="timeframe in eventList">
<h3>{{ timeframe.name }}</h3>
<li v-for="event in timeframe.content">
<router-link to="/admin/events/view" class="ticket" @click="setActiveTicket( event.eventID );" v-if="new Date( event.date ).getTime() > currentDate">
<router-link to="/admin/events/view" class="ticket" @click="setActiveTicket( event.eventID );" v-if="new Date( event.date ).getTime() > currentDate || timeframe.name === 'Drafts'">
<div class="ticket-name">
<h3>{{ event.name }}</h3>
<p>{{ event.description }}</p>
@@ -34,8 +35,12 @@
</ul>
</div>
<div class="order-app" v-else>
No future events are available!
No events are available!
</div>
<popups ref="popup" size="big" @data="( data ) => {
handleData( data );
}"></popups>
<rightClickMenu ref="rclk" @command="( command ) => { executeCommand( command ) }"></rightClickMenu>
</div>
</template>
@@ -91,37 +96,118 @@
</style>
<script>
import popups from '@/components/notifications/popups.vue';
import rightClickMenu from '@/components/settings/rightClickMenu.vue';
export default {
name: 'OrderView',
methods: {
setActiveTicket ( id ) {
sessionStorage.setItem( 'selectedTicket', id );
}
components: {
popups,
rightClickMenu,
},
data () {
return {
events: { 'test':{ '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':'2023-08-15', 'startingPrice':15, 'location': 'TestLocation', 'eventID': 'test', 'currency': 'CHF', 'logo': new URL( '/src/assets/logo.png', import.meta.url ).href }, 'test2':{ 'name': 'TestEvent2', 'description': 'This is a description for the TestEvent to test multiline support and proper positioning of the Fields', 'freeSeats': 2, 'maxSeats': 2, 'date':'2023-06-13', 'startingPrice':15, 'location': 'TestLocation', 'eventID': 'test2', 'currency': 'CHF', 'logo': new URL( '/src/assets/logo.png', import.meta.url ).href } },
currentDate: new Date().getTime(),
eventList: { 'upcoming': { 'name': 'Upcoming', 'content': {} }, 'past': { 'name': 'Past', 'content': {} } },
eventList: { 'upcoming': { 'name': 'Upcoming', 'content': {} }, 'past': { 'name': 'Past', 'content': {} }, 'drafts': { 'name': 'Drafts', 'content': {} } },
currentlyOpenMenu: '',
}
},
created() {
// Sort events object such that events closest to today are displayed first and past events displayed last
let sortable = [];
for ( let event in this.events ) {
sortable.push( [ this.events[ event ][ 'eventID' ], new Date( this.events[ event ][ 'date' ] ).getTime() ] );
}
sortable.sort( function( a, b ) {
return a[ 1 ] - b[ 1 ];
} );
this.loadData();
},
methods: {
loadData () {
fetch( '/admin/getAPI/getAllEvents' ).then( res => {
res.json().then( dat => {
this.events = dat[ 'live' ] ?? {};
this.eventList.drafts[ 'content' ] = dat[ 'drafts' ] ?? {};
let sortable = [];
for ( let event in this.events ) {
sortable.push( [ this.events[ event ][ 'eventID' ], new Date( this.events[ event ][ 'date' ] ).getTime() ] );
}
sortable.sort( function( a, b ) {
return a[ 1 ] - b[ 1 ];
} );
for ( let element in sortable ) {
if ( sortable[ element ][ 1 ] > this.currentDate ) {
this.eventList.upcoming.content[ sortable[ element ][ 0 ] ] = this.events[ sortable[ element ][ 0 ] ];
} else {
this.eventList.past.content[ sortable[ element ][ 0 ] ] = this.events[ sortable[ element ][ 0 ] ];
for ( let element in sortable ) {
if ( sortable[ element ][ 1 ] > this.currentDate ) {
this.eventList.upcoming.content[ sortable[ element ][ 0 ] ] = this.events[ sortable[ element ][ 0 ] ];
} else {
this.eventList.past.content[ sortable[ element ][ 0 ] ] = this.events[ sortable[ element ][ 0 ] ];
}
}
} );
} );
},
openRightClickMenu( id, event ) {
this.$refs.rclk.openRightClickMenu( event, { 'edit': { 'command': 'editEvent', 'symbol': 'edit', 'display': 'Edit event' }, 'delete': { 'command': 'deleteEvent', 'symbol': 'delete', 'display': 'Delete event' } } )
this.currentlyOpenMenu = id;
},
executeCommand( command ) {
if ( command === 'editEvent' ) {
sessionStorage.setItem( 'selectedTicket', this.currentlyOpenMenu );
this.$router.push( '/admin/events/view' );
} else if ( command === 'deleteEvent' ) {
this.$refs.popup.openPopup( 'Do you really want to delete the event ' + this.currentlyOpenMenu + '?', {}, 'confirm' );
this.currentPopup = 'delete';
}
}
},
addEvent () {
this.currentPopup = 'add';
this.$refs.popup.openPopup( 'Please give the new event a name for internal use', {}, 'text' );
},
setActiveTicket ( id ) {
sessionStorage.setItem( 'selectedTicket', id );
},
handleData ( data ) {
if ( this.currentPopup === 'delete' ) {
this.currentPopup = '';
if ( data.status === 'ok' ) {
delete this.events[ this.currentlyOpenMenu ];
}
} else if ( this.currentPopup === 'add' ) {
if ( data.status === 'ok' ) {
const options = {
method: 'post',
body: JSON.stringify( { 'event': data.data } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/admin/api/createEvent', options ).then( res => {
if ( res.status === 200 ) {
res.text().then( () => {
this.currentlyOpenMenu = '';
this.loadData();
} );
} else if ( res.status === 409 ) {
this.$refs.popup.openPopup( 'This event does already exist. Please choose a different identifier!', {}, 'string' );
}
} );
}
} else if ( this.currentPopup === 'delete' ) {
if ( data.status === 'ok' ) {
const options = {
method: 'post',
body: JSON.stringify( { 'event': data.data } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/admin/api/deleteEvent', options ).then( res => {
if ( res.status === 200 ) {
res.text().then( text => {
this.currentlyOpenMenu = '';
console.log( text );
} );
}
} );
}
}
},
}
};
</script>

View File

@@ -15,7 +15,7 @@
<div class="location-app" v-if="Object.keys( locations ).length">
<ul>
<li v-for="location in locations">
<div class="location" @click="selectLocation( location.locationID );" title="Edit this location" @contextmenu="( e ) => { e.preventDefault(); openRightClickMenu( location.locationID, e ); }">
<div class="location" @click="selectLocation( location.locationID );" title="Edit this location" @contextmenu="( e ) => { e.preventDefault(); openRightClickMenu( location.locationID, e, location['seatplan-enabled'] ); }">
<div class="location-name">
<h3>{{ location.locationID }} ({{ location.name }})</h3>
<p v-if="location['seatplan-enabled']">This location has a seatplan.</p>
@@ -127,8 +127,12 @@
}
, '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' } } )
openRightClickMenu( id, event, hasSeatplan ) {
if ( hasSeatplan ) {
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' } } )
} else {
this.$refs.rclk.openRightClickMenu( event, { 'edit': { 'command': 'editLocation', 'symbol': 'edit', 'display': 'Edit location' }, 'delete': { 'command': 'deleteLocation', 'symbol': 'delete', 'display': 'Delete location' } } )
}
this.currentlyOpenMenu = id;
},
executeCommand( command ) {
@@ -147,6 +151,21 @@
this.currentPopup = '';
if ( data.status === 'ok' ) {
delete this.locations[ this.currentlyOpenMenu ];
const options = {
method: 'post',
body: JSON.stringify( { 'location': this.currentlyOpenMenu } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/admin/api/deleteLocation', options ).then( res => {
if ( res.status === 200 ) {
res.text().then( text => {
console.log( text );
} );
}
} );
}
} else {
if ( data.status === 'settings' ) {

View File

@@ -10,6 +10,7 @@
<template>
<div>
<h2>Settings</h2>
<p>Changing any of these settings requires a restart of libreevent.</p>
<p>Currency codes used must be valid ISO 4217 codes. Read more on <a href="https://libreevent.janishutz.com/docs/admin-panel/settings#currency" target="_blank">this page</a> of the documentation <!-- https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes"--></p>
<settings v-model:settings="settings"></settings>
<table class="gateway-settings">
@@ -37,7 +38,7 @@
</div>
</div>
<rightClickMenu ref="rclk" @command="( command ) => { executeCommand( command ) }"></rightClickMenu>
<popups ref="popup" size="big"></popups>
<popups ref="popup" size="big" @data="( data ) => { handlePopupReturns( data ); }"></popups>
</div>
</template>
@@ -221,8 +222,16 @@
this.$refs.popup.openPopup( 'Do you really want to delete the user ' + this.currentlyOpenMenu + '?', {}, 'confirm' );
}
},
handlePopupReturns( message, data ) {
handlePopupReturns( data ) {
console.log( data );
if ( data.status === 'cancel' ) {
console.log( 'user canceled' );
return;
} else if ( data.status === 'settings' ) {
console.log( 'settings processing' )
} else {
console.log( 'hi' );
}
},
openRightClickMenu( id, event ) {
this.$refs.rclk.openRightClickMenu( event, { 'edit': { 'command': 'openPermissions', 'symbol': 'edit', 'display': 'Edit permissions' }, 'delete': { 'command': 'deleteUser', 'symbol': 'delete', 'display': 'Delete User' } } )

View File

@@ -10,16 +10,19 @@
<template>
<div class="details">
<h2>{{ event.name }}</h2>
<button @click="save( 'draft' )">Save as draft</button>
<div class="category-wrapper">
<h3>Event Details</h3>
<p>Event description</p>
<textarea v-model="event.description" class="big-text" cols="70" rows="3" placeholder="Event description..."></textarea>
<table class="category">
<tr>
<td>Event name</td>
<td><input type="text" v-model="event.name"></td>
</tr>
<tr>
<td>Event location</td>
<td v-if="Object.keys( locations ).length > 0">
<select v-model="event.location" class="small-text">
<option v-for="location in locations" value="location.locationID">{{ location.locationID }} ({{ location.name }})</option>
<select v-model="event.location" class="small-text" @change="handleLocationChange()">
<option v-for="location in locations" :value="location.locationID">{{ location.locationID }} ({{ location.name }})</option>
</select>
</td>
<td v-else>No locations configured yet. Configure one <router-link to="/admin/locations">here</router-link></td>
@@ -33,19 +36,42 @@
<router-link to="/admin/ticketEditor">Edit ticket layout</router-link>
</tr>
</table>
<h4>Event description</h4>
<textarea v-model="event.description" class="big-text" cols="70" rows="3" placeholder="Event description..."></textarea>
</div>
<div class="ticket-settings">
<!-- TODO: Finish -->
<h3>Age Groups</h3>
<button @click="addNew( 'ageGroup' )">Add another age group</button>
<p>With these settings you can manage and create different age categories which have to be set for every ticket.</p>
<div class="category-wrapper">
<table class="category" v-for="ageGroup in event.ageGroups">
{{ ageGroup.name }} <span class="material-symbols-outlined deleteButton" @click="deleteObject( 'ageGroup', ageGroup.id )" title="Delete age group">delete</span>
<tr class="category-details">
<td>
<div class="category-details">Group name: </div>
</td>
<td>
<input type="text" v-model="ageGroup.name">
</td>
</tr>
<tr>
<td>
<div class="category-details">Age (Leave empty for no age info): </div>
</td>
<td>
<input type="text" v-model="ageGroup.age">
</td>
</tr>
</table>
</div>
</div>
<div class="ticket-settings">
<!-- TODO: Load automatically -->
<h3>Ticket Settings</h3>
<h3>Categories</h3>
<button @click="addNew( 'category' )">Add another category</button>
<p>The foreground and background colours of the seats are used to show the customer to which category the seats belong.</p>
<div class="category-wrapper">
<table class="category" v-for="category in event.categories">
{{ category.name }}
{{ category.name }} <span class="material-symbols-outlined deleteButton" @click="deleteObject( 'category', category.id )" title="Delete category">delete</span>
<tr v-for="ageGroup in event.ageGroups">
<td>
<div class="category-details">{{ ageGroup.name }}<div style="display: inline;" v-if="ageGroup.age"> ({{ ageGroup.age }})</div>:</div>
@@ -66,12 +92,18 @@
<input type="text" data-coloris v-model="category.bg" onkeydown="return false;">
</td>
</tr>
<tr v-if="!hasSeatPlan">
<td><div class="category-details">Total tickets for this category</div></td>
<td>
<input type="number" v-model="category.ticketCount">
</td>
</tr>
</table>
</div>
</div>
<div>
<h3>Assets</h3>
<p>Here you can view and change all the marketing images for your event. All assets have to be jpg images.</p>
<p>Here you can change all the marketing images for your event. All assets have to be jpg images.</p>
<div style="display: flex;">
<picture-input
ref="logo"
@@ -106,15 +138,19 @@
<h3>General Settings</h3>
<settings v-model:settings="specialSettings"></settings>
</div>
<button @click="save( 'draft' )">Save as draft</button>
<div>
<h3>Danger Zone</h3>
<button>Go Live</button><button>Delete</button><br><br><br>
<!-- TODO: Add functions -->
<button @click="dangerZone( 'deploy' )">Go Live</button>
<button @click="dangerZone( 'undeploy' )" v-if="hasLiveVersion">Unpublish event</button>
<button @click="dangerZone( 'delete' )">Delete Event</button>
<br><br><br>
</div>
<!-- <div>
<p>Please read the documentation of this section if you want to use the requirements. It requires specific syntax to work. See <a href="https://libreevent.janishutz.com/docs/admin-panel/events#special-requirements" target="_blank">here</a> for more information</p>
</div> -->
<notifications ref="notification" location="topright"></notifications>
<popups ref="popups" size="normal" @data="( data ) => { handlePopup( data ) }"></popups>
</div>
</template>
@@ -143,16 +179,19 @@
.category-details {
margin-left: 7%;;
}
.deleteButton {
cursor: pointer;
font-size: 110%;
margin: 0;
}
</style>
<script>
import settings from '@/components/settings/settings.vue';
import notifications from '@/components/notifications/notifications.vue';
import popups from '@/components/notifications/popups.vue';
import PictureInput from 'vue-picture-input';
// TODO: When loading data form server, also load categories of this seat plan
// and from it construct the array, if not set already.
// TODO: Save info if draft or live
export default {
@@ -161,11 +200,12 @@
settings,
notifications,
PictureInput,
popups,
},
data() {
return {
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 },
event: { 'name': 'Unnamed event', 'description': '', 'location': '', 'date': '', 'categories': {}, 'ageGroups': { '1':{ 'id': 1, 'name':'Child', 'age':'0 - 15.99' }, '2':{ 'id': 2, 'name': 'Adult' } }, 'maxTickets': 2, 'eventID': 'untitled' },
specialSettings: {
// 'guest-purchase': {
// 'display': 'Enable guest purchase',
@@ -224,7 +264,13 @@
// 'value': '',
// 'type': 'text',
// },
}
},
command: '',
currentLocation: '',
toDelete: '',
currency: 'USD',
hasLiveVersion: false,
hasSeatPlan: true,
}
},
created () {
@@ -235,12 +281,23 @@
fetch( localStorage.getItem( 'url' ) + '/admin/getAPI/getLocations' ).then( res => {
res.json().then( data => {
this.locations = data;
fetch( localStorage.getItem( 'url' ) + '/admin/getAPI/getEvent?event=' + this.eventID ).then( res => {
if ( res.status === 200 ) {
res.json().then( data => {
this.event = data;
this.currentLocation = this.event.location;
this.hasSeatPlan = this.locations[ this.event.location ] ? ( this.locations[ this.event.location ][ 'seatplan-enabled' ] ?? false ) : false;
} ).catch( error => {
console.error( error );
} );
} else if ( res.status === 404 ) {
this.$router.push( '/admin/events' );
}
} );
} ).catch( error => {
console.error( error );
} );
} );
// TODO: Load data from server
},
methods: {
saveImages() {
@@ -263,11 +320,188 @@
this.$refs.notification.createNotification( 'No image selected!', 5, 'error', 'normal' );
}
},
loadLocationData() {
save ( action ) {
if ( Object.keys( this.event.ageGroups ).length > 0 && Object.keys( this.event.categories ).length > 0 ) {
for ( let ageGroup in this.event.ageGroups ) {
if ( this.event.ageGroups[ ageGroup ].name == '' ) {
this.$refs.popups.openPopup( 'One or more age groups are missing their names. Please ensure that all age groups have a name and try again!', {}, 'string' );
return;
}
}
for ( let category in this.event.categories ) {
for ( let price in this.event.categories[ category ].price ) {
if ( this.event.categories[ category ].price[ price ] < 0.5 || ( !this.event.categories[ category ].ticketCount && this.hasSeatPlan ) ) {
this.$refs.popups.openPopup( 'At least one of the prices for at least one of the categories is below the minimum of ' + this.currency + ' 0.5', {}, 'string' );
return;
}
}
}
this.event[ 'currency' ] = this.currency;
this.event[ 'isDraft' ] = true;
this.event.maxTickets = this.specialSettings[ 'maxTickets' ].value;
let url = localStorage.getItem( 'url' ) + '/admin/api/saveEvent';
if ( action === 'deploy' ) {
url = localStorage.getItem( 'url' ) + '/admin/api/deployEvent';
}
const options = {
method: 'post',
body: JSON.stringify( { 'event': this.event.eventID, 'eventData': this.event } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( url, options ).then( res => {
if ( res.status === 200 ) {
if ( action === 'deploy' ) {
this.$refs.notification.createNotification( 'Your event has been set to be live successfully!', 5, 'ok', 'normal' );
this.hasLiveVersion = true;
} else {
this.$refs.notification.createNotification( 'Saved as draft successfully!', 5, 'ok', 'normal' );
}
}
} );
} else {
this.$refs.popups.openPopup( 'Please ensure that you have at least one age group and one category defined!', {}, 'string' );
}
},
save () {
addNew( type ) {
if ( type === 'ageGroup' ) {
this.$refs.popups.openPopup( 'Choose a name for the age group', {}, 'text' );
this.command = 'addAgeGroup';
} else if ( type === 'category' ) {
this.$refs.popups.openPopup( 'Choose a name for the new category', {}, 'number', Object.keys( this.event.categories ).length + 1 );
this.command = 'addCategory';
}
},
deleteObject( type, data ) {
if ( type === 'ageGroup' ) {
this.$refs.popups.openPopup( 'Do you really want to delete this age group?', {}, 'confirm' );
this.command = 'deleteAgeGroup';
this.toDelete = data;
} else if ( type === 'category' ) {
this.$refs.popups.openPopup( 'Do you really want to delete this category', {}, 'confirm' );
this.command = 'deleteCategory';
this.toDelete = data;
}
},
handleLocationChange() {
if ( Object.keys( this.event.categories ).length > 1 && this.locations[ this.event.location ][ 'seatplan-enabled' ] ) {
this.command = 'locationChange';
this.$refs.popups.openPopup( 'You have edited the categories of this location. Changing the location now leads to data loss.', {}, 'confirm' );
} else {
this.command = 'locationChange';
this.handlePopup( { 'status': 'ok' } );
}
},
handlePopup( data ) {
if ( data.status === 'ok' ) {
if ( this.command === 'addCategory' ) {
this.command = '';
if ( !this.event.categories[ data.data ] ) {
this.event.categories[ data.data ] = { 'price': {}, 'bg': '#FFFFFF', 'fg': '#000000', 'name': 'Category ' + data.data, 'id': data.data, 'ticketCount': 1 };
for ( let ageGroup in this.event.ageGroups ) {
this.event.categories[ data.data ][ 'price' ][ ageGroup ] = 0;
}
} else {
this.$refs.popups.openPopup( 'That category already exists!', {}, 'string' );
}
} else if ( this.command === 'addAgeGroup' ) {
this.command = '';
for ( let ageGroup in this.event.ageGroups ) {
if ( this.event.ageGroups[ ageGroup ].name === data.data ) {
this.$refs.popups.openPopup( 'That age group exists already', {}, 'string' );
return;
}
}
this.event.ageGroups[ Object.keys( this.event.categories ).length + 1 ] = { 'id': Object.keys( this.event.categories ).length + 1, 'name': data.data };
for ( let ageGroup in this.event.ageGroups ) {
for ( let category in this.event.categories ) {
if ( !this.event.categories[ category ][ 'price' ][ ageGroup ] ) {
this.event.categories[ category ][ 'price' ][ ageGroup ] = 0;
}
}
}
} else if ( this.command === 'deleteCategory' ) {
delete this.event.categories[ this.toDelete ];
} else if ( this.command === 'deleteAgeGroup' ) {
delete this.event.ageGroups[ this.toDelete ];
} else if ( this.command === 'locationChange' ) {
this.currentLocation = this.event.location;
if ( this.locations[ this.event.location ][ 'seatplan-enabled' ] ) {
fetch( '/admin/getAPI/getSeatplan?location=' + this.event.location ).then( res => {
if ( res.status === 200 ) {
res.json().then( json => {
this.hasSeatPlan = this.locations[ this.event.location ][ 'seatplan-enabled' ] ?? false;
this.event.categories = {};
for ( let element in json.data ) {
if ( json.data[ element ][ 'type' ] === 'seat' || json.data[ element ][ 'type' ] === 'stand' ) {
this.event.categories[ json.data[ element ][ 'category' ] ] = { 'price': {}, 'bg': '#FFFFFF', 'fg': '#000000', 'name': 'Category ' + json.data[ element ][ 'category' ], 'id': json.data[ element ][ 'category' ], 'ticketCount': 1 };
for ( let ageGroup in this.event.ageGroups ) {
this.event.categories[ json.data[ element ][ 'category' ] ][ 'price' ][ ageGroup ] = 0;
}
}
}
} );
}
} )
}
} else if ( this.command === 'deployEvent' ) {
this.save( 'deploy' );
} else if ( this.command === 'undeployEvent' ) {
const options = {
method: 'post',
body: JSON.stringify( { 'event': sessionStorage.getItem( this.event.eventID ) } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( url, options ).then( res => {
if ( res.status === 200 ) {
this.hasLiveVersion = false;
this.$refs.notification.createNotification( 'Your event is no longer publicly visible!', 5, 'ok', 'normal' );
} else {
this.$refs.notification.createNotification( 'There was an error hiding your event', 5, 'error', 'normal' );
}
} );
} else if ( this.command === 'deleteEvent' ) {
const options = {
method: 'post',
body: JSON.stringify( { 'event': sessionStorage.getItem( this.event.eventID ) } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( url, options ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.createNotification( 'Your event has been deleted successfully!', 5, 'ok', 'normal' );
setTimeout( () => {
this.$router.push( '/admin/events' );
}, 5000 );
} else {
this.$refs.notification.createNotification( 'There was an error deleting your event', 5, 'error', 'normal' );
}
} );
}
} else if ( data.status === 'cancel' ) {
if ( this.command === 'locationChange' ) {
this.event.location = this.currentLocation;
}
}
},
dangerZone( action ) {
if ( action === 'deploy' ) {
this.$refs.popups.openPopup( 'Do you really want to go live with this event?', {}, 'confirm' );
this.command = 'deployEvent';
} else if ( action === 'undeploy' ) {
this.$refs.popups.openPopup( 'Do you really want to remove this event from the event listings?', {}, 'confirm' );
this.command = 'undeployEvent';
} else if ( type === 'delete' ) {
this.$refs.popups.openPopup( 'Do you really want to delete this event? This action cannot be undone', {}, 'confirm' );
this.command = 'deleteEvent';
}
}
}
};