Document Validation v1.0
Usage
An e-document-validation has 2 attributes that must be provided,
<e-document-validation
api-path="/api/v1-beta/generic/validate"
dark-mode
/>
Attributes
apiPath | Type: String | requireddarkMode | Type: Boolean | false
Examples
Below a few interactive examples of e-document-validation can be found.
Details
<e-document-validation
api-path="/api/v1-beta/generic/validate"
/>
Document Validator
Upload a file for checking and validation based on the latest document requirements. After the check, you will receive a report per document in PDF format.Source Code
e-document-validation.vue
<script setup lang="ts">
import { computed, ref, provide, nextTick, inject, unref, onMounted } from 'vue';
import { useDateFormat, useNow, useDark } from '@vueuse/core';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { includes, some, filter, size, map, endsWith, startsWith, slice } from 'lodash-es';
import jsPDF from 'jspdf';
import html2canvas from 'html2canvas';
import EDocumentValidationInput from '../e-document-validation-input/e-document-validation-input.vue';
export type Props = {
apiPath: string;
cdnPath: string;
optionId?: string;
downloadableReport?: boolean;
hasOptions?: boolean;
};
const props = withDefaults(defineProps<Props>(), {
optionId: null,
downloadableReport: false,
hasOptions: false
});
const router = useRouter();
const { t, locale } = useI18n({ useScope: 'global' });
const mobile = inject('isMobile', false as boolean);
const isMobile = computed(() => unref(mobile));
const isDark = useDark({
valueDark: 'dark',
valueLight: 'light'
});
const maxFileSizeMb = 10;
const downloadingReport = ref(false);
const downloadCancelled = ref(false);
const validationOptionsRef = ref([]);
const heroRef = ref<HTMLElement>();
const inputRef = ref<HTMLElement>();
const assertionsRef = ref<HTMLElement>();
const uploadedFileRef = ref<File>();
const uploadedFile = computed(() => uploadedFileRef.value);
const lastUploadedFileRef = ref<File>();
const lastUploadedFile = computed(() => lastUploadedFileRef.value);
const validationResultsRef = ref<object>();
const validationResults = computed(() => validationResultsRef.value);
function getApiUrl(subPath?: string): URL {
let base = props.apiPath;
if (subPath) {
if (endsWith(base, '/')) base = slice(base, 0, -1);
if (!startsWith(subPath, '/')) subPath = '/' + subPath;
base += subPath;
}
if (startsWith(base, 'http://') || startsWith(base, 'https://')) {
return new URL(base);
} else {
return new URL(base, window.location.origin);
}
}
async function getValidationOptions() {
if (validationOptionsRef.value.length > 0) return;
const userData = unref(inject('authUserData'));
if (!userData?.access_token) return [];
const apiPath = getApiUrl('options');
const headers = new Headers();
headers.append('Authorization', `Bearer ${userData?.access_token}`);
const fetchOptions = {
method: 'GET',
headers: headers
};
try {
const response = await fetch(apiPath, fetchOptions);
const responseJson = await response.json();
const validationOptions = map(responseJson, validationOption => ({
text: validationOption.name,
value: validationOption.id
}));
validationOptionsRef.value = validationOptions;
} catch (err) {
const logErrors = false;
if (logErrors) console.log(err);
}
}
const validationStatusRef = ref({
current: 'default',
last: {},
default: {
header: () => t('pages.validationportal.status.default.header'),
description: () => t('pages.validationportal.status.default.description', [maxFileSizeMb]),
image: '/validationportal/check-mark-blue.svg'
},
dragover: {
header: () => t('pages.validationportal.status.dragover.header'),
description: () => t('pages.validationportal.status.dragover.description'),
image: '/validationportal/dots-blue.svg'
},
uploading: {
header: () => t('pages.validationportal.status.uploading.header'),
description: () => '',
image: '/validationportal/dots-blue.svg'
},
valid: {
header: () => t('pages.validationportal.status.valid.header'),
description: () => '',
image: '/validationportal/check-mark-green.svg'
},
warnings: {
header: () => t('pages.validationportal.status.valid.header'),
description: () => '',
image: '/validationportal/check-mark-orange.svg'
},
invalid: {
header: () => t('pages.validationportal.status.invalid.header'),
description: () => '',
image: '/validationportal/exclamation-mark-red.svg'
},
inconclusive: {
header: () => t('pages.validationportal.status.inconclusive.header'),
description: () => '',
image: '/validationportal/question-mark-orange.svg'
},
error: {
header: () => '',
description: () => '',
image: '/validationportal/exclamation-mark-orange.svg'
}
});
const validationStatus = computed(() => {
const currentStatus = validationStatusRef.value.current;
const status = validationStatusRef.value[currentStatus];
return {
current: currentStatus,
header: typeof status.header === 'function' ? status.header() : status.header,
description:
typeof status.description === 'function' ? status.description() : status.description,
image: status.image
};
});
const reportAvailable = computed(
() =>
props.downloadableReport &&
includes(['valid', 'warnings', 'invalid', 'inconclusive'], validationStatus.value.current)
);
const formRef = ref<HTMLFormElement | null>(null);
const fileInputRef = ref<HTMLInputElement | null>(null);
const openAssertionsRef = ref([]);
const criticalAssertionsCount = computed(() => {
if (!validationResults.value || !validationResults.value.assertions) return 0;
return size(
filter(validationResults.value.assertions, assertion =>
includes(['error', 'fatal'], assertion.flag)
)
);
});
const hasWarnings = computed(() =>
some(validationResults.value.assertions, assertion => assertion.flag === 'warning')
);
// TODO: (result: ValidationResult)
function getStateByResult(result: object) {
if (result?.isValid) return 'valid';
if (result?.isValid === false) return 'invalid';
return 'inconclusive';
}
function setStatus(
newStatus = 'default',
newHeader?: string | (() => string),
newDescription?: string | (() => string)
) {
const currentStatus = validationStatus.value.current;
if (newStatus === 'dragover' && currentStatus !== 'dragover') {
validationStatusRef.value.last = validationStatusRef.value[currentStatus];
validationStatusRef.value.last['status'] = currentStatus;
}
validationStatusRef.value.current = newStatus;
if (newHeader) validationStatusRef.value[newStatus].header = newHeader;
if (newDescription) validationStatusRef.value[newStatus].description = newDescription;
}
// TODO: (result: ValidationResult)
function setStatusByResult(result: object) {
let state = getStateByResult(result);
const header = () =>
t(`pages.validationportal.status.${state}.header`, criticalAssertionsCount.value);
const description = `${uploadedFile.value?.name}<br>${getFileSize(uploadedFile.value?.size)}`;
if (state === 'valid' && hasWarnings.value) state = 'warnings';
setStatus(state, header, description);
}
function getFileSize(bytes: number) {
const KB = 1024;
const MB = KB * 1024;
if (bytes < KB) {
return `${bytes} Bytes`;
} else if (bytes < MB) {
return `${(bytes / KB).toFixed(2)} KB`;
} else {
return `${(bytes / MB).toFixed(2)} MB`;
}
}
async function submitValidation() {
if (!uploadedFileRef.value) return;
const apiPath = getApiUrl();
const apiParams = new URLSearchParams(apiPath.search);
const optionId = new URLSearchParams(window.location.search).get('optionId') || props?.optionId;
if (optionId && !apiParams.has('optionId')) {
apiParams.set('optionId', optionId);
}
apiPath.search = apiParams.toString();
const formData = new FormData();
formData.append('file', uploadedFileRef.value);
const headers = new Headers();
headers.append('X-EConnect-Language', locale.value);
const fetchOptions = {
method: 'POST',
headers: headers,
body: formData
};
try {
const response = await fetch(apiPath, fetchOptions);
validationResultsRef.value = await response.json();
openAssertionsRef.value = [];
setStatusByResult(validationResults.value);
lastUploadedFileRef.value = uploadedFileRef.value;
if (formRef.value) formRef.value.reset();
uploadedFileRef.value = null;
} catch {
setStatus();
}
}
function assertionDropdown(assertionKey) {
const index = openAssertionsRef.value.indexOf(assertionKey);
if (index === -1) {
openAssertionsRef.value.push(assertionKey);
} else {
openAssertionsRef.value.splice(index, 1);
}
}
function triggerFileInput(event) {
event.preventDefault();
if (fileInputRef.value) {
fileInputRef.value.click();
}
}
function preventFileOpening(event: DragEvent) {
event.preventDefault();
}
function openAllAssertions() {
const assertions = validationResults.value?.assertions;
if (assertions) {
openAssertionsRef.value = assertions.map((_, index) => index);
}
}
async function downloadReport() {
if (downloadingReport.value) return;
downloadingReport.value = true;
downloadCancelled.value = false;
const heroHeader = document.getElementById('hero-header');
const heroDescription = document.getElementById('hero-description');
heroHeader.innerText = t('pages.validationportal.report.header');
heroDescription.innerText = t('pages.validationportal.report.description');
const imgElement = heroRef.value.querySelector('.hero-image');
if (imgElement)
imgElement.src = `${props.cdnPath}/validationportal/report/${validationStatus.value.current}.png`;
openAllAssertions();
await nextTick();
const pdfWidth = 595;
const pdfHeight = 842;
const marginTop = 20;
const marginLeft = 20;
const marginBetween = 10;
const pageBackgroundColor = isDark.value ? '#151927' : '#F8F8F8';
const contentTopOffset = 40;
const logoHeight = 20;
const pageHeightAvailable = pdfHeight - contentTopOffset - logoHeight;
const eConnectLogo = isDark.value
? `${props.cdnPath}/logos/econnect-white.png`
: `${props.cdnPath}/logos/econnect.png`;
const doc = new jsPDF({ orientation: 'portrait', unit: 'pt', format: [pdfWidth, pdfHeight] });
const logo = new Image();
logo.crossOrigin = 'Anonymous';
logo.src = eConnectLogo;
await new Promise(resolve => (logo.onload = resolve));
const logoWidth = (logoHeight / logo.height) * logo.width;
const centerX = (pdfWidth - logoWidth) / 2;
function addLogo() {
doc.addImage(logo, 'PNG', centerX, marginTop, logoWidth, logoHeight, null, 'FAST');
}
function setPageBackground() {
doc.setFillColor(pageBackgroundColor);
doc.rect(0, 0, pdfWidth, pdfHeight, 'F');
}
setPageBackground();
addLogo();
let currentYPosition = contentTopOffset + logoHeight;
if (!downloadCancelled.value && heroRef.value) {
const heroCanvas = await html2canvas(heroRef.value, {
useCORS: true,
scale: 0.9,
logging: false,
backgroundColor: null,
ignoreElements: element => element.classList.contains('exclude-from-report')
});
if (downloadCancelled.value) {
downloadingReport.value = false;
return;
}
const heroImgData = heroCanvas.toDataURL('image/png');
const heroHeight = (heroCanvas.height / heroCanvas.width) * (pdfWidth - 2 * marginLeft);
doc.addImage(
heroImgData,
'PNG',
marginLeft,
currentYPosition,
pdfWidth - 2 * marginLeft,
heroHeight
);
currentYPosition += heroHeight + marginBetween;
}
if (!downloadCancelled.value && inputRef.value) {
const inputCanvas = await html2canvas(inputRef.value, {
useCORS: true,
scale: 0.9,
logging: false,
backgroundColor: null,
ignoreElements: element =>
element.classList.contains('assertions-container') ||
element.classList.contains('exclude-from-report')
});
const inputImgData = inputCanvas.toDataURL('image/png');
const inputHeight = (inputCanvas.height / inputCanvas.width) * (pdfWidth - 2 * marginLeft);
doc.addImage(
inputImgData,
'PNG',
marginLeft,
currentYPosition,
pdfWidth - 2 * marginLeft,
inputHeight
);
currentYPosition += inputHeight + marginBetween;
}
if (!downloadCancelled.value && assertionsRef.value) {
const assertions = Array.from(assertionsRef.value.children) as HTMLElement[];
for (const assertion of assertions) {
if (downloadCancelled.value) break;
const assertionCanvas = await html2canvas(assertion, {
useCORS: true,
scale: 0.9,
logging: false,
backgroundColor: null,
ignoreElements: element => element.classList.contains('exclude-from-report')
});
const assertionHeight =
(assertionCanvas.height / assertionCanvas.width) * (pdfWidth - 2 * marginLeft);
if (currentYPosition + assertionHeight > pageHeightAvailable) {
if (downloadCancelled.value) break;
doc.addPage();
setPageBackground();
addLogo();
currentYPosition = contentTopOffset + logoHeight;
}
const assertionImgData = assertionCanvas.toDataURL('image/png');
doc.addImage(
assertionImgData,
'PNG',
marginLeft,
currentYPosition,
pdfWidth - 2 * marginLeft,
assertionHeight
);
currentYPosition += assertionHeight + marginBetween;
}
}
if (!downloadCancelled.value) {
const formattedDate = useDateFormat(useNow(), 'DD-MM-YYYY');
const fileName = lastUploadedFile.value?.name.replace(/\.[^/.]+$/, '');
doc.save(`${formattedDate.value}-${fileName}-econnect-validation-report.pdf`);
}
if (imgElement) imgElement.src = props.cdnPath + validationStatus.value.image;
heroHeader.innerText = t('pages.validationportal.hero.header');
heroDescription.innerText = t('pages.validationportal.hero.description');
downloadingReport.value = false;
}
function cancelDownload() {
downloadCancelled.value = true;
downloadingReport.value = false;
}
onMounted(() => {
getValidationOptions();
const query = { ...router.query };
if (props.optionId && !query?.optionId) {
query.optionId = props.optionId;
router.replace({ query });
}
});
provide('validationStatusRef', validationStatusRef);
provide('validationResultsRef', validationResultsRef);
provide('validationOptionsRef', validationOptionsRef);
provide('openAssertionsRef', openAssertionsRef);
provide('formRef', formRef);
provide('fileInputRef', fileInputRef);
provide('uploadedFileRef', uploadedFileRef);
provide('reportAvailable', reportAvailable);
provide('getFileSize', getFileSize);
provide('inputRef', inputRef);
provide('assertionsRef', assertionsRef);
</script>
<template>
<div
v-if="downloadingReport"
class="background-overlay"
/>
<div
v-if="downloadingReport"
class="downloading-overlay"
>
<div class="info-card">
<h1>{{ t('pages.validationportal.report.downloading.header') }}</h1>
<span>{{ t('pages.validationportal.report.downloading.description') }}</span>
<span
class="cancel-button"
@click="cancelDownload"
>{{ t('pages.validationportal.report.downloading.cancelButton') }}</span
>
</div>
</div>
<div
class="e-document-validation"
:class="{ mobile: isMobile }"
@drop="preventFileOpening"
@dragover="preventFileOpening"
>
<div
ref="heroRef"
class="document-validation-hero"
>
<div>
<h1 id="hero-header">
{{ t('pages.validationportal.hero.header') }}
</h1>
<span id="hero-description">
{{ t('pages.validationportal.hero.description') }}
</span>
<button
class="hero-button exclude-from-report"
type="button"
:disabled="validationStatus.current === 'uploading'"
@click="reportAvailable ? downloadReport() : triggerFileInput($event)"
>
{{
downloadableReport && reportAvailable
? t('pages.validationportal.input.downloadButton')
: t('pages.validationportal.hero.button')
}}
</button>
</div>
<img
class="hero-image"
:src="cdnPath + validationStatus.image"
/>
</div>
<e-document-validation-input
:max-size-mb="maxFileSizeMb"
:has-options="hasOptions"
@click:assertion-header="id => assertionDropdown(id)"
@submit:validation="submitValidation"
@update:status="
newStatus => setStatus(newStatus?.status, newStatus?.header, newStatus?.description)
"
@download:report="downloadReport"
/>
</div>
</template>
<style scoped lang="scss">
.background-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--e-white);
z-index: 2;
opacity: 0.7;
}
.downloading-overlay {
max-width: 1150px;
position: fixed;
top: 0;
width: 100%;
height: 100%;
z-index: 3;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.info-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--e-foreground);
border-radius: 16px;
background-color: var(--e-white);
padding: 50px 50px 10px;
border: 4px solid var(--e-background);
h1 {
margin: 0;
}
.cancel-button {
cursor: pointer;
font-family: var(--e-navigation-button-font-family);
margin: 20px 0 10px;
}
}
}
.e-document-validation {
max-width: 1150px;
.document-validation-hero {
display: flex;
justify-content: space-between;
width: 100%;
border-radius: 16px;
padding: 20px;
background-color: var(--e-white);
div {
max-width: 60%;
margin: 60px;
h1,
span {
color: var(--e-foreground);
}
.e-button {
margin-top: 25px;
}
.hero-button {
display: flex;
margin-top: 25px;
background-color: var(--e-accent);
border: 2px solid var(--e-accent);
color: var(--e-gray-0);
border-radius: 15px;
cursor: pointer;
padding: 10px 20px;
font-family: var(--e-navigation-button-font-family);
font-size: var(--e-navigation-button-font-size);
font-weight: var(--e-navigation-button-font-weight);
text-transform: var(--e-navigation-button-font-caps);
&:hover,
&:focus {
background-color: unset;
color: var(--e-accent);
}
&:disabled {
cursor: unset;
background-color: var(--e-disabled);
border: 2px solid var(--e-disabled);
color: var(--e-gray-400);
&:hover,
&:focus {
background-color: var(--e-disabled);
color: var(--e-gray-400);
}
}
}
}
.hero-image {
max-width: 40%;
width: 40%;
padding: 20px;
margin: auto 0;
}
}
&.mobile .document-validation-hero {
flex-direction: column-reverse;
div {
max-width: unset;
margin: 10px;
}
.hero-image {
max-width: unset;
width: unset;
}
}
}
</style>