UNPKG

@digital-blueprint/greenlight-app

Version:

[GitHub Repository](https://github.com/digital-blueprint/greenlight-app) | [npmjs package](https://www.npmjs.com/package/@digital-blueprint/greenlight-app) | [Unpkg CDN](https://unpkg.com/browse/@digital-blueprint/greenlight-app/) | [Greenlight Bundle](ht

1,296 lines (1,145 loc) 61.3 kB
import {createInstance} from './i18n.js'; import {css, html} from 'lit'; import DBPGreenlightLitElement from './dbp-greenlight-lit-element'; import {ScopedElementsMixin} from '@open-wc/scoped-elements'; import * as commonUtils from '@dbp-toolkit/common/utils'; import { LoadingButton, Icon, MiniSpinner, 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 {FileSource} from '@dbp-toolkit/file-handling'; import {TextSwitch} from './textswitch.js'; import {QrCodeScanner} from '@dbp-toolkit/qr-code-scanner'; import {escapeRegExp, i18nKey, i18nForKey} from './utils.js'; import {Activity} from './activity.js'; import metadata from './dbp-acquire-3g-ticket.metadata.json'; import {getQRCodeFromFile} from './qrfilescanner.js'; import {InfoTooltip} from '@dbp-toolkit/tooltip'; class Acquire3GTicket extends ScopedElementsMixin(DBPGreenlightLitElement) { constructor() { super(); this._i18n = createInstance(); this.lang = this._i18n.language; this.entryPointUrl = ''; this.activity = new Activity(metadata); this.loading = false; this.processStarted = false; this.preselectionLoading = true; this._activationInProgress = false; this.preCheck = true; this.preselectedOption = ''; this.preselectionCheck = true; this.location = ''; this.serviceName = ''; this.hasValidProof = false; this.hasTicket = false; this.trustButtonChecked = false; this.detailedError = ''; this.showQrContainer = false; this.qrParsingLoading = false; this.QRCodeFile = null; this.wrongHash = []; this.wrongQR = []; this.resetWrongQr = false; this.resetWrongHash = false; this.greenPassHash = ''; this.isSelfTest = false; this.isConfirmChecked = false; this.person = {}; this.message = ''; this.proofUploadFailed = false; this.showCreateTicket = false; this.fileHandlingEnabledTargets = 'local'; this.nextcloudWebAppPasswordURL = ''; this.nextcloudWebDavURL = ''; this.nextcloudName = ''; this.nextcloudFileURL = ''; this.nextcloudAuthInfo = ''; this.isFullProof = false; } static get scopedElements() { return { 'dbp-icon': Icon, 'dbp-mini-spinner': MiniSpinner, 'dbp-loading-button': LoadingButton, 'dbp-inline-notification': InlineNotification, 'dbp-textswitch': TextSwitch, 'dbp-qr-code-scanner': QrCodeScanner, 'dbp-file-source': FileSource, 'dbp-info-tooltip': InfoTooltip, }; } static get properties() { return { ...super.properties, lang: {type: String}, loading: {type: Boolean, attribute: false}, processStarted: {type: Boolean, attribute: false}, entryPointUrl: {type: String, attribute: 'entry-point-url'}, preselectedOption: {type: String, attribute: 'preselected-option'}, hasValidProof: {type: Boolean, attribute: false}, hasTicket: {type: Boolean, attribute: false}, location: {type: String, attribute: false}, serviceName: {type: String, attribute: 'service-name'}, showQrContainer: {type: Boolean, attribute: false}, qrParsingLoading: {type: Boolean, attribute: false}, status: {type: Object, attribute: false}, wrongQR: {type: Array, attribute: false}, wrongHash: {type: Array, attribute: false}, QRCodeFile: {type: Object, attribute: false}, trustButtonChecked: {type: Boolean, attribute: false}, isConfirmChecked: {type: Boolean, attribute: false}, person: {type: Object, attribute: false}, isSelfTest: {type: Boolean, attribute: false}, preselectionLoading: {type: Boolean, attribute: false}, isFullProof: {type: Boolean, attribute: false}, proofUploadFailed: {type: Boolean, attribute: false}, showCreateTicket: {type: Boolean, attribute: false}, detailedError: {type: String, attribute: false}, message: {type: String, attribute: false}, fileHandlingEnabledTargets: {type: String, attribute: 'file-handling-enabled-targets'}, nextcloudWebAppPasswordURL: {type: String, attribute: 'nextcloud-web-app-password-url'}, nextcloudWebDavURL: {type: String, attribute: 'nextcloud-webdav-url'}, nextcloudName: {type: String, attribute: 'nextcloud-name'}, nextcloudFileURL: {type: String, attribute: 'nextcloud-file-url'}, nextcloudAuthInfo: {type: String, attribute: 'nextcloud-auth-info'}, }; } connectedCallback() { super.connectedCallback(); } update(changedProperties) { let that = this; changedProperties.forEach((oldValue, propName) => { switch (propName) { case 'lang': this._i18n.changeLanguage(this.lang); break; case 'status': if (oldValue !== undefined) { setTimeout(function () { that._('#notification-wrapper').scrollIntoView({ behavior: 'smooth', block: 'end', }); }, 10); } break; } }); super.update(changedProperties); } /** * Init a 3g activation from a QR code scan event * * @param event */ async doActivationWithQR(event) { let data = event.detail['code']; event.stopPropagation(); if (this._activationInProgress) return; this._activationInProgress = true; this.detailedError = ''; try { await this.checkQRCode(data); } finally { this._activationInProgress = false; this.loading = false; this.scrollToConfirmButton(); } } /** * Check uploaded file and search for QR code * If a QR Code is found, validate it and send an Activation Request * */ async doActivationManually() { const i18n = this._i18n; if (this._activationInProgress) return; this._activationInProgress = true; this.loading = true; this.detailedError = ''; let data = await this.searchQRInFile(); if (data === null) { send({ summary: i18n.t('acquire-3g-ticket.invalid-title'), body: i18n.t('acquire-3g-ticket.invalid-body'), type: 'danger', timeout: 5, }); this.message = i18nKey('acquire-3g-ticket.no-qr-code'); this.proofUploadFailed = true; this._activationInProgress = false; this.loading = false; return; } try { await this.checkQRCode(data.data); } finally { this._activationInProgress = false; this.loading = false; this.scrollToConfirmButton(); } } /** * Searches in the uploaded file for an QR Code * If the file is a pdf the search in all pages * The payload is null if no QR Code is found * * @returns {object} payload */ async searchQRInFile() { return getQRCodeFromFile(this.QRCodeFile); } /** * Stop QR code reader and hide container * */ stopQRReader() { if (this._('#qr-scanner')) { this._('#qr-scanner').stopScan = true; this.showQrContainer = false; } else { console.log('error: qr scanner is not available. Is it already stopped?'); } } /** * Start QR code reader and show container * */ showQrReader() { this.showQrContainer = true; if (this._('#qr-scanner')) { this._('#qr-scanner').stopScan = false; } //this._("#manualPassUploadWrapper").classList.add('hidden'); } /** * Show manual pass upload container * and stop QR code scanner * */ showManualUpload() { this._('#qr-scanner').stopScan = true; this.showQrContainer = false; this._('.proof-upload-container').scrollIntoView({behavior: 'smooth', block: 'start'}); this.openFileSource(); } /** * Uses textswitch, switches container (manually room select or QR room select * * @param name */ uploadSwitch(name) { this.proofUploadFailed = false; if (name === 'manual') { this.showManualUpload(); } else { this.showQrReader(); } } skipUpload() { this.proofUploadFailed = false; this.isConfirmChecked = false; if (this._('#digital-proof-mode')) this._('#digital-proof-mode').checked = false; if (this._('#text-switch')) this._('#text-switch')._active = ''; } /* * Open the file source * */ openFileSource() { const fileSource = this._('#file-source'); if (fileSource) { this._('#file-source').openDialog(); } } async getFilesToActivate(event) { this.QRCodeFile = event.detail.file; this.qrParsingLoading = true; this.hasValidProof = false; this.showCreateTicket = false; await this.doActivationManually(); this.qrParsingLoading = false; } async sendGetTicketsRequest() { const options = { method: 'GET', headers: { 'Content-Type': 'application/ld+json', Authorization: 'Bearer ' + this.auth.token, }, }; return await this.httpGetAsync( combineURLs(this.entryPointUrl, '/greenlight/permits'), options ); } async checkForValidTickets() { let responseData = await this.sendGetTicketsRequest(); let responseBody = await responseData.clone().json(); let status = responseData.status; let numTypes = parseInt(responseBody['hydra:totalItems']); if (isNaN(numTypes)) { numTypes = 0; } switch (status) { case 200: for (let i = 0; i < numTypes; i++) { //For the case we have more then one possible ticket (places), we need to check here if it is the same ticket as the selected this.hasTicket = true; console.log('Found a valid ticket for this room.'); } break; default: console.log('Get valid ticket request failed'); break; } } async checkCreateTicketResponse(response) { const i18n = this._i18n; let responseBody = await response.clone().json(); switch (response.status) { case 201: send({ summary: i18n.t('acquire-3g-ticket.create-ticket-success-title'), body: i18n.t('acquire-3g-ticket.create-ticket-success-body', { place: this.location, }), type: 'success', timeout: 5, }); this.location = this.preselectedOption; this.hasTicket = true; this.isConfirmChecked = false; if (this._('#digital-proof-mode')) { this._('#digital-proof-mode').checked = false; } this.processStarted = false; this.showCreateTicket = false; if (this.hasValidProof) { this.hasValidProof = false; //Could be expired until now this.isFullProof = false; this.preCheck = true; this.checkForValidProofLocal(); } this.preCheck = true; //initiates a new check and sets validProof to true await this.sendSuccessAnalyticsEvent('CreateTicketRequest', 'Success', ''); break; case 400: // Invalid input switch (responseBody['relay:errorId']) { case 'greenlight:consent-assurance-not-true': // The content of the consentAssurance attribute was not true. this.sendErrorAnalyticsEvent( 'CreateTicketRequest', 'InvalidInput: consent-assurance-not-true', this.location, response ); send({ summary: i18n.t('acquire-3g-ticket.confirm-not-checked-title'), body: i18n.t('acquire-3g-ticket.confirm-not-checked-body'), type: 'danger', timeout: 5, }); break; case 'greenlight:additional-information-not-valid': // The content of the additionalInformation attribute was not valid. this.sendErrorAnalyticsEvent( 'CreateTicketRequest', 'InvalidInput: additional-information-not-valid', this.location, response ); send({ summary: i18n.t('acquire-3g-ticket.other-error-title'), body: i18n.t('acquire-3g-ticket.other-error-body'), type: 'danger', timeout: 5, }); break; default: this.sendErrorAnalyticsEvent( 'CreateTicketRequest', 'InvalidInput: default', this.location, response ); send({ summary: i18n.t('acquire-3g-ticket.other-error-title'), body: i18n.t('acquire-3g-ticket.other-error-body'), type: 'danger', timeout: 5, }); break; } break; case 403: // Forbidden - Access Denied switch (responseBody['relay:errorId']) { case 'greenlight:current-person-not-found': // Current person wasn't found. this.sendErrorAnalyticsEvent( 'CreateTicketRequest', 'Access Denied: current-person-not-found', this.location, response ); send({ summary: i18n.t('acquire-3g-ticket.photo-not-available-title'), body: i18n.t('acquire-3g-ticket.photo-not-available-body', { serviceName: this.serviceName, }), type: 'danger', timeout: 5, }); break; case 'greenlight:additional-information-not-decoded': // The content of the additionalInformation attribute could not be decoded. default: this.sendErrorAnalyticsEvent( 'CreateTicketRequest', 'Access Denied: additional-information-not-decoded', this.location, response ); send({ summary: i18n.t('acquire-3g-ticket.other-error-title'), body: i18n.t('acquire-3g-ticket.other-error-body'), type: 'danger', timeout: 5, }); break; } break; case 422: // Unprocessable entity this.sendErrorAnalyticsEvent( 'CreateTicketRequest', 'UnprocessableEntity', this.location, response ); send({ summary: i18n.t('acquire-3g-ticket.other-error-title'), body: i18n.t('acquire-3g-ticket.other-error-body'), type: 'danger', timeout: 5, }); break; case 500: // Can't process Data switch (responseBody['relay:errorId']) { case 'greenlight:permit-not-created': // The permit could not be created. this.sendErrorAnalyticsEvent( 'CreateTicketRequest', 'Service unavailable: permit-not-created', this.location, response ); send({ summary: i18n.t('acquire-3g-ticket.photo-not-available-title'), body: i18n.t('acquire-3g-ticket.photo-not-available-body', { serviceName: this.serviceName, }), type: 'danger', timeout: 5, }); break; case 'greenlight:photo-service-error': // The photo service had an error! this.sendErrorAnalyticsEvent( 'CreateTicketRequest', 'Service unavailable: photo-service-error', this.location, response ); send({ summary: i18n.t('acquire-3g-ticket.photo-not-available-title'), body: i18n.t('acquire-3g-ticket.photo-not-available-body', { serviceName: this.serviceName, }), type: 'danger', timeout: 5, }); break; default: this.sendErrorAnalyticsEvent( 'CreateTicketRequest', 'ErrorInData', this.location, response ); send({ summary: i18n.t('acquire-3g-ticket.other-error-title'), body: i18n.t('acquire-3g-ticket.other-error-body'), type: 'danger', timeout: 5, }); break; } break; case 503: // Service unavailable switch (responseBody['relay:errorId']) { case 'greenlight:current-person-no-photo': // Photo for current person could not be loaded! this.sendErrorAnalyticsEvent( 'CreateTicketRequest', 'Service unavailable: current-person-no-photo', this.location, response ); send({ summary: i18n.t('acquire-3g-ticket.photo-not-available-title'), body: i18n.t('acquire-3g-ticket.no-photo-body', { serviceName: this.serviceName, }), type: 'danger', timeout: 5, }); break; default: this.sendErrorAnalyticsEvent( 'CreateTicketRequest', 'Service unavailable: default', this.location, response ); send({ summary: i18n.t('acquire-3g-ticket.other-error-title'), body: i18n.t('acquire-3g-ticket.other-error-body'), type: 'danger', timeout: 5, }); break; } break; // Error: something else doesn't work default: this.sendErrorAnalyticsEvent( 'CreateTicketRequest', 'UnknownError', this.location, response ); send({ summary: i18n.t('acquire-3g-ticket.other-error-title'), body: i18n.t('acquire-3g-ticket.other-error-body'), type: 'danger', timeout: 5, }); break; } } async createTicket(event) { const i18n = this._i18n; let button = event.target; let response; if (!this.isConfirmChecked) { send({ summary: i18n.t('acquire-3g-ticket.confirm-not-checked-title'), body: i18n.t('acquire-3g-ticket.confirm-not-checked-body'), type: 'danger', timeout: 8, }); return; } button.start(); try { response = await this.sendCreateTicketRequest(); await this.checkCreateTicketResponse(response); } finally { button.stop(); } } checkConfirmCheckmark() { this.isConfirmChecked = this._('#digital-proof-mode') && this._('#digital-proof-mode').checked; } checkTrustButtonCheckmark() { this.trustButtonChecked = this._('#trust-button') && this._('#trust-button').checked; } async removeProof() { await this.clearLocalStorage(); if (this._('#trust-button')) { this._('#trust-button').checked = true; this.trustButtonChecked = this._('#trust-button') && this._('#trust-button').checked; } this.hasValidProof = false; this.isFullProof = false; this.showCreateTicket = false; this.greenPassHash = ''; } async scrollToConfirmButton() { (await this.showCreateTicket) === true; if (this.showCreateTicket && this._('#scrollToConfirmBtn')) { this._('#scrollToConfirmBtn').scrollIntoView({behavior: 'smooth', block: 'end'}); } } static get styles() { // language=css return css` ${commonStyles.getThemeCSS()} ${commonStyles.getGeneralCSS(false)} ${commonStyles.getNotificationCSS()} ${CheckinStyles.getCheckinCss()} ${commonStyles.getButtonCSS()} ${commonStyles.getRadioAndCheckboxCss()} ${commonStyles.getActivityCSS()} .gray { color: #595959; } .valid-for { padding-bottom: 4px; display: flex; gap: 8px; } h2 { margin-top: 0; } h3 { margin-top: 2rem; } #last-checkbox { margin-top: 1rem; margin-bottom: 1.5rem; } .checkmark { height: 18px; width: 18px; top: 5px; } .button-container input[type='checkbox']:checked ~ .checkmark::after { top: 1px; left: 6px; } .info-icon { padding-left: 5px; } .tickets-notifications { margin-top: 3rem; } .store-cert-checkmark-wrapper { margin-top: 1rem; } .notification-wrapper { margin-top: 1.5rem; } .proof-upload-container { margin-top: 1.5rem; } .cert-found-checkbox-wrapper { margin-top: 1rem; display: flex; } .control { align-self: center; } .qr-loading { padding: 0 0 0 1em; } .btn-container { display: flex; justify-content: space-between; } .btn { display: contents; } .grid-container { margin-top: 2rem; padding-top: 2rem; flex-flow: column; } #text-switch { width: 50%; } /* Chrome, Safari, Edge, Opera */ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } /* Firefox */ input[type='number'] { -moz-appearance: textfield; } @keyframes linkIconOut { 0% { filter: invert(100%); -webkit-filter: invert(100%); } 100% { filter: invert(0%); -webkit-filter: invert(0%); } } .activations-btn { display: grid; grid-template-columns: repeat(2, max-content); column-gap: 10px; row-gap: 10px; } .activations { display: flex; justify-content: space-between; } .header { display: grid; align-items: center; } .show-file { margin-right: 15px; } .file-block { margin-top: 2rem; display: inline-block; width: 100%; } label.button { display: inline-block; } select:not(.select) { /*background-size: 13px;*/ /*background-position-x: calc(100% - 0.4rem);*/ /*padding-right: 1.3rem;*/ background-image: none; width: 100%; height: 29px; font-weight: 300; border: var(--dbp-border); border-color: var(--dbp-muted); } .loading-proof { padding: 0; } .tickets-wrapper { margin-top: 1.5rem; margin-bottom: 1.5rem; } .close-icon { color: var(--dbp-accent); display: inline; } #no-proof-continue-btn { margin-top: 1.5rem; } #manual-proof-checkmark { margin-top: 9px; } .border { margin-top: 2rem; margin-bottom: 2rem; border-top: var(--dbp-border); } .field { margin-top: 1rem; } .int-link-internal { transition: background-color 0.15s, color 0.15s; border-bottom: var(--dbp-border); } .inline-notification .int-link-internal { transition: background-color 0.15s, color 0.15s; border-bottom: var(--dbp-border); border-color: var(--dbp-on-content-surface); color: var(--dbp-on-content-surface); } .inline-notification .int-link-internal:hover { color: inherit; background-color: inherit; } .inline-notification .int-link-internal { border-bottom-color: var(--dbp-on-content-surface); } .check-icon { font-size: 18px; opacity: 0.7; padding: 0px 4px; } .g-proof-information { margin: 1.5em 0; border: var(--dbp-border); padding: 1.25rem 1.5rem 1.25rem 1.5rem; border-radius: var(--dbp-border-radius); display: flex; justify-content: space-between; } .g-proof-information h4 { margin-top: 0px; margin-bottom: 0.5em; } .wrapper { margin: 1.5em 0; } .confirm-btn label { margin-top: 1em; } .element { margin-top: 1.5rem; } @media only screen and (orientation: portrait) and (max-width: 768px) { .confirm-btn { display: flex; flex-direction: column; row-gap: 10px; } .confirm-btn.hidden { display: none; } #no-proof-continue-btn { display: block; } .btn-container { flex-direction: column; row-gap: 1.5em; } .qr-loading { padding: 1em; } .header { margin-bottom: 0.5rem; white-space: initial; } .btn { display: flex; flex-direction: column; text-align: center; } .logout { width: 100%; box-sizing: border-box; } #text-switch { display: block; width: 100%; } #refresh-btn { margin-top: 0.5rem; } .loading { justify-content: center; } .activations { display: block; } .activations-btn { display: flex; flex-direction: column; } #confirm-ticket-btn, #start-ticket-btn { width: 100%; margin-bottom: 0.5em; } .g-proof-information { flex-direction: column; } .checkmark { top: 10%; } .valid-for { padding-bottom: 8px; flex-direction: column; gap: 0; } } `; } _onScanStarted(e) { // We want to scroll after the next re-layout requestAnimationFrame(() => { setTimeout(() => { this._('#qr-scanner').scrollIntoView({behavior: 'smooth', block: 'start'}); }, 0); }); } render() { const i18n = this._i18n; const matchRegexString = '.*' + escapeRegExp(this.searchHashString) + '.*'; if ( this.isLoggedIn() && this.hasPermissions() && !this.isLoading() && this.preCheck && !this.loading ) { this.loading = true; this.checkForValidProofLocal().then(() => { this.loading = false; }); } if ( this.isLoggedIn() && this.hasPermissions() && !this.isLoading() && this.preselectedOption && this.preselectedOption !== '' && this.preselectionCheck ) { this.location = this.preselectedOption; this.checkForValidTickets().then(() => { this.preselectionLoading = false; }); this.preselectionCheck = false; } return html` <div class="notification is-warning ${classMap({ hidden: this.isLoggedIn() || this.isLoading(), })}"> ${i18n.t('error-login-message')} </div> <div class="notification is-danger ${classMap({ hidden: this.hasPermissions() || !this.isLoggedIn() || this.isLoading(), })}"> ${i18n.t('error-permission-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="${classMap({ hidden: !this.isLoggedIn() || this.isLoading() || !this.hasPermissions(), })}"> <h2>${this.activity.getName(this.lang)}</h2> <p class="subheadline"> <slot name="description">${this.activity.getDescription(this.lang)}</slot> </p> <div> <slot name="additional-information"> <p>${i18n.t('acquire-3g-ticket.additional-information')}</p> </slot> </div> <div class="control ${classMap({ hidden: !this.preCheck && !this.preselectionCheck && !this.preselectionLoading, })}"> <span class="loading"> <dbp-mini-spinner text=${i18n.t('loading-message')}></dbp-mini-spinner> </span> </div> ${this.hasPermissions() ? html` <!-- Create ticket start --> <div class="container ${classMap({ hidden: this.processStarted || this.preCheck || this.preselectionCheck || this.preselectionLoading, })}"> <div class="tickets-wrapper ${classMap({ hidden: !this.hasTicket || !this.hasValidProof, })}"> <dbp-inline-notification class="inline-notification"> <div slot="body"> ${i18n.t('acquire-3g-ticket.manage-tickets-text')} <a href="#" @click="${(e) => { this.dispatchEvent( new CustomEvent('dbp-show-activity', { detail: {name: 'show-tickets'}, }) ); e.preventDefault(); }}" title="${i18n.t( 'acquire-3g-ticket.manage-tickets-link' )}" class="int-link-internal"> <span> ${i18n.t( 'acquire-3g-ticket.manage-tickets-link' )}. </span> </a> </div> </dbp-inline-notification> </div> ${!this.preselectionLoading ? html` <dbp-loading-button type="${!this.hasTicket || !this.hasValidProof ? 'is-primary' : ''}" id="start-ticket-btn" value="${!this.hasTicket ? i18n.t( 'acquire-3g-ticket.request-ticket-button-text' ) : i18n.t('acquire-3g-ticket.create-new-ticket')}" @click="${() => { this.processStarted = true; this.scrollToConfirmButton(); }}" title="${i18n.t( 'acquire-3g-ticket.request-ticket-button-text' )}"></dbp-loading-button> ` : ``} </div> <!-- Create ticket start end --> <div class="border ${classMap({hidden: !this.processStarted})}"></div> <div class="container ${classMap({hidden: !this.processStarted})}"> <!-- 3G Proof Upload --> <div class="proof-upload-container ${classMap({ hidden: this.location === '' || this.showCreateTicket, })}"> <h3>${i18n.t('acquire-3g-ticket.3g-proof-label-text')}</h3> <label id="last-checkbox" class="button-container"> ${i18n.t('acquire-3g-ticket.trust-and-save-1')} <input type="checkbox" id="trust-button" name="trust-button" value="trust-button" @click="${this.checkTrustButtonCheckmark}" /> <span class="checkmark" id="trust-button-checkmark"></span> <dbp-info-tooltip class="info-tooltip" text-content="${i18n.t( 'acquire-3g-ticket.trust-and-save-2' )}"></dbp-info-tooltip> </label> <div id="btn-container" class="btn-container wrapper"> <dbp-textswitch id="text-switch" name1="qr-reader" ?disabled="${!this.trustButtonChecked}" name2="manual" name="${i18n.t( 'acquire-3g-ticket.qr-button-text' )} || ${i18n.t('acquire-3g-ticket.manually-button-text')}" class="switch" value1="${i18n.t('acquire-3g-ticket.qr-button-text')}" value2="${i18n.t( 'acquire-3g-ticket.manually-button-text' )}" @change=${(e) => this.uploadSwitch(e.target.name)}></dbp-textswitch> </div> <dbp-file-source id="file-source" context="${i18n.t('acquire-3g-ticket.filepicker-context')}" allowed-mime-types="image/*,application/pdf,.pdf" nextcloud-auth-url="${this.nextcloudWebAppPasswordURL}" nextcloud-web-dav-url="${this.nextcloudWebDavURL}" nextcloud-name="${this.nextcloudName}" nextcloud-file-url="${this.nextcloudFileURL}" nexcloud-auth-info="${this.nextcloudAuthInfo}" enabled-targets="${this.fileHandlingEnabledTargets}" decompress-zip lang="${this.lang}" text="${i18n.t('acquire-3g-ticket.filepicker-context')}" button-label="${i18n.t( 'acquire-3g-ticket.filepicker-button-title' )}" number-of-files="1" @dbp-file-source-file-selected="${this .getFilesToActivate}"></dbp-file-source> <div class="border ${classMap({hidden: !this.showQrContainer})}"> <div class="element"> <dbp-qr-code-scanner id="qr-scanner" lang="${this.lang}" stop-scan match-regex="${matchRegexString}" @scan-started="${this._onScanStarted}" @code-detected="${(event) => { this.doActivationWithQR(event); }}"></dbp-qr-code-scanner> </div> <div class="control ${classMap({hidden: !this.loading})}"> <span class="loading"> <dbp-mini-spinner></dbp-mini-spinner> </span> </div> </div> <div class="control ${classMap({hidden: !this.qrParsingLoading})}"> <span class="qr-loading"> <dbp-mini-spinner text=${i18n.t( 'acquire-3g-ticket.manual-uploading-message' )}></dbp-mini-spinner> </span> </div> </div> <!-- End 3G Proof Upload--> <!-- Show Proof --> <h3 class="${classMap({hidden: !this.showCreateTicket})}"> ${i18n.t('acquire-3g-ticket.create-ticket')} </h3> <div class="${classMap({ hidden: this.location === '' || this.loading, })}"> <div class="${classMap({ hidden: !this.hasValidProof || this.loading, })}"> <dbp-icon name="checkmark-circle" class="check-icon"></dbp-icon> ${i18nForKey(i18n.t, this.message)} </div> <div class="no-proof-found ${classMap({ hidden: !this.proofUploadFailed || this.loading, })}"> <div class="close-icon"> ${i18nForKey(i18n.t, this.message, { place: this.location, })} </div> ${this.detailedError ? html` <dbp-info-tooltip class="info-tooltip" text-content="${i18n.t( 'acquire-3g-ticket.invalid-document-prefix' ) + this.detailedError.replace(/\n/g, '<br>')}" interactive></dbp-info-tooltip> ` : ``} </div> </div> <div class="notification-wrapper ${classMap({ hidden: this.location === '' || !this.showCreateTicket, })}"> <div class="g-proof-information"> <div class="${classMap({ hidden: this.isSelfTest || !this.hasValidProof, })}"> <span class="header"> <h4>${i18n.t('acquire-3g-ticket.3g-proof')}</h4> ${this._hasMultipleTicketTypes() ? html` <span class="valid-for"> ${i18n.t( 'acquire-3g-ticket.3g-proof-valid-for' )}: <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> `