department-of-veterans-affairs/vets-website

View on GitHub
src/applications/mhv-medical-records/reducers/careSummariesAndNotes.js

Summary

Maintainability
D
2 days
Test Coverage
import { formatDateLong } from '@department-of-veterans-affairs/platform-utilities/exports';
import { Actions } from '../util/actionTypes';
import {
  EMPTY_FIELD,
  loincCodes,
  noteTypes,
  loadStates,
  dischargeSummarySortFields,
} from '../util/constants';
import {
  extractContainedResource,
  isArrayAndHasItems,
  decodeBase64Report,
  formatNameFirstToLast,
} from '../util/helpers';

const initialState = {
  /**
   * The last time that the list was fetched and known to be up-to-date
   * @type {Date}
   */
  listCurrentAsOf: undefined,
  /**
   * PRE_FETCH, FETCHING, FETCHED
   */
  listState: loadStates.PRE_FETCH,

  /**
   * The list of care summaries and notes returned from the api
   * @type {Array}
   */
  careSummariesAndNotesList: undefined,
  /**
   * New list of records retrieved. This list is NOT displayed. It must manually be copied into the display list.
   * @type {Array}
   */
  updatedList: undefined,
  /**
   * The care summaries and notes currently being displayed to the user
   */
  careSummariesAndNotesDetails: undefined,
};

export const getTitle = record => {
  const contentItem = record.content?.find(item => item.attachment);
  if (contentItem?.attachment?.title) {
    return contentItem.attachment.title;
  }
  if (isArrayAndHasItems(record.type?.coding)) {
    const codingItem = record.type.coding.find(item => item.display);
    return codingItem?.display ?? null;
  }
  return null;
};

export const getType = record => {
  if (isArrayAndHasItems(record.type?.coding)) {
    const codingItem = record.type?.coding.find(item => item.code);
    return codingItem?.code ?? null;
  }
  return null;
};

export const extractAuthenticator = record => {
  const authenticator = extractContainedResource(
    record,
    record.authenticator?.reference,
  );
  const name = authenticator?.name?.find(item => item.text);
  return formatNameFirstToLast(name) ?? null;
};

export const extractAuthor = record => {
  if (isArrayAndHasItems(record.author)) {
    const authorRef = record.author.find(item => item.reference);
    const author = extractContainedResource(record, authorRef?.reference);
    const name = author?.name?.find(item => item.text);
    return formatNameFirstToLast(name) ?? null;
  }
  return null;
};

export const extractLocation = record => {
  if (isArrayAndHasItems(record?.context?.related)) {
    const reference = record.context?.related?.find(item => item.reference)
      ?.reference;
    if (reference) {
      const resource = extractContainedResource(record, reference);
      return resource?.name ?? null;
    }
  }
  return null;
};

export const getNote = record => {
  if (isArrayAndHasItems(record.content)) {
    const contentItem = record.content.find(item => item.attachment);
    return decodeBase64Report(contentItem?.attachment?.data);
  }
  return null;
};

export const getDateSigned = record => {
  if (isArrayAndHasItems(record.authenticator?.extension)) {
    const ext = record.authenticator.extension.find(e => e.valueDateTime);
    if (ext) {
      const formattedDate = formatDateLong(ext.valueDateTime);
      return formattedDate !== 'Invalid date' ? formattedDate : null;
    }
  }
  return null;
};

export const getAttending = noteSummary => {
  return (
    noteSummary
      ?.split('ATTENDING:')[1]
      ?.split('\n')[0]
      ?.trim() || null
  );
};

const isValidDate = d => {
  return d instanceof Date && !Number.isNaN(d.getTime());
};

export const getDateFromBody = (noteSummary, label) => {
  const dateStr =
    noteSummary
      ?.split(label)[1]
      ?.split('\n')[0]
      ?.trim() || null;
  const date = dateStr ? new Date(dateStr) : null;
  return isValidDate(date) ? date : null;
};

export const getAdmissionDate = (record, noteSummary) => {
  let admissionDate = record.context?.period?.start
    ? new Date(record.context.period.start)
    : null;
  if (!admissionDate) {
    admissionDate = getDateFromBody(noteSummary, 'DATE OF ADMISSION:');
  }
  return admissionDate;
};

export const getDischargeDate = (record, noteSummary) => {
  let dischargeDate = record.context?.period?.end
    ? new Date(record.context.period.end)
    : null;
  if (!dischargeDate) {
    dischargeDate = getDateFromBody(noteSummary, 'DATE OF DISCHARGE:');
  }
  return dischargeDate;
};

export const convertAdmissionAndDischargeDetails = record => {
  const summary = getNote(record);

  const admissionDate = getAdmissionDate(record, summary);
  const dischargeDate = getDischargeDate(record, summary);
  const dateEntered = record.date ? new Date(record.date) : null;

  const sortByDate = admissionDate || dischargeDate || dateEntered;
  let sortByField = null;
  if (admissionDate) {
    sortByField = dischargeSummarySortFields.ADMISSION_DATE;
  } else if (dischargeDate) {
    sortByField = dischargeSummarySortFields.DISCHARGE_DATE;
  } else if (dateEntered) {
    sortByField = dischargeSummarySortFields.DATE_ENTERED;
  }

  return {
    id: record.id,
    name: getTitle(record),
    type: getType(record),
    admissionDate: admissionDate ? formatDateLong(admissionDate) : EMPTY_FIELD,
    dischargeDate: dischargeDate ? formatDateLong(dischargeDate) : EMPTY_FIELD,
    dateEntered: dateEntered ? formatDateLong(dateEntered) : EMPTY_FIELD,
    admittedBy: getAttending(summary) || EMPTY_FIELD,
    dischargedBy: extractAuthor(record) || EMPTY_FIELD,
    location: extractLocation(record) || EMPTY_FIELD,
    summary: summary || EMPTY_FIELD,
    sortByDate,
    sortByField,
  };
};

const convertProgressNote = record => {
  return {
    id: record.id || null,
    name: getTitle(record) || EMPTY_FIELD,
    type: getType(record),
    date: record.date ? formatDateLong(record.date) : EMPTY_FIELD,
    dateSigned: getDateSigned(record) || EMPTY_FIELD,
    writtenBy: extractAuthor(record) || EMPTY_FIELD,
    signedBy: extractAuthenticator(record) || EMPTY_FIELD,
    location: extractLocation(record) || EMPTY_FIELD,
    note: getNote(record) || EMPTY_FIELD,
    sortByDate: record.date ? new Date(record.date) : null,
  };
};

/**
 * @param {Object} record - A FHIR DiagnosticReport or DocumentReference object
 * @returns the type of note/summary that was passed
 */
export const getRecordType = record => {
  const typeMapping = {
    [loincCodes.DISCHARGE_SUMMARY]: noteTypes.DISCHARGE_SUMMARY,
    [loincCodes.PHYSICIAN_PROCEDURE_NOTE]: noteTypes.PHYSICIAN_PROCEDURE_NOTE,
    [loincCodes.CONSULT_RESULT]: noteTypes.CONSULT_RESULT,
  };

  for (const [code, noteType] of Object.entries(typeMapping)) {
    if (record?.type?.coding.some(coding => coding.code === code)) {
      return noteType;
    }
  }

  return noteTypes.OTHER;
};

/**
 * Maps each record type to a converter function
 */
const notesAndSummariesConverterMap = {
  [noteTypes.DISCHARGE_SUMMARY]: convertAdmissionAndDischargeDetails,
  [noteTypes.PHYSICIAN_PROCEDURE_NOTE]: convertProgressNote,
  [noteTypes.CONSULT_RESULT]: convertProgressNote,
};

/**
 * @param {Object} record - A FHIR DocumentReference object
 * @returns the appropriate frontend object for display
 */
export const convertCareSummariesAndNotesRecord = record => {
  const type = getRecordType(record);
  const convertRecord = notesAndSummariesConverterMap[type];
  return convertRecord
    ? convertRecord(record)
    : { ...record, type: noteTypes.OTHER };
};

export const careSummariesAndNotesReducer = (state = initialState, action) => {
  switch (action.type) {
    case Actions.CareSummariesAndNotes.GET: {
      return {
        ...state,
        careSummariesAndNotesDetails: convertCareSummariesAndNotesRecord(
          action.response,
        ),
      };
    }
    case Actions.CareSummariesAndNotes.GET_FROM_LIST: {
      return {
        ...state,
        careSummariesAndNotesDetails: action.response,
      };
    }
    case Actions.CareSummariesAndNotes.GET_LIST: {
      const oldList = state.careSummariesAndNotesList;
      const newList =
        action.response.entry
          ?.map(note => {
            return convertCareSummariesAndNotesRecord(note.resource);
          })
          .filter(record => record.type !== noteTypes.OTHER)
          .sort((a, b) => {
            if (!a.sortByDate) return 1; // Push nulls to the end
            if (!b.sortByDate) return -1; // Keep non-nulls at the front
            return b.sortByDate.getTime() - a.sortByDate.getTime();
          }) || [];
      return {
        ...state,
        listCurrentAsOf: action.isCurrent ? new Date() : null,
        listState: loadStates.FETCHED,
        careSummariesAndNotesList:
          typeof oldList === 'undefined' ? newList : oldList,
        updatedList: typeof oldList !== 'undefined' ? newList : undefined,
      };
    }
    case Actions.CareSummariesAndNotes.COPY_UPDATED_LIST: {
      const originalList = state.careSummariesAndNotesList;
      const { updatedList } = state;
      if (
        Array.isArray(originalList) &&
        Array.isArray(updatedList) &&
        originalList.length !== updatedList.length
      ) {
        return {
          ...state,
          careSummariesAndNotesList: state.updatedList,
          updatedList: undefined,
        };
      }
      return {
        ...state,
      };
    }
    case Actions.CareSummariesAndNotes.CLEAR_DETAIL: {
      return {
        ...state,
        careSummariesAndNotesDetails: undefined,
      };
    }
    case Actions.CareSummariesAndNotes.UPDATE_LIST_STATE: {
      return {
        ...state,
        listState: action.payload,
      };
    }
    default:
      return state;
  }
};