@econnect/webcomponents-library
Documentation
Styleguide
Documentation
Styleguide
  • Getting Started

    • Installation
    • Patch Notes
    • Styleguide
  • Styling

    • e-connect colors
  • UI Components

    • Form

      • Search Field
      • Form Field
      • Forms
    • Filter

      • Filter
      • Search Filter
    • Grid

      • Grid
      • Grid Row
    • Inputs

      • File Input
      • Input Checkbox
      • Input Dropdown
      • Input Date
      • Input Date Time
      • Input Date Time Local
      • Input File
      • Input Label
      • Input Multi Select
      • Input Number
      • Input Radio
      • Input Range
      • Input Select
      • Input Switch
      • Input Slider
      • Input Text
      • Input TextArea
      • Input Time
      • Input Time Range
    • Controls

      • Booleans
      • Button
      • Checkboxes
      • Date Displays
      • Dropdown Lists
      • Text Displays
    • Headers

      • e-header
    • Cards

      • e-card
    • Images

      • Icon
      • Images
      • Flag Image
      • Hamburger
    • Iframe
    • Pager
    • Settings
    • Document Validation
    • Navigation

      • Navigation
      • Navigation Menu
      • Navigation Footer
      • Navigation Button
      • Navigation Button Item
      • Navigation Search Item

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.

Drag & Drop

Upload .xml with a maximum size of 10MB

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>
Last Updated:: 12/2/24, 4:46 PM
Contributors: Antony Elfferich
Prev
Settings