department-of-veterans-affairs/vets-website

View on GitHub
src/platform/pdf/templates/medical_records.js

Summary

Maintainability
D
2 days
Test Coverage
/**
 * Lab and Test Results PDF template.
 *
 * NB: The order in which items are added to the document is important,
 * and thus PDFKit requires performing operations synchronously.
 */
/* eslint-disable no-await-in-loop */

import { MissingFieldsException } from '../utils/exceptions/MissingFieldsException';

import {
  createAccessibleDoc,
  addHorizontalRule,
  createDetailItem,
  createHeading,
  createSubHeading,
  getTestResultBlockHeight,
  registerVaGovFonts,
  generateInitialHeaderContent,
  generateFinalHeaderContent,
  generateFooterContent,
} from './utils';

const defaultConfig = {
  margins: {
    top: 40,
    bottom: 40,
    left: 20,
    right: 20,
  },
  headings: {
    H1: {
      font: 'Bitter-Bold',
      size: 24,
    },
    H2: {
      font: 'Bitter-Bold',
      size: 18,
    },
    H3: {
      font: 'Bitter-Bold',
      size: 16,
    },
  },
  subHeading: {
    font: 'Bitter-Regular',
    size: 12,
  },
  text: {
    boldFont: 'SourceSansPro-Bold',
    monospaceFont: 'RobotoMono-Regular',
    font: 'SourceSansPro-Regular',
    size: 12,
  },
};

const generateIntroductionContent = async (doc, parent, data, config) => {
  const headOptions = { x: 20, paragraphGap: 5 };
  const subHeadOptions = { paragraphGap: 0 };
  const introduction = doc.struct('Sect', {
    title: 'Introduction',
  });
  parent.add(introduction);
  introduction.add(createHeading(doc, 'H1', config, data.title, headOptions));
  if (data.preface) {
    introduction.add(
      createSubHeading(doc, config, data.preface, subHeadOptions),
    );
  }
  doc.moveDown();
  introduction.end();
};

const generateDetailsContent = async (doc, parent, data, config) => {
  const details = doc.struct('Sect', {
    title: 'Details',
  });
  parent.add(details);
  if (data.details.header) {
    const headOptions = { x: 20, paragraphGap: 12 };
    details.add(
      createHeading(doc, 'H2', config, data.details.header, headOptions),
    );
  }
  const itemIndent = data.details.header ? 30 : 20;
  for (const item of data.details.items) {
    const structs = await createDetailItem(doc, config, itemIndent, item);
    for (const struct of structs) {
      details.add(struct);
    }
  }
  doc.moveDown();
  details.end();
};

const generateResultItemContent = async (
  item,
  doc,
  results,
  hasHorizontalRule,
  config,
) => {
  const headingOptions = { paragraphGap: 10, x: 30 };
  if (item.header) {
    results.add(
      await createHeading(doc, 'H3', config, item.header, headingOptions),
    );
  }

  for (const resultItem of item.items) {
    const structs = await createDetailItem(doc, config, 40, resultItem);
    for (const struct of structs) {
      results.add(struct);
    }
  }

  if (hasHorizontalRule) {
    addHorizontalRule(doc, 30, 1.5, 1.5);
  }
};

const generateResultsContent = async (doc, parent, data, config) => {
  const results = doc.struct('Sect', {
    title: 'Results',
  });
  parent.add(results);
  if (data.results.header) {
    const headingOptions = { paragraphGap: 12, x: 20 };
    results.add(
      createHeading(doc, 'H2', config, data.results.header, headingOptions),
    );
  }

  if (data.results.preface) {
    const prefaceOptions = { paragraphGap: 12, x: 20 };
    results.add(
      createSubHeading(doc, config, data.results.preface, prefaceOptions),
    );
  }

  const hasHorizontalRule = data.results.sectionSeparators !== false;
  if (data.results.items.length === 1) {
    await generateResultItemContent(
      data.results.items[0],
      doc,
      results,
      hasHorizontalRule,
      config,
    );
  } else {
    for (const item of data.results.items) {
      // Insert a pagebreak if the next block will not fit on the current page,
      // taking the footer height into account.
      const blockHeight = getTestResultBlockHeight(
        doc,
        item,
        hasHorizontalRule,
      );
      if (doc.y + blockHeight > 740) await doc.addPage();

      await generateResultItemContent(
        item,
        doc,
        results,
        hasHorizontalRule,
        config,
      );
    }
  }
  results.end();
};

const validate = data => {
  const requiredFields = [
    'title',
    'headerLeft',
    'headerRight',
    'footerLeft',
    'footerRight',
  ];

  const missingFields = requiredFields.filter(field => !data[field]);
  if (missingFields.length) {
    throw new MissingFieldsException(missingFields);
  }
};

const generate = async (data, config = defaultConfig) => {
  validate(data);

  const doc = createAccessibleDoc(data, config);

  await registerVaGovFonts(doc);

  doc.addPage({ margins: config.margins });

  const wrapper = doc.struct('Document');
  doc.addStructure(wrapper);

  // Add content synchronously to ensure that reading order
  // is left intact for screen reader users.

  await generateInitialHeaderContent(doc, wrapper, data, config);

  await generateIntroductionContent(doc, wrapper, data, config);

  if (data.details) {
    await generateDetailsContent(doc, wrapper, data, config);
  }

  if (data.results) {
    await generateResultsContent(doc, wrapper, data, config);
  }

  await generateFinalHeaderContent(doc, data, config);
  await generateFooterContent(doc, wrapper, data, config);

  wrapper.end();

  doc.flushPages();
  return doc;
};

export { generate };