From 092cba01c34222a1a68943134b0ea47497376de2 Mon Sep 17 00:00:00 2001
From: Patrizio Bekerle <patrizio@bekerle.com>
Date: Tue, 16 Jun 2020 13:58:19 +0200
Subject: [PATCH] Implement signature verification page

---
 .../vpu-signature-verification.metadata.json  |  17 +
 assets/vpu-signature.topic.metadata.json.ejs  |   4 +-
 rollup.config.js                              |   1 +
 src/i18n/de/translation.json                  |  30 +
 src/i18n/en/translation.json                  |  30 +
 src/vpu-signature-verification.js             | 836 ++++++++++++++++++
 6 files changed, 917 insertions(+), 1 deletion(-)
 create mode 100644 assets/vpu-signature-verification.metadata.json
 create mode 100644 src/vpu-signature-verification.js

diff --git a/assets/vpu-signature-verification.metadata.json b/assets/vpu-signature-verification.metadata.json
new file mode 100644
index 0000000..3c4ac9a
--- /dev/null
+++ b/assets/vpu-signature-verification.metadata.json
@@ -0,0 +1,17 @@
+{
+  "element": "vpu-signature-verification",
+  "module_src": "vpu-signature-verification.js",
+  "routing_name": "signature-verification",
+  "name": {
+    "de": "Signierte Dokumente verifizieren",
+    "en": "Verify signed documents"
+  },
+  "short_name": {
+    "de": "Signierte Dokumente verifizieren",
+    "en": "Verify signed documents"
+  },
+  "description": {
+    "de": "Erlaubt das Verifizieren von signierten PDF-Dokumenten",
+    "en": "Allows verification of signed PDF-documents"
+  }
+}
diff --git a/assets/vpu-signature.topic.metadata.json.ejs b/assets/vpu-signature.topic.metadata.json.ejs
index 7085bfb..36f71a9 100644
--- a/assets/vpu-signature.topic.metadata.json.ejs
+++ b/assets/vpu-signature.topic.metadata.json.ejs
@@ -15,7 +15,9 @@
   "activities": [
     {"path": "vpu-official-signature-pdf-upload.metadata.json",
      "visible": <%= environment == "local" || environment == "development" %>},
-    {"path": "vpu-qualified-signature-pdf-upload.metadata.json"}
+    {"path": "vpu-qualified-signature-pdf-upload.metadata.json"},
+    {"path": "vpu-signature-verification.metadata.json",
+     "visible": <%= environment == "local" || environment == "development" %>}
   ],
   "attributes": []
 }
\ No newline at end of file
diff --git a/rollup.config.js b/rollup.config.js
index 2befe42..379e185 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -137,6 +137,7 @@ export default {
       'src/' + pkg.name + '.js',
       'src/vpu-official-signature-pdf-upload.js',
       'src/vpu-qualified-signature-pdf-upload.js',
+      'src/vpu-signature-verification.js',
     ] : glob.sync('test/**/*.js'),
     output: {
       dir: 'dist',
diff --git a/src/i18n/de/translation.json b/src/i18n/de/translation.json
index 8b3cd22..cc46492 100644
--- a/src/i18n/de/translation.json
+++ b/src/i18n/de/translation.json
@@ -67,6 +67,36 @@
     "file-label": "Dokument",
     "confirm-page-leave": "Sind Sie sicher, dass Sie die Seite verlassen wollen? Es stehen signierte Dokumente zum Download bereit."
   },
+  "signature-verification": {
+    "upload-field-label": "PDF-Dokumente zum Überprüfen der Signaturen hochladen",
+    "upload-area-text": "Sie können in diesem Bereich PDF-Dokumente per Drag & Drop oder per Direktauswahl hochladen. Die PDF-Dokumente dürfen sich auch in ZIP-Dateien befinden.",
+    "current-verification-process-label": "Aktueller Verifikationsprozess",
+    "queued-files-label": "Dokumente in der Warteschlange",
+    "queued-files-empty1": "Kein Dokument in der Warteschlange",
+    "queued-files-empty2": "Sie können jetzt ein neues Dokument hochladen",
+    "remove-failed-file-button-title": "Fehlgeschlagenes Dokument entfernen",
+    "remove-queued-file-button-title": "Dokument aus der Warteschlange entfernen",
+    "clear-all": "Alle entfernen",
+    "start-verification-process-button": "Verifikations starten",
+    "stop-verification-process-button": "Verifikation unterbrechen",
+    "show-preview": "Dokument anzeigen",
+    "positioning-automatic": "Automatisch",
+    "positioning-manual": "Manuell",
+    "close-preview": "Dokumentansicht schließen",
+    "preview-label": "Dokumentenansicht",
+    "start-verification-process-button-running-title": "Verifikationsvorgang läuft gerade",
+    "remove-current-file-button-title": "Aktuellen Verifikationsprozess abbrechen",
+    "verified-files-label": "Verifizierte Dokumente",
+    "upload-button-label": "PDF-Dokumente auswählen",
+    "error-files-label": "Fehlgeschlagene Verifikationsvorgänge",
+    "re-upload-file-button-title": "Erneut hochladen",
+    "upload-status-file-text": "({{fileSize}}) wird hochgeladen und verifiziert...",
+    "re-upload-all-button": "Alle erneut hochladen",
+    "re-upload-all-button-title": "Alle fehlgeschlagen Uploads erneut hochladen",
+    "file-label": "Dokument",
+    "given-name": "Vorname",
+    "last-name": "Nachname"
+  },
   "pdf-preview": {
     "first": "Erste",
     "previous": "Vorherige",
diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json
index 8287f72..393bdca 100644
--- a/src/i18n/en/translation.json
+++ b/src/i18n/en/translation.json
@@ -67,6 +67,36 @@
     "file-label": "document",
     "confirm-page-leave": "Are you sure you want to leave this page? There are still signed documents ready to be downloaded."
   },
+  "signature-verification": {
+    "upload-field-label": "Upload PDF-documents to verify",
+    "upload-area-text": "In this area you can upload PDF-documents via Drag & Drop or by selecting them directly. The PDF-documents can also be located in a ZIP-file.",
+    "current-verification-process-label": "Current Verification process",
+    "queued-files-label": "Queued documents",
+    "queued-files-empty1": "No queued documents",
+    "queued-files-empty2": "You can now upload more documents",
+    "remove-failed-file-button-title": "Remove failed document",
+    "remove-queued-file-button-title": "Remove document from queue",
+    "clear-all": "Clear all",
+    "start-verification-process-button": "Start Verification",
+    "stop-verification-process-button": "Stop Verification",
+    "show-preview": "Show document",
+    "positioning-automatic": "Automatic",
+    "positioning-manual": "Manual",
+    "close-preview": "Close preview",
+    "preview-label": "Document view",
+    "start-verification-process-button-running-title": "Verification process running",
+    "remove-current-file-button-title": "Cancel current verification process",
+    "verified-files-label": "Verified documents",
+    "upload-button-label": "Select PDF-documents",
+    "error-files-label": "Failed verification processes",
+    "re-upload-file-button-title": "Upload again",
+    "upload-status-file-text": "({{fileSize}}) is currently uploading and being processed...",
+    "re-upload-all-button": "Upload all",
+    "re-upload-all-button-title": "Upload all failed uploads again",
+    "file-label": "document",
+    "given-name": "Given name",
+    "last-name": "Last name"
+  },
   "pdf-preview": {
     "first": "First",
     "previous": "Previous",
diff --git a/src/vpu-signature-verification.js b/src/vpu-signature-verification.js
new file mode 100644
index 0000000..e90c970
--- /dev/null
+++ b/src/vpu-signature-verification.js
@@ -0,0 +1,836 @@
+import {createI18nInstance} from './i18n.js';
+import {humanFileSize} from 'vpu-common/i18next.js';
+import {css, html} from 'lit-element';
+import {ScopedElementsMixin} from '@open-wc/scoped-elements';
+import VPUSignatureLitElement from "./vpu-signature-lit-element";
+import {PdfPreview} from "./vpu-pdf-preview";
+import * as commonUtils from 'vpu-common/utils';
+import {Icon, MiniSpinner, Button} from 'vpu-common';
+import * as commonStyles from 'vpu-common/styles';
+import {classMap} from 'lit-html/directives/class-map.js';
+import {FileUpload} from 'vpu-file-upload';
+import JSONLD from "vpu-common/jsonld";
+
+const i18n = createI18nInstance();
+
+class SignatureVerification extends ScopedElementsMixin(VPUSignatureLitElement) {
+    constructor() {
+        super();
+        this.lang = i18n.language;
+        this.entryPointUrl = commonUtils.getAPiUrl();
+        this.verifiedFiles = [];
+        this.verifiedFilesCount = 0;
+        this.errorFiles = [];
+        this.errorFilesCount = 0;
+        this.uploadInProgress = false;
+        this.queueingInProgress = false;
+        this.uploadStatusFileName = "";
+        this.uploadStatusText = "";
+        this.currentFile = {};
+        this.currentFileName = "";
+        this.currentFilePlacementMode = "";
+        this.currentFileSignaturePlacement = {};
+        this.queueBlockEnabled = false;
+        this.queuedFiles = [];
+        this.queuedFilesCount = 0;
+        this.verificationProcessEnabled = false;
+        this.verificationProcessActive = false;
+        this.previewInProgress = false;
+        this.currentPreviewQueueKey = '';
+
+        // will be set in function update
+        this.verificationUrl = "";
+    }
+
+    static get scopedElements() {
+        return {
+            'vpu-icon': Icon,
+            'vpu-fileupload': FileUpload,
+            'vpu-pdf-preview': PdfPreview,
+            'vpu-mini-spinner': MiniSpinner,
+            'vpu-button': Button,
+        };
+    }
+
+    static get properties() {
+        return {
+            lang: { type: String },
+            entryPointUrl: { type: String, attribute: 'entry-point-url' },
+            verifiedFiles: { type: Array, attribute: false },
+            verifiedFilesCount: { type: Number, attribute: false },
+            queuedFilesCount: { type: Number, attribute: false },
+            errorFiles: { type: Array, attribute: false },
+            errorFilesCount: { type: Number, attribute: false },
+            uploadInProgress: { type: Boolean, attribute: false },
+            queueingInProgress: { type: Boolean, attribute: false },
+            uploadStatusFileName: { type: String, attribute: false },
+            uploadStatusText: { type: String, attribute: false },
+            verificationProcessEnabled: { type: Boolean, attribute: false },
+            verificationProcessActive: { type: Boolean, attribute: false },
+            queueBlockEnabled: { type: Boolean, attribute: false },
+            currentFile: { type: Object, attribute: false },
+            currentFileName: { type: String, attribute: false },
+            previewInProgress: { type: Boolean, attribute: false },
+            isSignaturePlacement: { type: Boolean, attribute: false },
+        };
+    }
+
+    connectedCallback() {
+        super.connectedCallback();
+        // needs to be called in a function to get the variable scope of "this"
+        setInterval(() => { this.handleQueuedFiles(); }, 1000);
+    }
+
+    onQueuedFilesChanged(ev) {
+        const detail = ev.detail;
+        if (!this.queueBlockEnabled && detail.queuedFilesCount)
+            this.queueBlockEnabled = true;
+        this.queuedFiles = detail.queuedFiles;
+        this.queuedFilesCount = detail.queuedFilesCount;
+    }
+
+    /**
+     * Processes queued files
+     */
+    async handleQueuedFiles() {
+        this.endVerificationProcessIfQueueEmpty();
+        if (this.queuedFilesCount === 0) {
+            // reset verificationProcessEnabled button
+            this.verificationProcessEnabled = false;
+            return;
+        }
+
+        if (!this.verificationProcessEnabled || this.uploadInProgress) {
+            return;
+        }
+
+        this.previewInProgress = false;
+
+        const key = Object.keys(this.queuedFiles)[0];
+
+        // take the file off the queue
+        let file = this.takeFileFromQueue(key);
+        this.currentFile = file;
+
+        this.uploadInProgress = true;
+        let params = {};
+
+        await this._("#file-upload").uploadFile(file, params);
+        this.uploadInProgress = false;
+    }
+
+    /**
+     * Called when preview is "canceled"
+     *
+     * @param event
+     */
+    hidePDF(event) {
+        this.previewInProgress = false;
+    }
+
+    /**
+     * Decides if the "beforeunload" event needs to be canceled
+     *
+     * @param event
+     */
+    onReceiveBeforeUnload(event) {
+        // we don't need to stop if there are no signed files
+        if (this.verifiedFilesCount === 0) {
+            return;
+        }
+
+        // we need to handle custom events ourselves
+        if (!event.isTrusted) {
+            // note that this only works with custom event since calls of "confirm" are ignored
+            // in the non-custom event, see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
+            const result = confirm(i18n.t('signature-verification.confirm-page-leave'));
+
+            // don't stop the page leave if the user wants to leave
+            if (result) {
+                return;
+            }
+        }
+
+        // Cancel the event as stated by the standard
+        event.preventDefault();
+
+        // Chrome requires returnValue to be set
+        event.returnValue = '';
+    }
+
+    endVerificationProcessIfQueueEmpty() {
+        if (this.queuedFilesCount === 0 && this.verificationProcessActive) {
+            this.verificationProcessActive = false;
+        }
+    }
+
+    /**
+     * @param ev
+     */
+    onAllUploadStarted(ev) {
+        console.log("Start queuing process!");
+        this.queueingInProgress = true;
+    }
+
+    /**
+     * @param ev
+     */
+    onAllUploadFinished(ev) {
+        console.log("Finished queuing process!");
+        this.queueingInProgress = false;
+    }
+
+    /**
+     * @param ev
+     */
+    onFileUploadStarted(ev) {
+        this.uploadStatusFileName = ev.detail.fileName;
+        this.uploadStatusText = i18n.t('signature-verification.upload-status-file-text', {
+            fileName: ev.detail.fileName,
+            fileSize: humanFileSize(ev.detail.fileSize, false),
+        });
+    }
+
+    addToErrorFiles(file) {
+        this.endVerificationProcessIfQueueEmpty();
+
+        // this doesn't seem to trigger an update() execution
+        this.errorFiles[Math.floor(Math.random() * 1000000)] = file;
+        // this triggers the correct update() execution
+        this.errorFilesCount++;
+
+        if (window._paq !== undefined) {
+            window._paq.push(['trackEvent', 'officiallyVerification', 'VerificationFailed', file.json["hydra:description"]]);
+        }
+    }
+
+    /**
+     * @param ev
+     */
+    onFileUploadFinished(ev) {
+        if (ev.detail.status !== 201) {
+            this.addToErrorFiles(ev.detail);
+        } else if (ev.detail.json["@type"] === "https://schema.tugraz.at/ElectronicSignatureVerificationReport" ) {
+            // this doesn't seem to trigger an update() execution
+            this.verifiedFiles.push(ev.detail.json);
+            // this triggers the correct update() execution
+            this.verifiedFilesCount++;
+            const entryPoint = ev.detail.json;
+            this.currentFileName = entryPoint.name;
+            this.endVerificationProcessIfQueueEmpty();
+        }
+    }
+
+    update(changedProperties) {
+        changedProperties.forEach((oldValue, propName) => {
+            switch (propName) {
+                case "lang":
+                    i18n.changeLanguage(this.lang);
+                    break;
+                case "entryPointUrl":
+                    JSONLD.initialize(this.entryPointUrl, (jsonld) => {
+                        const apiUrlBase = jsonld.getApiUrlForEntityName("ElectronicSignatureVerificationReport");
+                        this.verificationUrl = apiUrlBase + "/create";
+                    });
+                    break;
+            }
+
+            // console.log(propName, oldValue);
+        });
+
+        super.update(changedProperties);
+    }
+
+    onLanguageChanged(e) {
+        this.lang = e.detail.lang;
+    }
+
+    /**
+     * Re-Upload all failed files
+     */
+    reUploadAllClickHandler() {
+        const that = this;
+
+        // we need to make a copy and reset the queue or else our queue will run crazy
+        const errorFilesCopy = {...this.errorFiles};
+        this.errorFiles = [];
+        this.errorFilesCount = 0;
+
+        commonUtils.asyncObjectForEach(errorFilesCopy, async (file, id) => {
+            await this.fileUploadClickHandler(file.file, id);
+        });
+
+        that._("#re-upload-all-button").stop();
+    }
+
+    /**
+     * Queues a failed pdf-file again
+     *
+     * @param file
+     * @param id
+     */
+    async fileQueueingClickHandler(file, id) {
+        this.takeFailedFileFromQueue(id);
+        return this._("#file-upload").queueFile(file);
+    }
+
+    /**
+     * Shows the preview
+     *
+     * @param key
+     */
+    async showPreview(key) {
+        if (this.verificationProcessEnabled) {
+            return;
+        }
+
+        const file = this._("#file-upload").getQueuedFile(key);
+        this.currentFile = file;
+        this.currentPreviewQueueKey = key;
+        console.log(file);
+        // start signature placement process
+        this.previewInProgress = true;
+        const previewTag = this.constructor.getScopedTagName("vpu-pdf-preview");
+        await this._(previewTag).showPDF(file);
+    }
+
+    /**
+     * Takes a file off of the queue
+     *
+     * @param key
+     */
+    takeFileFromQueue(key) {
+        return this._("#file-upload").takeFileFromQueue(key);
+    }
+
+    /**
+     * Takes a failed file off of the queue
+     *
+     * @param key
+     */
+    takeFailedFileFromQueue(key) {
+        const file = this.errorFiles.splice(key, 1);
+        this.errorFilesCount = Object.keys(this.errorFiles).length;
+        return file;
+    }
+
+    clearQueuedFiles() {
+        this._("#file-upload").clearQueuedFiles();
+    }
+
+    clearVerifiedFiles() {
+        this.verifiedFiles = [];
+        this.verifiedFilesCount = 0;
+    }
+
+    clearErrorFiles() {
+        this.errorFiles = [];
+        this.errorFilesCount = 0;
+    }
+
+    isUserInterfaceDisabled() {
+        return this.previewInProgress || this.externalAuthInProgress || this.uploadInProgress;
+    }
+
+    static get styles() {
+        // language=css
+        return css`
+            ${commonStyles.getThemeCSS()}
+            ${commonStyles.getGeneralCSS(false)}
+            ${commonStyles.getButtonCSS()}
+            ${commonStyles.getNotificationCSS()}
+
+            #pdf-preview {
+                min-width: 320px;
+            }
+
+            h2:first-child {
+                margin-top: 0;
+            }
+
+            h2 {
+                margin-bottom: 10px;
+            }
+
+            strong {
+                font-weight: 600;
+            }
+
+            #pdf-preview .box-header {
+                border: 1px solid #000;
+                border-bottom-width: 0;
+                padding: 0.5em 0.5em 0 0.5em;
+            }
+
+            .hidden {
+                display: none;
+            }
+
+            .files-block.field:not(:last-child) {
+                margin-bottom: 40px;
+            }
+
+            .files-block .file {
+                margin: 10px 0;
+            }
+
+            .error-files .file {
+                display: grid;
+                grid-template-columns: 40px auto;
+            }
+
+            .files-block .file .button-box {
+                display: flex;
+                align-items: center;
+            }
+
+            .files-block .file .info {
+                display: inline-block;
+                vertical-align: middle;
+            }
+
+            .file .info strong {
+                font-weight: 600;
+            }
+
+            .notification vpu-mini-spinner {
+                position: relative;
+                top: 2px;
+                margin-right: 5px;
+            }
+
+            .error, #cancel-verification-process {
+                color: #e4154b;
+            }
+            
+            #cancel-verification-process:hover {
+                color: white;
+            }
+
+            /* using vpu-icon doesn't work */
+            button > [name=close], a > [name=close] {
+                font-size: 0.8em;
+            }
+
+            a > [name=close] {
+                color: red;
+            }
+
+            .empty-queue {
+                margin: 10px 0;
+            }
+
+            #grid-container {
+                display: flex;
+                flex-flow: row wrap;
+            }
+
+            #grid-container > div {
+                margin-right: 20px;
+            }
+
+            #grid-container > div:last-child {
+                margin-right: 0;
+                flex: 1 0;
+            }
+
+            .file-block {
+                max-width: 320px;
+            }
+
+            .file-block, .box {
+                border: solid 1px black;
+                padding: 10px;
+            }
+
+            .file-block, .box .file {
+                margin-top: 0;
+            }
+
+            .file-block {
+                margin-bottom: 10px;
+            }
+
+            .file-block .header {
+                display: grid;
+                align-items: center;
+                grid-template-columns: auto 40px;
+                grid-gap: 10px;
+            }
+
+            .file-block.error .header {
+                grid-template-columns: auto 80px;
+            }
+
+            .file-block.error .header .buttons {
+                white-space: nowrap;
+            }
+
+            .file-block div.bottom-line {
+                display: grid;
+                align-items: center;
+                grid-template-columns: auto 190px;
+                grid-gap: 10px;
+                margin-top: 10px;
+            }
+
+            .file-block.error div.bottom-line {
+                display: block;
+            }
+
+            .file-block div.bottom-line .headline {
+                text-align: right;
+            }
+
+            .file-block .filename, .file-block div.bottom-line .headline {
+                text-overflow: ellipsis;
+                overflow: hidden;
+            }
+
+            .file-block .filename {
+                white-space: nowrap;
+            }
+
+            #pdf-preview .button.is-cancel {
+                color: #e4154b;
+            }
+
+            .is-right {
+                float: right;
+            }
+
+            .error-files .header {
+                color: black;
+            }
+
+            /* prevent hovering of disabled default button */
+            .button[disabled]:not(.is-primary):hover {
+                background-color: inherit;
+                color: inherit;
+            }
+
+            .is-disabled, .is-disabled.button[disabled] {
+                opacity: 0.2;
+                pointer-events: none;
+            }
+
+            #pdf-preview .box-header {
+                display: flex;
+                justify-content: space-between;
+                align-items: start;
+            }
+
+            #pdf-preview .box-header .filename {
+                overflow: hidden;
+                text-overflow: ellipsis;
+                margin-right: 0.5em;
+            }
+
+            table.signatures {
+                margin-top: 10px;
+            }
+
+            /* Handling for small displays (like mobile devices) */
+            @media (max-width: 680px) {
+                /* Modal preview, upload and external auth */
+                div.right-container > * {
+                    position: fixed;
+                    padding: 10px;
+                    top: 0;
+                    left: 0;
+                    right: 0;
+                    bottom: 0;
+                    background-color: white;
+                    overflow-y: scroll;
+                }
+
+                /* Don't use the whole screen for the upload progress */
+                #upload-progress {
+                    top: 10px;
+                    left: 10px;
+                    right: 10px;
+                    bottom: inherit;
+                }
+
+                #grid-container > div {
+                    margin-right: 0;
+                    width: 100%;
+                }
+
+                .file-block {
+                    max-width: inherit;
+                }
+            }
+        `;
+    }
+
+    /**
+     * Returns the list of queued files
+     *
+     * @returns {*[]}
+     */
+    getQueuedFilesHtml() {
+        const ids = Object.keys(this.queuedFiles);
+        let results = [];
+
+        ids.forEach((id) => {
+            const file = this.queuedFiles[id];
+
+            results.push(html`
+                <div class="file-block">
+                    <div class="header">
+                        <span class="filename"><strong>${file.name}</strong> (${humanFileSize(file.size)})</span>
+                        <button class="button close"
+                            ?disabled="${this.verificationProcessEnabled}"
+                            title="${i18n.t('signature-verification.remove-queued-file-button-title')}"
+                            @click="${() => { this.takeFileFromQueue(id); }}">
+                            <vpu-icon name="trash"></vpu-icon></button>
+                    </div>
+                    <div class="bottom-line">
+                        <div></div>
+                        <button class="button"
+                            ?disabled="${this.verificationProcessEnabled}"
+                            @click="${() => { this.showPreview(id); }}">${i18n.t('signature-verification.show-preview')}</button>
+                    </div>
+                </div>
+            `);
+        });
+
+        return results;
+    }
+
+    /**
+     * Returns the list of successfully signed files
+     *
+     * @returns {*[]}
+     */
+    getVerifiedFilesHtml() {
+        const ids = Object.keys(this.verifiedFiles);
+        let results = [];
+
+        ids.forEach((id) => {
+            const report = this.verifiedFiles[id];
+            console.log("report", report);
+            let signatures = [];
+
+            report.signatures.forEach((signature) => {
+                console.log("signature", signature);
+
+                signatures.push(html`
+                    <tr>
+                        <td>${signature.givenName}</td>
+                        <td>${signature.familyName}</td>
+                    </tr>
+                `);
+            });
+
+            results.push(html`
+                <div class="file-block">
+                    <div class="header">
+                        <span class="filename"><strong>${report.name}</strong></span>
+                    </div>
+                    <table class="signatures">
+                        <thead>
+                            <th>${i18n.t('signature-verification.given-name')}</th>
+                            <th>${i18n.t('signature-verification.last-name')}</th>
+                        </thead>
+                        <tbody>
+                            ${signatures}
+                        </tbody>
+                    </table>
+                </div>
+            `);
+        });
+
+        return results;
+    }
+
+    /**
+     * Returns the list of files of failed signature processes
+     *
+     * @returns {*[]}
+     */
+    getErrorFilesHtml() {
+        const ids = Object.keys(this.errorFiles);
+        let results = [];
+
+        ids.forEach((id) => {
+            const data = this.errorFiles[id];
+
+            results.push(html`
+                <div class="file-block error">
+                    <div class="header">
+                        <span class="filename"><strong>${data.file.name}</strong> (${humanFileSize(data.file.size)})</span>
+                        <div class="buttons">
+                            <button class="button"
+                                    title="${i18n.t('signature-verification.re-upload-file-button-title')}"
+                                    @click="${() => {this.fileQueueingClickHandler(data.file, id);}}"><vpu-icon name="reload"></vpu-icon></button>
+                            <button class="button"
+                                title="${i18n.t('signature-verification.remove-failed-file-button-title')}"
+                                @click="${() => { this.takeFailedFileFromQueue(id); }}">
+                                <vpu-icon name="trash"></vpu-icon></button>
+                        </div>
+                    </div>
+                    <div class="bottom-line">
+                        <strong class="error">${data.json["hydra:description"]}</strong>
+                    </div>
+                </div>
+            `);
+        });
+
+        return results;
+    }
+
+    hasSignaturePermissions() {
+        return this._hasSignaturePermissions('ROLE_SCOPE_OFFICIAL-SIGNATURE');
+    }
+
+    render() {
+        return html`
+            <div class="${classMap({hidden: !this.isLoggedIn() || !this.hasSignaturePermissions() || this.isLoading()})}">
+                <div class="field">
+                    <h2>${i18n.t('signature-verification.upload-field-label')}</h2>
+                    <div class="control">
+                        <vpu-fileupload id="file-upload"
+                            allowed-mime-types="application/pdf"
+                            decompress-zip
+                            always-send-file
+                            deferred
+                            lang="${this.lang}"
+                            url="${this.verificationUrl}"
+                            ?disabled="${this.verificationProcessActive}"
+                            text="${i18n.t('signature-verification.upload-area-text')}"
+                            button-label="${i18n.t('signature-verification.upload-button-label')}"
+                            @vpu-fileupload-all-start="${this.onAllUploadStarted}"
+                            @vpu-fileupload-file-start="${this.onFileUploadStarted}"
+                            @vpu-fileupload-file-finished="${this.onFileUploadFinished}"
+                            @vpu-fileupload-all-finished="${this.onAllUploadFinished}"
+                            @vpu-fileupload-queued-files-changed="${this.onQueuedFilesChanged}"
+                            ></vpu-fileupload>
+                    </div>
+                </div>
+                           <div id="grid-container">
+                    <div class="left-container">
+                        <div class="files-block field ${classMap({hidden: !this.queueBlockEnabled})}">
+                            <!-- Queued files headline and queueing spinner -->
+                            <h2 class="${classMap({"is-disabled": this.isUserInterfaceDisabled()})}">
+                                ${i18n.t('signature-verification.queued-files-label')}
+                                <vpu-mini-spinner id="queueing-in-progress-spinner"
+                                                  style="font-size: 0.7em"
+                                                  class="${classMap({hidden: !this.queueingInProgress})}"></vpu-mini-spinner>
+                            </h2>
+                            <!-- Buttons to start/stop verification process and clear queue -->
+                            <div class="control field">
+                                <button @click="${this.clearQueuedFiles}"
+                                        ?disabled="${this.queuedFilesCount === 0 || this.verificationProcessActive || this.isUserInterfaceDisabled()}"
+                                        class="button ${classMap({"is-disabled": this.isUserInterfaceDisabled()})}">
+                                    ${i18n.t('signature-verification.clear-all')}
+                                </button>
+                                <button @click="${() => { this.verificationProcessEnabled = true; this.verificationProcessActive = true; }}"
+                                        ?disabled="${this.queuedFilesCount === 0}"
+                                        class="button is-right is-primary ${classMap(
+            {
+                "is-disabled": this.isUserInterfaceDisabled(),
+                hidden: this.verificationProcessActive
+            })}">
+                                    ${i18n.t('signature-verification.start-verification-process-button')}
+                                </button>
+                                <!-- -->
+                                <button @click="${this.stopVerificationProcess}"
+                                        ?disabled="${this.uploadInProgress}"
+                                        id="cancel-verification-process"
+                                        class="button is-right ${classMap({hidden: !this.verificationProcessActive})}">
+                                    ${i18n.t('signature-verification.stop-verification-process-button')}
+                                </button>
+                                <!-- -->
+                            </div>
+                            <!-- List of queued files -->
+                            <div class="control file-list ${classMap({"is-disabled": this.isUserInterfaceDisabled()})}">
+                                ${this.getQueuedFilesHtml()}
+                            </div>
+                            <!-- Text "queue empty" -->
+                            <div class="empty-queue control ${classMap({hidden: this.queuedFilesCount !== 0, "is-disabled": this.isUserInterfaceDisabled()})}">
+                                ${i18n.t('signature-verification.queued-files-empty1')}<br />
+                                ${i18n.t('signature-verification.queued-files-empty2')}
+                            </div>
+                        </div>
+                        <!-- List of verified PDFs -->
+                        <div class="files-block field ${classMap({hidden: this.verifiedFilesCount === 0, "is-disabled": this.isUserInterfaceDisabled()})}">
+                            <h2>${i18n.t('signature-verification.verified-files-label')}</h2>
+                            <!-- Button to clear verified PDFs -->
+                            <div class="field ${classMap({hidden: this.verifiedFilesCount === 0})}">
+                                <div class="control">
+                                    <button @click="${this.clearVerifiedFiles}"
+                                            class="button">
+                                        ${i18n.t('signature-verification.clear-all')}
+                                    </button>
+                                </div>
+                            </div>
+                            <div class="control">
+                                ${this.getVerifiedFilesHtml()}
+                            </div>
+                        </div>
+                        <!-- List of errored files -->
+                        <div class="files-block error-files field ${classMap({hidden: this.errorFilesCount === 0, "is-disabled": this.isUserInterfaceDisabled()})}">
+                            <h2>${i18n.t('signature-verification.error-files-label')}</h2>
+                            <!-- Button to upload errored files again -->
+                            <div class="field ${classMap({hidden: this.errorFilesCount === 0})}">
+                                <div class="control">
+                                    <button @click="${this.clearErrorFiles}"
+                                            class="button">
+                                        ${i18n.t('signature-verification.clear-all')}
+                                    </button>
+                                    <vpu-button id="re-upload-all-button"
+                                                ?disabled="${this.uploadInProgress}"
+                                                value="${i18n.t('signature-verification.re-upload-all-button')}"
+                                                title="${i18n.t('signature-verification.re-upload-all-button-title')}"
+                                                class="is-right"
+                                                @click="${this.reUploadAllClickHandler}"
+                                                type="is-primary"></vpu-button>
+                                </div>
+                            </div>
+                            <div class="control">
+                                ${this.getErrorFilesHtml()}
+                            </div>
+                        </div>
+                    </div>
+                    <div class="right-container">
+                        <!-- PDF preview -->
+                        <div id="pdf-preview" class="field ${classMap({hidden: !this.previewInProgress})}">
+                            <h2>${i18n.t('signature-verification.preview-label')}</h2>
+                            <div class="box-header">
+                                <div class="filename">
+                                    <strong>${this.currentFile.name}</strong> (${humanFileSize(this.currentFile !== undefined ? this.currentFile.size : 0)})
+                                </div>
+                                <button class="button is-cancel"
+                                    @click="${this.hidePDF}"><vpu-icon name="close"></vpu-icon></button>
+                            </div>
+                            <vpu-pdf-preview lang="${this.lang}"
+                                             signature-placeholder-image="official-signature-placeholder.png"
+                                             signature-width="146"
+                                             signature-height="42"
+                                             @vpu-pdf-preview-cancel="${this.hidePDF}"></vpu-pdf-preview>
+                        </div>
+                        <!-- File upload progress -->
+                        <div id="upload-progress" class="field notification is-info ${classMap({hidden: !this.uploadInProgress})}">
+                            <vpu-mini-spinner></vpu-mini-spinner>
+                            <strong>${this.uploadStatusFileName}</strong>
+                            ${this.uploadStatusText}
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <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.hasSignaturePermissions() || !this.isLoggedIn() || this.isLoading()})}">
+                ${i18n.t('error-permission-message')}
+            </div>
+            <div class="${classMap({hidden: !this.isLoading()})}">
+                <vpu-mini-spinner></vpu-mini-spinner>
+            </div>
+        `;
+    }
+}
+
+commonUtils.defineCustomElement('vpu-signature-verification', SignatureVerification);
-- 
GitLab