UNPKG

@dbp-topics/greenlight

Version:

[GitHub Repository](https://github.com/digital-blueprint/greenlight-frontend) | [npmjs package](https://www.npmjs.com/package/@dbp-topics/greenlight) | [Unpkg CDN](https://unpkg.com/browse/@dbp-topics/greenlight/) | [Greenlight Bundle](https://gitlab.tugr

771 lines (708 loc) 28.9 kB
import {css, html} from 'lit'; import DBPGreenlightTicketLitElement, {getTicketCss} from './dbp-greenlight-ticket-lit-element'; import {ScopedElementsMixin} from '@open-wc/scoped-elements'; import * as commonUtils from '@dbp-toolkit/common/utils'; import {InlineNotification, combineURLs} from '@dbp-toolkit/common'; import {classMap} from 'lit/directives/class-map.js'; import * as commonStyles from '@dbp-toolkit/common/styles'; import * as CheckinStyles from './styles'; import {send} from '@dbp-toolkit/common/notification'; import qrcode from 'qrcode-generator'; import {InfoTooltip} from '@dbp-toolkit/tooltip'; import {Activity} from './activity'; import metadata from './dbp-show-active-tickets.metadata.json'; class ShowActiveTickets extends ScopedElementsMixin(DBPGreenlightTicketLitElement) { constructor() { super(); this.activity = new Activity(metadata); this.activeTickets = []; this.locationName = 'Ticket'; this.currentTicket = {}; this.greenPassHash = ''; this.hasValidProof = false; this.isSelfTest = false; this.loadingTickets = true; this.preCheck = false; this.validationFailed = false; this.isFullProof = false; } static get scopedElements() { return { ...super.scopedElements, 'dbp-inline-notification': InlineNotification, 'dbp-info-tooltip': InfoTooltip, }; } static get properties() { return { ...super.properties, activeTickets: {type: Array, attribute: false}, locationName: {type: String, attribute: 'preselected-option'}, currentTicket: {type: Object, attribute: false}, greenPassHash: {type: String, attribute: false}, hasValidProof: {type: Boolean, attribute: false}, isSelfTest: {type: Boolean, attribute: false}, loadingTickets: {type: Boolean, attribute: false}, isFullProof: {type: Boolean, attribute: false}, }; } loginCallback() { super.loginCallback(); this.getListOfActiveTickets(); this.checkForValidProofLocalWrapper(); } /** * Parse an activeTicket response and return a list * * @param response * @returns {Array} list */ parseActiveTickets(response) { let list = []; let numTypes = parseInt(response['hydra:totalItems']); if (isNaN(numTypes)) { numTypes = 0; } for (let i = 0; i < numTypes; i++) { list[i] = response['hydra:member'][i]; } return list; } /** * Sends a delete Ticket request * * @param ticketID */ async sendDeleteTicketRequest(ticketID) { const options = { method: 'DELETE', headers: { Authorization: 'Bearer ' + this.auth.token, }, }; return await this.httpGetAsync( combineURLs(this.entryPointUrl, '/greenlight/permits/' + encodeURIComponent(ticketID)), options ); } /** * Gets a specific ticket * * @param ticketID * @returns {object} response */ async getActiveTicketRequest(ticketID) { const options = { method: 'GET', headers: { 'Content-Type': 'application/ld+json', Authorization: 'Bearer ' + this.auth.token, }, }; // const additionalInformation = this.hasValidProof && !this.isSelfTest ? 'local-proof' : ''; let additionalInformation; if (this._hasMultipleTicketTypes() && this.hasValidProof) { additionalInformation = this.isFullProof ? 'full' : 'partial'; } else if (!this._hasMultipleTicketTypes() && this.hasValidProof && !this.isSelfTest) { additionalInformation = 'local-proof'; } else { additionalInformation = ''; } return await this.httpGetAsync( combineURLs( this.entryPointUrl, '/greenlight/permits/' + encodeURIComponent(ticketID) + '?additional-information=' + encodeURIComponent(additionalInformation) ), options ); } /** * Gets the active tickets * * @returns {object} response */ async getActiveTicketsRequest() { const options = { method: 'GET', headers: { 'Content-Type': 'application/ld+json', Authorization: 'Bearer ' + this.auth.token, }, }; // const additionalInformation = this.hasValidProof ? 'local-proof' : ''; let additionalInformation; if (this._hasMultipleTicketTypes() && this.hasValidProof) { additionalInformation = this.isFullProof ? 'full' : 'partial'; } else if (!this._hasMultipleTicketTypes() && this.hasValidProof && !this.isSelfTest) { additionalInformation = 'local-proof'; } else { additionalInformation = ''; } return await this.httpGetAsync( combineURLs( this.entryPointUrl, '/greenlight/permits?additional-information=' + encodeURIComponent(additionalInformation) ), options ); } /** * Updates a ticket and sets a timer for next update * Notifies the user if something went wrong * * @returns {boolean} */ async updateTicket() { if ( this.ticketOpen === false || (this.currentTicket && Object.keys(this.currentTicket).length === 0) ) return false; const i18n = this._i18n; let responseData = await this.getActiveTicketRequest(this.currentTicket.identifier); let responseBody = ''; try { responseBody = await responseData.clone().json(); } catch (e) { this.setTimeoutIsSet = false; this.showReloadButton = true; return false; } let ret; switch (responseData.status) { case 200: // Success this.sendSuccessAnalyticsEvent('UpdateTicketRequest', 'Success', ''); this.showReloadButton = false; this.currentTicket = responseBody; this.ticketImage = responseBody.image; this.setTimer(responseBody.imageValidFor * 1000 + 1000); ret = true; break; case 401: this.sendErrorAnalyticsEvent( 'UpdateTicketRequest', 'LoggedOut', this.location, responseData ); this.getListOfActiveTickets(); send({ summary: i18n.t('show-active-tickets.logged-out-title'), body: i18n.t('show-active-tickets.logged-out-body', {place: this.locationName}), type: 'warning', timeout: 5, }); this.showReloadButton = false; this.setTimeoutIsSet = false; ret = false; break; case 404: this.sendErrorAnalyticsEvent( 'UpdateTicketRequest', 'NotFound', this.location, responseData ); this.getListOfActiveTickets(); send({ summary: i18n.t('show-active-tickets.delete-ticket-notfound-title'), body: i18n.t('show-active-tickets.delete-ticket-notfound-body', { place: this.locationName, }), type: 'warning', timeout: 5, }); this.showReloadButton = false; this.setTimeoutIsSet = false; ret = false; break; default: this.sendErrorAnalyticsEvent( 'UpdateTicketRequest', 'UnknownError', this.location, responseData ); this.getListOfActiveTickets(); console.log('Update ticket failed'); this.setTimeoutIsSet = false; this.showReloadButton = true; ret = false; break; } return ret; } /** * Check if a local Proof exists wrapper * */ async checkForValidProofLocalWrapper() { this.loading = true; this.preCheck = true; await this.checkForValidProofLocal(); if (!this.greenPassHash || this.greenPassHash === -1) { this.hasValidProof = false; this.isSelfTest = false; this.isFullProof = false; } this.loading = false; } /** * Generate a QR Code at #qr-code-hash * if a valid local stored evidence is found * */ async generateQrCode() { await this.checkForValidProofLocal(); if (this.greenPassHash && this.greenPassHash !== -1 && this.hasValidProof) { let typeNumber = 0; let errorCorrectionLevel = 'Q'; let qr = qrcode(typeNumber, errorCorrectionLevel); qr.addData(this.greenPassHash, 'Alphanumeric'); qr.make(); let opts = {}; opts.cellSize = 2; opts.scalable = true; if (this._('#qr-code-hash')) this._('#qr-code-hash').innerHTML = qr.createSvgTag(opts); } else { this.hasValidProof = false; this.isSelfTest = false; this.isFullProof = false; } } /** * Generate a QR Code if a hash is avaible and valid, * updates the ticket and shows it in modal view * * @param {object} ticket */ async showTicket(ticket) { this.ticketLoading = true; await this.openTicket('ShowTicket'); await this.generateQrCode(); this.currentTicket = ticket; let success = await this.updateTicket(); if (!success) { this.currentTicket = {}; } this.ticketLoading = false; } /** * Sends a delete Ticket Request for the specific entry, * Checks the response and update the listview * * @param {object} ticket */ async deleteTicket(ticket) { const i18n = this._i18n; if (!confirm(i18n.t('show-active-tickets.delete-dialog-text'))) { return; } let response = await this.sendDeleteTicketRequest(ticket.identifier); let responseBody = await response.clone(); await this.checkDeleteTicketResponse(responseBody); await this.getListOfActiveTickets(); } /** * Checks the response from DeleteTicketRequest * and notify the user * * @param {object} response */ async checkDeleteTicketResponse(response) { const i18n = this._i18n; switch (response.status) { case 204: this.sendSuccessAnalyticsEvent('DeleteTicketRequest', 'Success', ''); send({ summary: i18n.t('show-active-tickets.delete-ticket-success-title'), body: i18n.t('show-active-tickets.delete-ticket-success-body', { place: this.locationName, }), type: 'success', timeout: 5, }); break; case 401: this.sendErrorAnalyticsEvent( 'DeleteTicketRequest', 'Loggedout', this.location, response ); send({ summary: i18n.t('show-active-tickets.logged-out-title'), body: i18n.t('show-active-tickets.logged-out-body'), type: 'warning', timeout: 5, }); break; case 404: this.sendErrorAnalyticsEvent( 'DeleteTicketRequest', 'NotFound', this.location, response ); send({ summary: i18n.t('show-active-tickets.delete-ticket-notfound-title'), body: i18n.t('show-active-tickets.delete-ticket-notfound-body', { place: this.locationName, }), type: 'warning', timeout: 5, }); break; default: this.sendErrorAnalyticsEvent( 'DeleteTicketRequest', 'UnknownError', this.location, response ); send({ summary: i18n.t('show-active-tickets.other-error-title'), body: i18n.t('show-active-tickets.other-error-body'), type: 'danger', timeout: 5, }); break; } this.locationName = ''; } /** * Get a list of active tickets and checks the response of the request * */ async getListOfActiveTickets() { let response = await this.getActiveTicketsRequest(); await this.checkActiveTicketsRequest(response); } /** * Checks the response from getActiveTicketsRequest * updates the ticket list * and notify the user if something went wrong * * @param {object} response */ async checkActiveTicketsRequest(response) { let responseBody = await response.clone().json(); if (responseBody !== undefined && response.status === 200) { this.activeTickets = this.parseActiveTickets(responseBody); } else { // else it failed, but we want to fail soft console.log('Update tickets failed'); } this.loadingTickets = false; } static get styles() { // language=css return css` ${commonStyles.getThemeCSS()} ${commonStyles.getGeneralCSS(false)} ${commonStyles.getActivityCSS()} ${commonStyles.getNotificationCSS()} ${CheckinStyles.getCheckinCss()} ${commonStyles.getButtonCSS()} ${commonStyles.getModalDialogCSS()} ${getTicketCss()} .gray { color: var(--dbp-muted); } .valid-for { display: flex; gap: 8px; padding-top: 2px; } .red { color: var(--dbp-danger); } .green { color: var(--dbp-success); } .warning { color: var(--dbp-warning); } .qr-code-wrapper { width: 100%; } #qr-code-hash svg { display: block; width: 80%; margin: auto; } .green-pass-evidence { line-height: 30px; } .qr-code-wrapper.self-test-qr, .self-test-qr { margin: 20px auto; width: 60%; } @media only screen and (orientation: landscape) and (max-width: 768px) { #qr-code-hash svg { height: 100%; } #qr-code-wrapper { width: 80%; } } @media only screen and (orientation: portrait) and (max-width: 768px) { #qr-code-hash svg { width: 100%; } .valid-for { padding-bottom: 8px; flex-direction: column; gap: 0; padding-top: 0; } .header { grid-row-gap: 8px; margin-bottom: 0; } .new-ticket-button { min-height: 24px; } } `; } render() { const i18n = this._i18n; const validTill = i18n.t('valid-till') + i18n.t('date-time', { clock: this.person.validUntil ? this.formatValidUntilTime(this.person.validUntil) : '', date: this.person.validUntil ? this.formatValidUntilDate(this.person.validUntil) : '', }) + '. ' + i18n.t('validity-tooltip', {place: this.locationName}) + ' ' + i18n.t('validity-tooltip-ticket-text'); const additionalInformation = html` <div class="information-container ${classMap({ hidden: this.hasValidProof || this.ticketLoading, })}"> <div class="${classMap({hidden: this.hasValidProof})}"> <span> <h4>${i18n.t('show-active-tickets.no-3g-evidence')}</h4> </span> <slot name="greenlight-reference"> <p>${i18n.t('show-active-tickets.no-evidence')}</p> </slot> </div> </div> <div class="proof-container ${classMap({ hidden: !this.hasValidProof || this.ticketLoading, })}"> <div class="green-pass-evidence ${classMap({ hidden: this.isSelfTest || !this.hasValidProof, })}"> <span> <h4>${i18n.t('show-active-tickets.3-g-evidence-greenpass')}</h4> </span> </div> <div class="${classMap({hidden: !this.isSelfTest || !this.hasValidProof})}"> <span> <h4>${i18n.t('show-active-tickets.self-test-found')}</h4> ${i18n.t('show-active-tickets.self-test-information')} <a class="int-link-external" title="${i18n.t('show-active-tickets.self-test')}" target="_blank" rel="noopener" href="${this.greenPassHash}"> ${i18n.t('show-active-tickets.self-test-link')} </a> </span> </div> <div class="qr-code-wrapper ${classMap({'self-test-qr': this.isSelfTest})}"> <div id="qr-code-hash"></div> </div> <div class="${classMap({hidden: !this.isSelfTest || !this.hasValidProof})}"> <slot name="greenlight-reference-invalid"> ${i18n.t('show-active-tickets.invalid-evidence')} </slot> </div> </div> `; const ticketList = html` ${this.activeTickets.map( (ticket) => html` <div class="ticket"> <span class="header"> <h3>${i18n.t('entry-ticket')}: ${this.locationName}</h3> <span class="header ${classMap({hidden: !this.hasValidProof})}"> <span> <b> ${i18n.t('show-active-tickets.status')} <span class="green"> ${i18n.t('show-active-tickets.status-active')} </span> </b> </span> <span class="${classMap({hidden: this.isSelfTest})}"> <b> ${i18n.t('show-active-tickets.3-g-evidence')}: <span class="green"> ${i18n.t( 'show-active-tickets.3-g-evidence-green-pass-valid' )} </span> </b> <dbp-info-tooltip class="tooltip" text-content="${validTill}" interactive></dbp-info-tooltip> </span> <span class="${classMap({hidden: !this.isSelfTest})}"> <b> ${i18n.t('show-active-tickets.3-g-evidence')}: <span class="warning"> ${i18n.t('show-active-tickets.3-g-evidence-selftest')} </span> </b> </span> </span> <span class="header ${classMap({ hidden: this.hasValidProof || this.validationFailed, })}"> <b> ${i18n.t('show-active-tickets.status')} <span class="red"> ${i18n.t('show-active-tickets.status-inactive')} </span> </b> <b> ${i18n.t('show-active-tickets.3-g-evidence')}: <span class="red"> ${i18n.t('show-active-tickets.3-g-evidence-invalid')} </span> </b> <span> <slot name="3-g-evidence-invalid-text"> <!-- TODO Use this slot and add a link to faq--> ${i18n.t('show-active-tickets.3-g-evidence-invalid-text')} ${i18n.t( 'show-active-tickets.3-g-evidence-maximize-saving' )} </slot> </span> </span> <span class="header ${classMap({hidden: !this.validationFailed})}"> <b> ${i18n.t('show-active-tickets.status')} <span class="red"> ${i18n.t('show-active-tickets.validation-failed')} </span> </b> <span>${i18n.t('show-active-tickets.validation-failed-text')}</span> </span> <span class="${classMap({ hidden: !this._hasMultipleTicketTypes() || this.validationFailed || !this.hasValidProof, })}"> <div class="valid-for"> <b>${i18n.t('acquire-3g-ticket.3g-proof-valid-for')}:</b> <div class="validity-check"> <slot name="partial-validity"> ${i18n.t('partial-validity-default')} </slot> <div class="full-validity"> ${this.isFullProof ? html` <slot name="full-validity"> ${i18n.t('full-validity-default')} </slot> ` : html` <slot name="no-full-validity" class="full-validity invalid gray"> ${i18n.t('no-full-validity-default')} </slot> `} </div> </div> </div> </span> </span> <div class="btn"> <dbp-loading-button class="${classMap({hidden: !this.hasValidProof})}" type="is-primary" @click="${() => { this.showTicket(ticket); }}" title="${i18n.t('show-active-tickets.show-btn-text')}"> ${i18n.t('show-active-tickets.show-btn-text')} </dbp-loading-button> <a class="button new-ticket-button ${classMap({ hidden: this.hasValidProof || this.validationFailed, })}" href="#" @click="${(e) => { this.dispatchEvent( new CustomEvent('dbp-show-activity', { detail: {name: 'acquire-3g-ticket'}, }) ); e.preventDefault(); }}" title="${i18n.t('show-active-tickets.new-ticket')}"> ${i18n.t('show-active-tickets.new-ticket')} </a> <dbp-loading-button id="delete-btn" class="${classMap({hidden: this.validationFailed})}" @click="${() => { this.deleteTicket(ticket); }}" title="${i18n.t('delete-btn-text')}"> ${i18n.t('delete-btn-text')} </dbp-loading-button> </div> </div> ` )} `; const permissions = !this.isLoggedIn() || this.isLoading() || !this.hasPermissions(); return html` <div class="notification is-warning ${classMap({ hidden: this.isLoggedIn() || this.isLoading(), })}"> ${i18n.t('error-login-message')} </div> <div class="control ${classMap({hidden: this.isLoggedIn() || !this.isLoading()})}"> <span class="loading"> <dbp-mini-spinner text=${i18n.t('loading-message')}></dbp-mini-spinner> </span> </div> <div class="notification is-danger ${classMap({ hidden: this.hasPermissions() || !this.isLoggedIn() || this.isLoading(), })}"> ${i18n.t('error-permission-message')} </div> ${this.getTicketUI(permissions, ticketList, additionalInformation)} `; } } commonUtils.defineCustomElement('dbp-show-active-tickets', ShowActiveTickets);