diff --git a/assets/vpu-qualified-signature-pdf-upload.metadata.json b/assets/vpu-qualified-signature-pdf-upload.metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..25d61b95bc830be123b7045f6312a08af1caf239 --- /dev/null +++ b/assets/vpu-qualified-signature-pdf-upload.metadata.json @@ -0,0 +1,17 @@ +{ + "element": "vpu-qualified-signature-pdf-upload", + "module_src": "vpu-qualified-signature-pdf-upload.js", + "routing_name": "qualified-pdf-upload", + "name": { + "de": "Persönliche aufbringen", + "en": "Qualifiedly sign" + }, + "short_name": { + "de": "Persönliche Signatur aufbringen", + "en": "Qualifiedly sign" + }, + "description": { + "de": "Erlaubt das Hochladen von PDF Dateien um sie mit einer persönlichen Signatur zu versehen", + "en": "Allows upload of PDF files to qualifiedly sign them" + } +} diff --git a/assets/vpu-signature.topic.metadata.json b/assets/vpu-signature.topic.metadata.json index 5f421efe115df843447166a752b811f8f151e7dc..3c6b9ec0930a2674e46c86e3d5bb9896ff7869c8 100644 --- a/assets/vpu-signature.topic.metadata.json +++ b/assets/vpu-signature.topic.metadata.json @@ -13,7 +13,8 @@ }, "routing_name": "signature", "activities": [ - {"path": "vpu-official-signature-pdf-upload.metadata.json"} + {"path": "vpu-official-signature-pdf-upload.metadata.json"}, + {"path": "vpu-qualified-signature-pdf-upload.metadata.json"} ], "attributes": [] } \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js index 567a122afee18447d01c8d94acfe8a49512aa74b..d446bf13be4ac688f070577a3fd3fb851523e2cc 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -143,6 +143,7 @@ export default { input: (build != 'test') ? [ 'src/vpu-signature.js', 'src/vpu-official-signature-pdf-upload.js', + 'src/vpu-qualified-signature-pdf-upload.js', ] : glob.sync('test/**/*.js'), output: { dir: 'dist', diff --git a/src/i18n/de/translation.json b/src/i18n/de/translation.json index 008adb4f034278b961b6db70dc2b1270ba1a3683..b5d717e3613741ffc792665c7dc4e893d5fed14d 100644 --- a/src/i18n/de/translation.json +++ b/src/i18n/de/translation.json @@ -13,6 +13,20 @@ "re-upload-all-button": "Alle erneut hochladen", "re-upload-all-button-title": "Alle fehlgeschlagen Uploads erneut hochladen" }, + "qualified-pdf-upload": { + "upload-field-label": "PDF Dateien zum Signieren hochladen", + "upload-area-text": "Sie können in diesem Bereich PDF Dateien per Drag & Drop oder per Direktauswahl hochladen", + "signed-files-label": "Signierte Dateien", + "download-zip-button": "Als ZIP Datei herunterladen", + "download-zip-button-tooltip": "Alle signierten Dateien als ZIP Datei herunterladen", + "upload-button-label": "PDF Dateien auswählen", + "download-file-button-title": "Signiertes PDF herunterladen", + "error-files-label": "Fehlgeschlagene Signiervorgänge", + "re-upload-file-button-title": "Erneut hochladen", + "upload-status-file-text": "({{fileSize}}) wird hochgeladen und verarbeitet...", + "re-upload-all-button": "Alle erneut hochladen", + "re-upload-all-button-title": "Alle fehlgeschlagen Uploads erneut hochladen" + }, "error-summary": "Ein Fehler ist aufgetreten", "error-permission-message": "Sie müssen das Recht auf Amtssignaturen besitzen um diese Funktion nutzen zu können!", "error-login-message": "Sie müssen eingeloggt sein um diese Funktion nutzen zu können!" diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json index 752e914ab8293bc83bc8af1c620fcdeba4d013c2..5705ce8d1a07d325e7446527b2937d734991c44f 100644 --- a/src/i18n/en/translation.json +++ b/src/i18n/en/translation.json @@ -13,6 +13,20 @@ "re-upload-all-button": "Upload all", "re-upload-all-button-title": "Upload all failed uploads again" }, + "qualified-pdf-upload": { + "upload-field-label": "Upload PDF files to sign", + "upload-area-text": "In this area you can upload PDF files via Drag & Drop or by selecting them directly", + "signed-files-label": "Signed files", + "download-zip-button": "Download ZIP", + "download-zip-button-tooltip": "Download all signed files as ZIP file", + "upload-button-label": "Select PDF files", + "download-file-button-title": "Download signed PDF", + "error-files-label": "Failed signing 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" + }, "error-summary": "An error occurred", "error-permission-message": "You need have permissions to use the official signature to use this function!", "error-login-message": "You need to be logged in to use this function!" diff --git a/src/vpu-qualified-signature-pdf-upload.js b/src/vpu-qualified-signature-pdf-upload.js new file mode 100644 index 0000000000000000000000000000000000000000..84e5096ea49613dd96a09179ab01999bb3d69bc7 --- /dev/null +++ b/src/vpu-qualified-signature-pdf-upload.js @@ -0,0 +1,320 @@ +import {createI18nInstance} from './i18n.js'; +import {humanFileSize} from 'vpu-common/i18next.js'; +import {css, html} from 'lit-element'; +import VPUSignatureLitElement from "./vpu-signature-lit-element"; +import * as commonUtils from 'vpu-common/utils'; +import * as utils from './utils'; +import JSZip from 'jszip/dist/jszip.js'; +import 'file-saver'; +import * as commonStyles from 'vpu-common/styles'; +import {classMap} from 'lit-html/directives/class-map.js'; +import 'vpu-file-upload'; + +const i18n = createI18nInstance(); + +class QualifiedSignaturePdfUpload extends VPUSignatureLitElement { + constructor() { + super(); + this.lang = i18n.language; + this.entryPointUrl = commonUtils.getAPiUrl(); + this.signingUrl = this.entryPointUrl + "/qualifiedly_signed_documents/sign"; + this.signedFiles = []; + this.signedFilesCount = 0; + this.errorFiles = []; + this.errorFilesCount = 0; + this.uploadInProgress = false; + this.uploadStatusFileName = ""; + this.uploadStatusText = ""; + } + + static get properties() { + return { + lang: { type: String }, + entryPointUrl: { type: String, attribute: 'entry-point-url' }, + signedFiles: { type: Array, attribute: false }, + signedFilesCount: { type: Number, attribute: false }, + errorFiles: { type: Array, attribute: false }, + errorFilesCount: { type: Number, attribute: false }, + uploadInProgress: { type: Boolean, attribute: false }, + uploadStatusFileName: { type: String, attribute: false }, + uploadStatusText: { type: String, attribute: false }, + }; + } + + connectedCallback() { + super.connectedCallback(); + + this.updateComplete.then(()=>{ + const fileUpload = this._("#file-upload"); + fileUpload.addEventListener('vpu-fileupload-all-start', this.onAllUploadStarted.bind(this)); + fileUpload.addEventListener('vpu-fileupload-file-start', this.onFileUploadStarted.bind(this)); + fileUpload.addEventListener('vpu-fileupload-file-finished', this.onFileUploadFinished.bind(this)); + fileUpload.addEventListener('vpu-fileupload-all-finished', this.onAllUploadFinished.bind(this)); + }); + } + + /** + * @param ev + */ + onAllUploadStarted(ev) { + console.log("Start upload process!"); + this.uploadInProgress = true; + } + + /** + * @param ev + */ + onFileUploadStarted(ev) { + console.log(ev); + this.uploadStatusFileName = ev.detail.fileName; + this.uploadStatusText = i18n.t('qualified-pdf-upload.upload-status-file-text', { + fileName: ev.detail.fileName, + fileSize: humanFileSize(ev.detail.fileSize, false), + }); + } + + /** + * @param ev + */ + onFileUploadFinished(ev) { + if (ev.detail.status !== 201) { + // this doesn't seem to trigger an update() execution + this.errorFiles[Math.floor(Math.random() * 1000000)] = ev.detail; + // this triggers the correct update() execution + this.errorFilesCount++; + } else if (ev.detail.json["@type"] === "http://schema.org/MediaObject" ) { + // this doesn't seem to trigger an update() execution + this.signedFiles.push(ev.detail.json); + // this triggers the correct update() execution + this.signedFilesCount++; + } + } + + /** + * @param ev + */ + onAllUploadFinished(ev) { + console.log("Finished upload process!"); + this.uploadInProgress = false; + } + + update(changedProperties) { + changedProperties.forEach((oldValue, propName) => { + if (propName === "lang") { + i18n.changeLanguage(this.lang); + } + + console.log(propName, oldValue); + }); + + super.update(changedProperties); + } + + onLanguageChanged(e) { + this.lang = e.detail.lang; + } + + /** + * Download signed pdf files as zip + */ + zipDownloadClickHandler() { + // see: https://stuk.github.io/jszip/ + let zip = new JSZip(); + const that = this; + let fileNames = []; + + // add all signed pdf files + this.signedFiles.forEach((file) => { + let fileName = file.name; + + // add pseudo-random string on duplicate file name + if (fileNames.indexOf(fileName) !== -1) { + fileName = utils.baseName(fileName) + "-" + Math.random().toString(36).substring(7) + ".pdf"; + } + + fileNames.push(fileName); + zip.file(fileName, utils.getPDFFileBase64Content(file), {base64: true}); + }); + + zip.generateAsync({type:"blob"}) + .then(function(content) { + // save with FileSaver.js + // see: https://github.com/eligrey/FileSaver.js + saveAs(content, "signed-documents.zip"); + + that._("#zip-download-button").stop(); + }); + } + + /** + * 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(); + } + + /** + * Download one signed pdf file + * + * @param file + */ + fileDownloadClickHandler(file) { + const arr = utils.convertDataURIToBinary(file.contentUrl); + const blob = new Blob([arr], { type: utils.getDataURIContentType(file.contentUrl) }); + + // see: https://github.com/eligrey/FileSaver.js + saveAs(blob, file.name); + } + + /** + * Uploads a failed pdf file again + * + * @param file + * @param id + */ + async fileUploadClickHandler(file, id) { + this.uploadInProgress = true; + this.errorFiles.splice(id, 1); + this.errorFilesCount = this.errorFiles.length; + await this._("#file-upload").uploadFile(file); + this.uploadInProgress = false; + } + + static get styles() { + // language=css + return css` + ${commonStyles.getThemeCSS()} + ${commonStyles.getGeneralCSS()} + ${commonStyles.getButtonCSS()} + ${commonStyles.getNotificationCSS()} + + h2 { + margin-bottom: inherit; + } + + .hidden { + display: none; + } + + .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 { + color: #e4154b; + } + `; + } + + getSignedFilesHtml() { + return this.signedFiles.map(file => html` + <div class="file"> + <a class="is-download" + title="${i18n.t('qualified-pdf-upload.download-file-button-title')}" + @click="${() => {this.fileDownloadClickHandler(file);}}"> + ${file.name} (${humanFileSize(file.contentSize)}) <vpu-icon name="download"></vpu-icon></a> + </div> + `); + } + + getErrorFilesHtml() { + return this.errorFiles.map((data, id) => html` + <div class="file"> + <div class="button-box"> + <button class="button is-small" + title="${i18n.t('qualified-pdf-upload.re-upload-file-button-title')}" + @click="${() => {this.fileUploadClickHandler(data.file, id);}}"><vpu-icon name="reload"></vpu-icon></button> + </div> + <div class="info"> + ${data.file.name} (${humanFileSize(data.file.size)}) + <strong class="error">${data.json["hydra:description"]}</strong> + </div> + </div> + `); + } + + render() { + return html` + <div class="${classMap({hidden: !this.isLoggedIn() || !this.hasSignaturePermissions()})}"> + <div class="field"> + <h2>${i18n.t('qualified-pdf-upload.upload-field-label')}</h2> + <div class="control"> + <vpu-fileupload id="file-upload" lang="${this.lang}" url="${this.signingUrl}" accept="application/pdf" + text="${i18n.t('qualified-pdf-upload.upload-area-text')}" button-label="${i18n.t('qualified-pdf-upload.upload-button-label')}"></vpu-fileupload> + </div> + </div> + <div class="field notification is-info ${classMap({hidden: !this.uploadInProgress})}"> + <vpu-mini-spinner></vpu-mini-spinner> + <strong>${this.uploadStatusFileName}</strong> + ${this.uploadStatusText} + </div> + <div class="files-block field ${classMap({hidden: this.signedFilesCount === 0})}"> + <h2>${i18n.t('qualified-pdf-upload.signed-files-label')}</h2> + <div class="control"> + ${this.getSignedFilesHtml()} + </div> + </div> + <div class="field ${classMap({hidden: this.signedFilesCount === 0})}"> + <div class="control"> + <vpu-button id="zip-download-button" value="${i18n.t('qualified-pdf-upload.download-zip-button')}" title="${i18n.t('qualified-pdf-upload.download-zip-button-tooltip')}" @click="${this.zipDownloadClickHandler}" type="is-primary"></vpu-button> + </div> + </div> + <div class="files-block error-files field ${classMap({hidden: this.errorFilesCount === 0})}"> + <h2 class="error">${i18n.t('qualified-pdf-upload.error-files-label')}</h2> + <div class="control"> + ${this.getErrorFilesHtml()} + </div> + </div> + <div class="field ${classMap({hidden: this.errorFilesCount === 0})}"> + <div class="control"> + <vpu-button id="re-upload-all-button" ?disabled="${this.uploadInProgress}" value="${i18n.t('qualified-pdf-upload.re-upload-all-button')}" title="${i18n.t('qualified-pdf-upload.re-upload-all-button-title')}" @click="${this.reUploadAllClickHandler}" type="is-primary"></vpu-button> + </div> + </div> + </div> + <div class="notification is-warning ${classMap({hidden: this.isLoggedIn()})}"> + ${i18n.t('error-login-message')} + </div> + <div class="notification is-danger ${classMap({hidden: this.hasSignaturePermissions() || !this.isLoggedIn()})}"> + ${i18n.t('error-permission-message')} + </div> + `; + } +} + +commonUtils.defineCustomElement('vpu-qualified-signature-pdf-upload', QualifiedSignaturePdfUpload);