@digital-blueprint/checkin-app
Version:
[GitHub Repository](https://github.com/digital-blueprint/checkin-app) | [npmjs package](https://www.npmjs.com/package/@digital-blueprint/checkin-app) | [Unpkg CDN](https://unpkg.com/browse/@digital-blueprint/checkin-app/) | [Checkin Bundle](https://github
724 lines (660 loc) • 24 kB
JavaScript
import DBPLitElement from '@dbp-toolkit/common/dbp-lit-element';
import {getStackTrace} from '@dbp-toolkit/common/error';
import {send} from '@dbp-toolkit/common/notification';
/**
* Dummy function to mark strings as i18next keys for i18next-scanner
*
* @param {string} key
* @param {object} [options]
* @returns {string} The key param as is
*/
function i18nKey(key, options) {
return key;
}
export default class DBPCheckInLitElement extends DBPLitElement {
constructor() {
super();
this.isSessionRefreshed = false;
this.auth = {};
}
static get properties() {
return {
...super.properties,
auth: {type: Object},
};
}
connectedCallback() {
super.connectedCallback();
this._loginStatus = '';
this._loginState = [];
}
/**
* Request a re-render every time isLoggedIn()/isLoading() changes
*/
_updateAuth() {
this._loginStatus = this.auth['login-status'];
let newLoginState = [this.isLoggedIn(), this.isLoading()];
if (this._loginState.toString() !== newLoginState.toString()) {
this.requestUpdate();
}
this._loginState = newLoginState;
}
update(changedProperties) {
changedProperties.forEach((oldValue, propName) => {
switch (propName) {
case 'auth':
this._updateAuth();
break;
}
});
super.update(changedProperties);
}
/**
* Returns if a person is set in or not
*
* @returns {boolean} true or false
*/
isLoggedIn() {
return this.auth.person !== undefined && this.auth.person !== null;
}
/**
* Returns true if a person has successfully logged in
*
* @returns {boolean} true or false
*/
isLoading() {
if (this._loginStatus === 'logged-out') return false;
return !this.isLoggedIn() && this.auth.token !== undefined;
}
/**
* Send a fetch to given url with given options
*
* @param url
* @param options
* @returns {object} response (error or result)
*/
async httpGetAsync(url, options) {
let response = await fetch(url, options)
.then((result) => {
if (!result.ok) throw result;
return result;
})
.catch((error) => {
return error;
});
return response;
}
/**
* Gets the active checkins of the current logged in user
*
* @returns {object} response
*/
async getActiveCheckIns() {
let response;
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/ld+json',
Authorization: 'Bearer ' + this.auth.token,
},
};
response = await this.httpGetAsync(
this.entryPointUrl + '/checkin/check-in-actions',
options
);
return response;
}
/**
* Checkout at a specific location
*
* @param locationHash
* @param seatNumber (optional)
* @returns {object} response
*/
async sendCheckOutRequest(locationHash, seatNumber) {
let response;
let body = {
location: '/checkin/places/' + locationHash,
seatNumber: parseInt(seatNumber),
};
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/ld+json',
Authorization: 'Bearer ' + this.auth.token,
},
body: JSON.stringify(body),
};
response = await this.httpGetAsync(
this.entryPointUrl + '/checkin/check-out-actions',
options
);
return response;
}
/**
* Checkin at a specific location
*
* @param locationHash
* @param seatNumber (optional)
* @returns {object} response
*/
async sendCheckInRequest(locationHash, seatNumber) {
let body = {
location: '/checkin/places/' + locationHash,
seatNumber: parseInt(seatNumber),
};
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/ld+json',
Authorization: 'Bearer ' + this.auth.token,
},
body: JSON.stringify(body),
};
return await this.httpGetAsync(this.entryPointUrl + '/checkin/check-in-actions', options);
}
/**
* Sends an analytics error event for the request of a room
*
* @param category
* @param action
* @param room
* @param responseData
*/
async sendErrorAnalyticsEvent(category, action, room, responseData = {}) {
let responseBody = {};
// Use a clone of responseData to prevent "Failed to execute 'json' on 'Response': body stream already read"
// after this function, but still a TypeError will occur if .json() was already called before this function
try {
responseBody = await responseData.clone().json();
} catch (e) {
// NOP
}
const data = {
status: responseData.status || '',
url: responseData.url || '',
description: responseBody['hydra:description'] || '',
room: room,
// get 5 items from the stack trace
stack: getStackTrace().slice(1, 6),
};
// console.log("sendErrorEvent", data);
this.sendSetPropertyEvent('analytics-event', {
category: category,
action: action,
name: JSON.stringify(data),
});
}
/**
* Sends a Checkin request and do error handling and parsing
* Include message for user when it worked or not
* Saves invalid QR codes in array in this.wrongHash, so no multiple requests are send
*
* Possible paths: checkin, refresh session, invalid input, roomhash wrong, invalid seat number
* no seat number, already checkedin, no permissions, any other errors, location hash empty
*
* @param locationHash
* @param seatNumber
* @param locationName
* @param category
* @param refresh (default = false)
* @param setAdditional (default = false)
*/
async doCheckIn(
locationHash,
seatNumber,
locationName,
category,
refresh = false,
setAdditional = false
) {
const i18n = this._i18n;
// Error: no location hash detected
if (locationHash.length <= 0) {
this.saveWrongHashAndNotify(
i18n.t('check-in.error-title'),
i18n.t('check-in.error-body'),
locationHash,
seatNumber
);
this.sendSetPropertyEvent('analytics-event', {
category: category,
action: 'CheckInFailedNoLocationHash',
});
return;
}
let responseData = await this.sendCheckInRequest(locationHash, seatNumber);
await this.checkCheckinResponse(
responseData,
locationHash,
seatNumber,
locationName,
category,
refresh,
setAdditional
);
}
/**
* Parse the response of a checkin or guest checkin request
* Include message for user when it worked or not
* Saves invalid QR codes in array in this.wrongHash, so no multiple requests are send
*
* Possible paths: checkin, refresh session, invalid input, roomhash wrong, invalid seat number
* no seat number, already checkedin, no permissions, any other errors, location hash empty
*
* @param responseData
* @param locationHash
* @param seatNumber
* @param locationName
* @param category
* @param refresh (default = false)
* @param setAdditional (default = false)
*/
async checkCheckinResponse(
responseData,
locationHash,
seatNumber,
locationName,
category,
refresh = false,
setAdditional = false
) {
const i18n = this._i18n;
let status = responseData.status;
let responseBody = await responseData.clone().json();
switch (status) {
case 201:
if (setAdditional) {
this.checkedInRoom = responseBody.location.name;
this.checkedInSeat = responseBody.seatNumber;
this.checkedInEndTime = responseBody.endTime;
this.identifier = responseBody['identifier'];
this.agent = responseBody['agent'];
this.stopQRReader();
this.isCheckedIn = true;
this._('#text-switch')._active = '';
locationName = responseBody.location.name;
}
if (refresh) {
send({
summary: i18n.t('check-in.success-refresh-title', {room: locationName}),
body: i18n.t('check-in.success-refresh-body', {room: locationName}),
type: 'success',
timeout: 5,
});
this.sendSetPropertyEvent('analytics-event', {
category: category,
action: 'RefreshSuccess',
name: locationName,
});
} else if (category === 'GuestCheckInRequest') {
send({
summary: i18n.t('guest-check-in.success-checkin-title', {
email: this.guestEmail,
}),
body: i18n.t('guest-check-in.success-checkin-body', {
email: this.guestEmail,
}),
type: 'success',
timeout: 5,
});
locationName = responseBody.location.name;
//Refresh necessary fields and values - keep time and place because it is nice to have for the next guest
this._('#email-field').value = '';
this.guestEmail = '';
this._('#select-seat').value = '';
this.seatNr = '';
this.isEmailSet = false;
} else {
send({
summary: i18n.t('check-in.success-checkin-title', {room: locationName}),
body:
seatNumber !== ''
? i18n.t('check-in.success-checkin-seat-body', {
room: locationName,
seat: seatNumber,
})
: i18n.t('check-in.success-checkin-body', {room: locationName}),
type: 'success',
timeout: 5,
});
}
this.sendSetPropertyEvent('analytics-event', {
category: category,
action: 'CheckInSuccess',
name: locationName,
});
await this.checkOtherCheckins(locationHash, seatNumber);
break;
// Invalid Input
case 400:
this.saveWrongHashAndNotify(
i18n.t('check-in.invalid-input-title'),
i18n.t('check-in.invalid-input-body'),
locationHash,
seatNumber
);
this.sendSetPropertyEvent('analytics-event', {
category: category,
action: 'CheckInFailed400',
name: locationName,
});
break;
// No permissions
case 403:
this.saveWrongHashAndNotify(
i18n.t('check-in.no-permission-title'),
i18n.t('check-in.no-permission-body'),
locationHash,
seatNumber
);
await this.sendErrorAnalyticsEvent(
category,
'CheckInFailed403',
locationName,
responseData
);
break;
// Error if room not exists
case 404:
this.saveWrongHashAndNotify(
i18n.t('check-in.hash-false-title'),
i18n.t('check-in.hash-false-body'),
locationHash,
seatNumber
);
this.sendSetPropertyEvent('analytics-event', {
category: category,
action: 'CheckInFailed404',
name: locationName,
});
break;
// Can't checkin at provided place
case 424:
await this.sendErrorAnalyticsEvent(
category,
'CheckInFailed424',
locationName,
responseData
);
await this.checkErrorDescription(
responseBody['hydra:description'],
locationHash,
seatNumber
);
break;
// Error: something else doesn't work
default:
this.saveWrongHashAndNotify(
i18n.t('check-in.error-title'),
i18n.t('check-in.error-body'),
locationHash,
seatNumber
);
this.sendSetPropertyEvent('analytics-event', {
category: category,
action: 'CheckInFailed',
name: locationName,
});
break;
}
}
async checkErrorDescription(errorDescription, locationHash, seatNumber) {
const i18n = this._i18n;
console.log(errorDescription);
switch (errorDescription) {
// Error: invalid seat number
case 'seatNumber must not exceed maximumPhysicalAttendeeCapacity of location!':
case 'seatNumber too low!':
this.saveWrongHashAndNotify(
i18n.t('check-in.invalid-seatnr-title'),
i18n.t('check-in.invalid-seatnr-body'),
locationHash,
seatNumber
);
break;
// Error: no seat numbers at provided room
case "Location doesn't have any seats activated, you cannot set a seatNumber!":
this.saveWrongHashAndNotify(
i18n.t('check-in.no-seatnr-title'),
i18n.t('check-in.no-seatnr-body'),
locationHash,
seatNumber
);
break;
// Error: no seat given
case 'Location has seats activated, you need to set a seatNumber!':
this.saveWrongHashAndNotify(
i18n.t('guest-check-in.no-seatnr-title'),
i18n.t('guest-check-in.no-seatnr-body'),
locationHash,
seatNumber
);
break;
// Error: you are already checked in here
case 'There are already check-ins at the location with provided seat for the current user!':
await this.checkOtherCheckins(locationHash, seatNumber, true);
break;
// Error: Email is already checked in here
case 'There are already check-ins at the location with provided seat for the email address!':
send({
summary: i18n.t('guest-check-in.already-checkin-title'),
body: i18n.t('guest-check-in.already-checkin-body'),
type: 'warning',
timeout: 5,
});
break;
default:
// Error: the endTime is too high
if (errorDescription.includes("The endDate can't be after ")) {
send({
summary: i18n.t('guest-check-in.max-time-title'),
body: i18n.t('guest-check-in.max-time-body'),
type: 'danger',
timeout: 5,
});
}
break;
}
}
async checkOtherCheckins(locationHash, seatNumber, checkOtherSeats = false) {
const i18n = this._i18n;
let getActiveCheckInsResponse = await this.getActiveCheckIns();
if (getActiveCheckInsResponse.status !== 200) {
this.saveWrongHashAndNotify(
i18n.t('check-in.error-title'),
i18n.t('check-in.error-body'),
locationHash,
seatNumber
);
return;
}
let getActiveCheckInsBody = await getActiveCheckInsResponse.json();
let checkInsArray = getActiveCheckInsBody['hydra:member'];
let checkActiveCheckin = checkInsArray.filter(
(x) =>
x.location.identifier === locationHash &&
x.seatNumber === (seatNumber === '' ? null : parseInt(seatNumber))
);
if (checkActiveCheckin.length === 0) return -1;
this.checkinCount = checkInsArray.length;
if (checkInsArray.length > 1) {
this.status = {
summary: i18nKey('check-in.other-checkins-notification-title'),
body: i18nKey('check-in.other-checkins-notification-body', {count: 0}),
type: 'warning',
options: {count: checkInsArray.length - 1},
};
}
if (!checkOtherSeats) return;
let atActualRoomCheckIn = checkInsArray.filter(
(x) =>
x.location.identifier === locationHash &&
x.seatNumber === (seatNumber === '' ? null : parseInt(seatNumber))
);
if (atActualRoomCheckIn.length !== 1) {
this.saveWrongHashAndNotify(
i18n.t('check-in.error-title'),
i18n.t('check-in.error-body'),
locationHash,
seatNumber
);
return;
}
this.checkedInRoom = atActualRoomCheckIn[0].location.name;
this.checkedInEndTime = atActualRoomCheckIn[0].endTime;
this.checkedInSeat = atActualRoomCheckIn[0].seatNumber;
this.stopQRReader();
this.isCheckedIn = true;
this._('#text-switch')._active = '';
send({
summary: i18n.t('check-in.already-checkin-title'),
body: i18n.t('check-in.already-checkin-body'),
type: 'warning',
timeout: 5,
});
}
saveWrongHashAndNotify(title, body, locationHash, seatNumber) {
send({
summary: title,
body: body,
type: 'danger',
timeout: 5,
});
if (this.wrongHash) this.wrongHash.push(locationHash + '-' + seatNumber);
}
/**
* Do a refresh: sends a checkout request, if this is successfully init a checkin
* sends an error notification if something wen wrong
*
* @param locationHash
* @param seatNumber
* @param locationName
* @param category
* @param setAdditionals (default = false)
*/
async refreshSession(locationHash, seatNumber, locationName, category, setAdditionals = false) {
const i18n = this._i18n;
let responseCheckout = await this.sendCheckOutRequest(locationHash, seatNumber);
if (responseCheckout.status === 201) {
await this.doCheckIn(
locationHash,
seatNumber,
locationName,
category,
true,
setAdditionals
);
return;
}
if (responseCheckout.status === 424) {
//check if there is a checkin at wanted seat
let check = await this.checkOtherCheckins(this.locationHash, this.seatNumber);
if (check === -1) {
this.sendSetPropertyEvent('analytics-event', {
category: 'CheckInRequest',
action: 'CheckOutFailedNoCheckin',
name: this.checkedInRoom,
});
await this.doCheckIn(
locationHash,
seatNumber,
locationName,
category,
true,
setAdditionals
);
return;
}
}
send({
summary: i18n.t('check-in.refresh-failed-title'),
body: i18n.t('check-in.refresh-failed-body', {room: locationName}),
type: 'warning',
timeout: 5,
});
await this.sendErrorAnalyticsEvent(
'CheckInRequest',
'RefreshFailed',
locationName,
responseCheckout
);
}
/**
* Parse a incoming date to a readable date
*
* @param date
* @returns {string} readable date
*/
getReadableDate(date) {
const i18n = this._i18n;
let newDate = new Date(date);
let month = newDate.getMonth() + 1;
return (
i18n.t('check-in.checked-in-at', {
clock: newDate.getHours() + ':' + ('0' + newDate.getMinutes()).slice(-2),
}) +
' ' +
newDate.getDate() +
'.' +
month +
'.' +
newDate.getFullYear()
);
}
async checkCheckoutResponse(
response,
locationHash,
seatNumber,
locationName,
category,
that = null,
setAdditional = function () {}
) {
const i18n = this._i18n;
if (response.status === 201) {
send({
summary: i18n.t('check-out.checkout-success-title'),
body: i18n.t('check-out.checkout-success-body', {room: locationName}),
type: 'success',
timeout: 5,
});
this.sendSetPropertyEvent('analytics-event', {
category: category,
action: 'CheckOutSuccess',
name: locationName,
});
setAdditional(that);
} else if (response.status === 424) {
//check if there is a checkin at wanted seat
let check = await this.checkOtherCheckins(locationHash, seatNumber);
if (check === -1) {
this.sendSetPropertyEvent('analytics-event', {
category: category,
action: 'CheckOutFailedNoCheckin',
name: locationName,
});
setAdditional(that);
send({
summary: i18n.t('check-out.already-checked-out-title'),
body: i18n.t('check-out.already-checked-out-body', {room: locationName}),
type: 'warning',
timeout: 5,
});
}
} else {
send({
summary: i18n.t('check-out.checkout-failed-title'),
body: i18n.t('check-out.checkout-failed-body', {room: locationName}),
type: 'warning',
timeout: 5,
});
await this.sendErrorAnalyticsEvent(
category,
'CheckOutFailed',
this.checkedInRoom,
response
);
}
}
}