free seat tracker rewrite

This commit is contained in:
2023-09-30 13:44:07 +02:00
parent fa5be769e2
commit 9e9c4b0ec6
4 changed files with 164 additions and 94 deletions

View File

@@ -18,7 +18,20 @@ class POSTHandler {
this.temporarilySelected = {}; this.temporarilySelected = {};
this.temporarilySelectedTotals = {}; this.temporarilySelectedTotals = {};
this.temporaryTotals = {};
this.freeSeats = {};
this.settings = JSON.parse( fs.readFileSync( path.join( __dirname + '/../../config/settings.config.json' ) ) ); this.settings = JSON.parse( fs.readFileSync( path.join( __dirname + '/../../config/settings.config.json' ) ) );
/*
Here, GC-Duty is scheduled to run every so often (defined in settings.config.json file, no GUI setting available.
If you are a developer and are thinking about adding a GUI setting for this, please consider that this is a
advanced setting and many people might not understand what it changes. The config file is mentioned in the
"advanced settings" section of the admin panel documentation where all the available settings in the config file
are explained in some detail.)
*/
setInterval( () => {
this.gc();
}, parseInt( this.settings.gcInterval ) * 1000 );
} }
loadData () { loadData () {
@@ -27,109 +40,163 @@ class POSTHandler {
db.getJSONData( 'events' ).then( dat => { db.getJSONData( 'events' ).then( dat => {
this.events = dat; this.events = dat;
this.ticketTotals = {}; this.ticketTotals = {};
this.detailedTicketTotals = {};
for ( let event in this.events ) { for ( let event in this.events ) {
this.ticketTotals[ event ] = this.events[ event ][ 'totalSeats' ]; this.ticketTotals[ event ] = this.events[ event ][ 'totalSeats' ];
this.detailedTicketTotals[ event ] = {};
for ( let category in this.events[ event ].categories ) {
this.detailedTicketTotals[ event ][ category ] = this.events[ event ].categories[ category ].ticketCount ?? 0;
}
} }
for ( let event in this.allSelectedSeats ) { for ( let event in this.allSelectedSeats ) {
for ( let t in this.allSelectedSeats[ event ] ) { for ( let t in this.allSelectedSeats[ event ] ) {
if ( this.allSelectedSeats[ event ][ t ][ 'count' ] ) { if ( this.allSelectedSeats[ event ][ t ][ 'count' ] ) {
this.ticketTotals[ event ] -= this.allSelectedSeats[ event ][ t ][ 'count' ]; this.ticketTotals[ event ] -= parseInt( this.allSelectedSeats[ event ][ t ][ 'count' ] );
} else { } else {
this.ticketTotals[ event ] -= 1; this.ticketTotals[ event ] -= 1;
} }
} }
} }
this.countFreeSeats();
} ); } );
} ); } );
} }
gc() {
// this function acts as the database garbage collector. TicketTimeout can be changed from the GUI.
db.getData( 'temp' ).then( tempData => {
let data = tempData;
console.info( '[ DB ] Garbage Collecting' );
for ( let item in data ) {
if ( new Date( data[ item ][ 'timestamp' ] ).getTime() + ( parseInt( this.settings.ticketTimeout ) * 1000 ) <= new Date().getTime() ) {
let dat = JSON.parse( data[ item ].data );
for ( let event in dat ) {
for ( let ticket in dat[ event ] ) {
this.temporaryTotals -= this.temporarilySelectedTotals[ data[ item ].user_id ][ event ][ ticket ];
delete this.temporarilySelectedTotals[ data[ item ].user_id ][ event ][ ticket ];
delete this.temporarilySelected[ event ][ ticket ];
}
}
db.deleteDataSimple( 'temp', 'entry_id', data[ item ].entry_id ).then( () => {
console.debug( '[ DB ] Garbage collected a session' );
} ).catch( err => {
console.error( '[ DB ] GC-ERROR: ' + err );
} );
}
}
this.countFreeSeats();
} );
}
// Add lang in the future // Add lang in the future
handleCall ( call, data, session ) { handleCall ( call, data, session ) {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
if ( call === 'reserveTicket' ) { if ( call === 'reserveTicket' ) {
// TODO: probably rewrite from scratch
if ( data.count || data.count === 0 ) { if ( data.count || data.count === 0 ) {
db.getDataSimple( 'temp', 'user_id', session.id ).then( dat => { db.getDataSimple( 'temp', 'user_id', session.id ).then( dat => {
if ( dat[ 0 ] ) { if ( dat[ 0 ] ) {
let totalTicketsPerID = {}; // data.count is the total amount of tickets currently selected
// sum up total of tickets per category (based on a sliced ID of the ticket selected, let totalTickets = 0;
// as ticketID is based on category and ageGroup)
let tickets = JSON.parse( dat[ 0 ].data )[ data.eventID ]; // sum up total of tickets
let info = JSON.parse( dat[ 0 ].data );
let tickets = info[ data.eventID ];
for ( let ticket in tickets ) { for ( let ticket in tickets ) {
if ( !totalTicketsPerID[ ticket.slice( 0, ticket.indexOf( '_' ) ) ] ) { totalTickets += tickets[ ticket ].count;
totalTicketsPerID[ ticket.slice( 0, ticket.indexOf( '_' ) ) ] = 0;
}
totalTicketsPerID[ ticket.slice( 0, ticket.indexOf( '_' ) ) ] += tickets[ ticket ].count;
} }
// find actual ticket ID and check if there are tickets for this ticketID already
const id = data.id.slice( 0, data.id.indexOf( '_' ) ); const id = data.id.slice( 0, data.id.indexOf( '_' ) );
if ( !totalTicketsPerID[ id ] ) { // check if total ticket count exceeds max tickets per order
totalTicketsPerID[ id ] = 0;
}
totalTicketsPerID[ id ] += 1;
let totalTickets = 0;
for ( let category in totalTicketsPerID ) {
totalTickets += totalTicketsPerID[ category ];
}
if ( this.settings.maxTickets !== 0 ) { if ( this.settings.maxTickets !== 0 ) {
if ( totalTickets >= this.settings.maxTickets ) { if ( totalTickets >= this.settings.maxTickets ) {
reject( { 'code': 418, 'message': 'ERR_TOO_MANY_TICKETS' } ); reject( { 'code': 418, 'message': 'ERR_TOO_MANY_TICKETS' } );
} }
} }
if ( totalTicketsPerID[ id ] <= this.detailedTicketTotals[ data.eventID ][ id ] ) {
let info = {}; if ( !this.temporarilySelectedTotals[ session.id ] ) {
info[ data.eventID ] = tickets; this.temporarilySelectedTotals[ session.id ] = {};
if ( data.count < 1 ) { this.temporarilySelectedTotals[ session.id ][ data.eventID ] = {};
if ( Object.keys( info[ data.eventID ] ).length < 1 ) { }
delete info[ data.eventID ];
// check if total ticket count exceeds max tickets for this event and adjust if necessary
let ticketCount = data.count;
if ( this.events[ data.eventID ].maxTickets == 0 || totalTickets < this.events[ data.eventID ].maxTickets ) {
// check if enough tickets are still available
if ( totalTickets < this.ticketTotals[ data.eventID ] - this.temporarilySelectedTotals[ session.id ][ data.eventID ][ id ] ) {
if ( data.count > this.ticketTotals[ data.eventID ] ) {
ticketCount = this.ticketTotals[ data.eventID ];
}
info[ data.eventID ] = tickets;
if ( data.count < 1 ) {
if ( Object.keys( info[ data.eventID ] ).length < 1 ) {
delete info[ data.eventID ];
} else {
delete info[ data.eventID ][ data.id ];
}
} else { } else {
delete info[ data.eventID ][ data.id ]; info[ data.eventID ][ data.id ] = data;
} }
} else {
info[ data.eventID ][ data.id ] = data;
}
let ticketCount = data.count;
const maxTickets = this.detailedTicketTotals[ data.eventID ][ data.id.slice( 0, data.id.indexOf( '_' ) ) ];
if ( ticketCount > maxTickets ) {
ticketCount = maxTickets;
}
if ( maxTickets > 0 ) {
db.writeDataSimple( 'temp', 'user_id', session.id, { 'user_id': session.id, 'timestamp': new Date().toString(), 'data': JSON.stringify( info ) } ); db.writeDataSimple( 'temp', 'user_id', session.id, { 'user_id': session.id, 'timestamp': new Date().toString(), 'data': JSON.stringify( info ) } );
this.temporarilySelected[ id ] = info; if ( !this.temporarilySelected[ data.eventID ] ) {
if ( !this.temporarilySelectedTotals[ data.eventID ] ) { this.temporarilySelected[ data.eventID ] = {};
this.ticketTotals[ data.eventID ] = 0;
} }
this.ticketTotals[ data.eventID ] -= ticketCount; if ( !this.temporaryTotals[ data.eventID ] ) {
this.temporaryTotals[ data.eventID ] = 0;
}
this.temporarilySelected[ data.eventID ][ id ] = info;
this.temporaryTotals[ data.eventID ] -= this.temporarilySelectedTotals[ session.id ][ data.eventID ][ id ];
this.temporaryTotals[ data.eventID ] += ticketCount;
this.temporarilySelectedTotals[ session.id ][ data.eventID ][ id ] = ticketCount;
this.countFreeSeats();
resolve( { 'status': 'ok', 'ticketCount': ticketCount } ); resolve( { 'status': 'ok', 'ticketCount': ticketCount } );
} else { } else {
reject( { 'code': 409, 'message': 'ERR_ALL_OCCUPIED' } ); reject( { 'code': 409, 'message': 'ERR_ALL_OCCUPIED' } );
} }
} else {
reject( { 'code': 418, 'message': 'ERR_TOO_MANY_TICKETS' } );
} }
} else { } else {
let info = {}; // find actual ticket ID and check if there are tickets for this ticketID already
info[ data.eventID ] = {}; const id = data.id.slice( 0, data.id.indexOf( '_' ) );
info[ data.eventID ][ data.id ] = data;
let ticketCount = data.count; if ( !this.temporarilySelectedTotals[ session.id ] ) {
const maxTickets = this.detailedTicketTotals[ data.eventID ][ data.category ]; this.temporarilySelectedTotals[ session.id ] = {};
if ( ticketCount > maxTickets ) { this.temporarilySelectedTotals[ session.id ][ data.eventID ] = {};
ticketCount = maxTickets; this.temporarilySelectedTotals[ session.id ][ data.eventID ][ id ] = 0;
} }
if ( maxTickets > 0 ) {
db.writeDataSimple( 'temp', 'user_id', session.id, { 'user_id': session.id, 'timestamp': new Date().toString(), 'data': JSON.stringify( info ) } ); let ticketCount = data.count;
this.allSelectedSeats[ data.id ] = info; if ( this.events[ data.eventID ].maxTickets == 0 || ticketCount < this.events[ data.eventID ].maxTickets ) {
this.ticketTotals[ data.eventID ] -= ticketCount;
resolve( { 'status': 'ok', 'ticketCount': ticketCount } ); // check if enough tickets are still available
if ( ticketCount < this.ticketTotals[ data.eventID ] - this.temporarilySelectedTotals[ session.id ][ data.eventID ][ id ] ) {
if ( data.count < this.ticketTotals[ data.eventID ] ) {
ticketCount = this.ticketTotals[ data.eventID ];
// Create details
let info = {};
info[ data.eventID ] = {};
info[ data.eventID ][ id ] = data;
if ( !this.temporarilySelected[ data.eventID ] ) {
this.temporarilySelected[ data.eventID ] = {};
}
if ( !this.temporaryTotals[ data.eventID ] ) {
this.temporaryTotals[ data.eventID ] = 0;
}
db.writeDataSimple( 'temp', 'user_id', session.id, { 'user_id': session.id, 'timestamp': new Date().toString(), 'data': JSON.stringify( info ) } );
this.temporarilySelected[ data.eventID ][ id ] = info;
this.temporaryTotals[ data.eventID ] -= this.temporarilySelectedTotals[ session.id ][ data.eventID ][ id ];
this.temporaryTotals[ data.eventID ] += ticketCount;
this.temporarilySelectedTotals[ session.id ][ data.eventID ][ id ] = ticketCount;
this.countFreeSeats();
resolve( 'ok' );
} else {
reject( { 'code': 409, 'message': 'ERR_ALL_OCCUPIED' } );
}
} else {
reject( { 'code': 409, 'message': 'ERR_ALL_OCCUPIED' } );
}
} else { } else {
reject( { 'code': 409, 'message': 'ERR_ALL_OCCUPIED' } ); reject( { 'code': 418, 'message': 'ERR_TOO_MANY_TICKETS' } );
} }
} }
} ); } );
@@ -137,20 +204,34 @@ class POSTHandler {
if ( !this.allSelectedSeats[ data.eventID ] ) { if ( !this.allSelectedSeats[ data.eventID ] ) {
this.allSelectedSeats[ data.eventID ] = {}; this.allSelectedSeats[ data.eventID ] = {};
} }
if ( this.allSelectedSeats[ data.eventID ][ data.id ] ) { if ( !this.temporarilySelected[ data.eventID ] ) {
this.temporarilySelected[ data.eventID ] = {};
}
if ( this.allSelectedSeats[ data.eventID ][ data.id ] || this.temporarilySelected[ data.eventID ][ data.id ] ) {
reject( { 'code': 409, 'message': 'ERR_ALREADY_SELECTED' } ); reject( { 'code': 409, 'message': 'ERR_ALREADY_SELECTED' } );
} else { } else {
let info = {}; let info = {};
info[ data.eventID ] = {}; info[ data.eventID ] = {};
info[ data.eventID ][ data.id ] = data; info[ data.eventID ][ data.id ] = data;
db.writeDataSimple( 'temp', 'user_id', session.id, { 'user_id': session.id, 'timestamp': new Date().toString(), 'data': JSON.stringify( info ) } ).catch( err => { db.writeDataSimple( 'temp', 'user_id', session.id, { 'user_id': session.id, 'timestamp': new Date().toString(), 'data': JSON.stringify( info ) } ).then( () => {
if ( !this.temporarilySelectedTotals[ session.id ] ) {
this.temporarilySelectedTotals[ session.id ] = {};
this.temporarilySelectedTotals[ session.id ][ data.eventID ] = {};
}
if ( !this.temporaryTotals[ data.eventID ] ) {
this.temporaryTotals[ data.eventID ] = 0;
}
this.temporarilySelected[ data.eventID ] = info[ data.eventID ];
this.temporaryTotals[ data.eventID ] += 1;
this.temporarilySelectedTotals[ session.id ][ data.eventID ][ data.id ] = 1;
this.countFreeSeats();
resolve( 'ok' );
} ).catch( err => {
console.error( err ); console.error( err );
} ); } );
resolve( 'ok' );
} }
} }
} else if ( call === 'deselectTicket' ) { } else if ( call === 'deselectTicket' ) {
// TODO: probably rewrite from scratch
db.getDataSimple( 'temp', 'user_id', session.id ).then( dat => { db.getDataSimple( 'temp', 'user_id', session.id ).then( dat => {
let transmit = {}; let transmit = {};
if ( dat[ 0 ] ) { if ( dat[ 0 ] ) {
@@ -168,11 +249,13 @@ class POSTHandler {
reject( { 'code': 404, 'message': 'ERR_DATA_NOT_FOUND' } ); reject( { 'code': 404, 'message': 'ERR_DATA_NOT_FOUND' } );
} }
const allSeats = this.allSelectedSeats[ data.eventID ]; const allSeats = this.temporarilySelected[ data.eventID ];
for ( let seat in allSeats ) { for ( let seat in allSeats ) {
if ( allSeats[ seat ].component === data.component ) { if ( allSeats[ seat ].component === data.component ) {
if ( allSeats[ seat ].id === data.id ) { if ( allSeats[ seat ].id === data.id ) {
delete this.allSelectedSeats[ data.eventID ][ seat ]; this.temporaryTotals[ data.eventID ] -= 1;
delete this.temporarilySelectedTotals[ session.id ][ data.eventID ][ seat ];
delete this.temporarilySelected[ data.eventID ][ seat ];
} }
} }
} }
@@ -182,6 +265,7 @@ class POSTHandler {
} }
db.writeDataSimple( 'temp', 'user_id', session.id, { 'user_id': session.id, 'data': JSON.stringify( transmit ) } ).then( () => { db.writeDataSimple( 'temp', 'user_id', session.id, { 'user_id': session.id, 'data': JSON.stringify( transmit ) } ).then( () => {
this.countFreeSeats();
resolve( 'ok' ); resolve( 'ok' );
} ).catch( error => { } ).catch( error => {
console.error( error ); console.error( error );
@@ -208,11 +292,21 @@ class POSTHandler {
} }
getReservedSeats ( event ) { getReservedSeats ( event ) {
return this.allSelectedSeats[ event ] ? Object.values( this.allSelectedSeats[ event ] ) : {}; return this.allSelectedSeats[ event ] ? Object.values( Object.assign( {}, this.allSelectedSeats[ event ], this.temporarilySelected[ event ] ) ) : {};
}
countFreeSeats() {
this.freeSeats = {};
for ( let el in this.ticketTotals ) {
this.freeSeats[ el ] = this.ticketTotals[ el ];
}
for ( let el in this.temporaryTotals ) {
this.freeSeats[ el ] -= this.temporaryTotals[ el ];
}
} }
getFreeSeatsCount() { getFreeSeatsCount() {
return this.ticketTotals; return this.freeSeats;
} }
} }

View File

@@ -209,28 +209,3 @@ module.exports.saveSettings = ( settings ) => {
} }
fs.writeFileSync( path.join( __dirname + '/../../config/settings.config.json' ), settingsToSave ); fs.writeFileSync( path.join( __dirname + '/../../config/settings.config.json' ), settingsToSave );
}; };
const gc = () => {
// this function acts as the database garbage collector. TicketTimeout can be changed from the GUI.
this.getData( 'temp' ).then( tempData => {
let data = tempData;
console.info( '[ DB ] Garbage Collecting' );
for ( let item in data ) {
if ( new Date( data[ item ][ 'timestamp' ] ).getTime() + ( parseInt( settings.ticketTimeout ) * 1000 ) > new Date().getTime() ) {
delete data[ item ];
console.debug( '[ DB ] Garbage collected a session' );
}
}
} );
};
/*
Here, GC-Duty is scheduled to run every so often (defined in settings.config.json file, no GUI setting available.
If you are a developer and are thinking about adding a GUI setting for this, please consider that this is a
advanced setting and many people might not understand what it changes. The config file is mentioned in the
"advanced settings" section of the admin panel documentation where all the available settings in the config file
are explained in some detail.)
*/
setInterval( () => {
gc();
}, parseInt( settings.gcInterval ) * 1000 );

View File

@@ -398,7 +398,7 @@
// Make call to server to deselect ticket // Make call to server to deselect ticket
const options = { const options = {
method: 'post', method: 'post',
body: JSON.stringify( { 'id': seat[ 'id' ], 'eventID': this.event.eventID, 'component': seat.component } ), body: JSON.stringify( { 'id': seat[ 'id' ], 'eventID': this.event.eventID, 'component': seat.componentID } ),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'charset': 'utf-8' 'charset': 'utf-8'

View File

@@ -48,6 +48,7 @@
self.$refs.notification.cancelNotification( startNotification ); self.$refs.notification.cancelNotification( startNotification );
self.$refs.notification.createNotification( 'Your tickets are ready! Starting download...', 10, 'progress', 'normal' ); self.$refs.notification.createNotification( 'Your tickets are ready! Starting download...', 10, 'progress', 'normal' );
localStorage.removeItem( 'cart' ); localStorage.removeItem( 'cart' );
fetch( '/getAPI/reloadData' ).catch( () => {} );
setTimeout( () => { setTimeout( () => {
open( '/tickets/tickets.pdf' ); open( '/tickets/tickets.pdf' );
source.close(); source.close();