Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SNOMED Hydrating, Filtering, Special MR Generation #1648

Draft
wants to merge 19 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(snomed): cx specific workflow
Refs: #1442
  • Loading branch information
jonahkaye committed Mar 6, 2024
commit 9dfda6ce3184a9fa596b347a4bcec8fcbb9abd2a
103 changes: 17 additions & 86 deletions packages/core/src/external/aws/lambda-logic/bundle-to-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,33 +36,18 @@ const CPT_CODE = "cpt";
export const bundleToHtml = (fhirBundle: Bundle): string => {
const {
patient,
practitioners,
diagnosticReports,
medications,
medicationStatements,
conditions,
allergies,
locations,
procedures,
observationOther,
observationSocialHistory,
observationVitals,
observationLaboratory,
encounters,
immunizations,
familyMemberHistories,
relatedPersons,
tasks,
coverages,
organizations,
} = extractFhirTypesFromBundle(fhirBundle);

if (!patient) {
throw new Error("No patient found in bundle");
}

const aweVisits = getAnnualWellnessVisits(conditions);

const htmlPage = `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http:https://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http:https://www.w3.org/1999/xhtml" xml:lang="en">
Expand Down Expand Up @@ -233,27 +218,11 @@ export const bundleToHtml = (fhirBundle: Bundle): string => {
${createMRHeader(patient)}
<div class="divider"></div>
<div id="mr-sections">
${createAWESection(diagnosticReports, practitioners, aweVisits, organizations)}
${createMedicationSection(medications, medicationStatements)}
${createDiagnosticReportsSection(
diagnosticReports,
practitioners,
aweVisits,
organizations
)}

${createConditionSection(conditions, encounters)}
${createAllergySection(allergies)}
${createProcedureSection(procedures)}
${createObservationSocialHistorySection(observationSocialHistory)}
${createObservationVitalsSection(observationVitals)}
${createObservationLaboratorySection(observationLaboratory)}
${createOtherObservationsSection(observationOther)}
${createImmunizationSection(immunizations)}
${createFamilyHistorySection(familyMemberHistories)}
${createRelatedPersonSection(relatedPersons)}
${createTaskSection(tasks)}
${createCoverageSection(coverages, organizations)}
${createEncountersSection(encounters, locations)}
</div>
</body>
</html>
Expand Down Expand Up @@ -435,12 +404,6 @@ function createMRHeader(patient: Patient) {
<h4>Table of Contents</h4>
<ul id="nav">
<div class='half'>
<li>
<a href="#awe">Annual Wellness Exams</a>
</li>
<li>
<a href="#reports">Reports</a>
</li>
<li>
<a href="#medications">Medications</a>
</li>
Expand All @@ -455,39 +418,7 @@ function createMRHeader(patient: Patient) {
>Procedures</a
>
</li>
<li>
<a href="#social-history">Social History</a>
</li>
<li>
<a href="#vitals">Vitals</a>
</li>
</div>
<div class='half'>
<li>
<a href="#laboratory">Laboratory</a>
</li>
<li>
<a href="#other-observations">Other Observations</a>
</li>
<li>
<a href="#immunizations">Immunizations</a>
</li>
<li>
<a href="#family-member-history">Family Member History</a>
</li>
<li>
<a href="#related-persons">Related Persons</a>
</li>
<li>
<a href="#tasks">Tasks</a>
</li>
<li>
<a href="#coverage">Coverage</a>
</li>
<li>
<a href="#encounters">Encounters</a>
</li>
</div>

</ul>
</div>
</div>
Expand Down Expand Up @@ -521,7 +452,7 @@ type EncounterSection = {
};
};

function createAWESection(
export function createAWESection(
diagnosticReports: DiagnosticReport[],
practitioners: Practitioner[],
aweVisits: Condition[],
Expand Down Expand Up @@ -563,7 +494,7 @@ function createAWESection(
`;
}

function createDiagnosticReportsSection(
export function createDiagnosticReportsSection(
diagnosticReports: DiagnosticReport[],
practitioners: Practitioner[],
aweVisits: Condition[],
Expand Down Expand Up @@ -969,9 +900,9 @@ function createConditionSection(conditions: Condition[], encounter: Encounter[])
return aDate === bDate && aText === bText;
})
.reduce((acc, condition) => {
const codeName = getSpecificCode(condition.code?.coding ?? [], [ICD_10_CODE, SNOMED_CODE]);
const codeName = getSpecificCode(condition.code?.coding ?? [], [SNOMED_CODE, ICD_10_CODE]);
const idc10Code = condition.code?.coding?.find(code =>
code.system?.toLowerCase().includes(ICD_10_CODE)
code.system?.toLowerCase().includes(SNOMED_CODE)
);

const name =
Expand Down Expand Up @@ -1286,7 +1217,7 @@ type RenderObservation = {
lastDate: string;
};

function createObservationSocialHistorySection(observations: Observation[]) {
export function createObservationSocialHistorySection(observations: Observation[]) {
if (!observations) {
return "";
}
Expand Down Expand Up @@ -1417,7 +1348,7 @@ function renderSocialHistoryValue(observation: Observation) {
}
}

function createObservationVitalsSection(observations: Observation[]) {
export function createObservationVitalsSection(observations: Observation[]) {
if (!observations) {
return "";
}
Expand Down Expand Up @@ -1501,7 +1432,7 @@ function renderVitalsValue(observation: Observation) {
}
}

function createObservationLaboratorySection(observations: Observation[]) {
export function createObservationLaboratorySection(observations: Observation[]) {
if (!observations) {
return "";
}
Expand Down Expand Up @@ -1607,7 +1538,7 @@ function createObservationsByDate(observations: Observation[]): string {
.join("");
}

function createOtherObservationsSection(observations: Observation[]) {
export function createOtherObservationsSection(observations: Observation[]) {
if (!observations) {
return "";
}
Expand Down Expand Up @@ -1728,7 +1659,7 @@ function renderClassDisplay(encounter: Encounter) {
}
}

function createImmunizationSection(immunizations: Immunization[]) {
export function createImmunizationSection(immunizations: Immunization[]) {
if (!immunizations) {
return "";
}
Expand Down Expand Up @@ -1787,7 +1718,7 @@ function createImmunizationSection(immunizations: Immunization[]) {
return createSection("Immunizations", immunizationTableContents);
}

function createFamilyHistorySection(familyMemberHistories: FamilyMemberHistory[]) {
export function createFamilyHistorySection(familyMemberHistories: FamilyMemberHistory[]) {
if (!familyMemberHistories) {
return "";
}
Expand Down Expand Up @@ -1867,7 +1798,7 @@ function renderAdministrativeGender(familyMemberHistory: FamilyMemberHistory): s
return null;
}

function createRelatedPersonSection(relatedPersons: RelatedPerson[]) {
export function createRelatedPersonSection(relatedPersons: RelatedPerson[]) {
if (!relatedPersons) {
return "";
}
Expand Down Expand Up @@ -1937,7 +1868,7 @@ function renderRelatedPersonAddresses(relatedPerson: RelatedPerson) {
});
}

function createTaskSection(tasks: Task[]) {
export function createTaskSection(tasks: Task[]) {
if (!tasks) {
return "";
}
Expand Down Expand Up @@ -1999,7 +1930,7 @@ function createTaskSection(tasks: Task[]) {
return createSection("Tasks", taskTableContents);
}

function createEncountersSection(encounters: Encounter[], locations: Location[]) {
export function createEncountersSection(encounters: Encounter[], locations: Location[]) {
const mappedLocations = mapResourceToId<Location>(locations);

if (!encounters) {
Expand Down Expand Up @@ -2061,7 +1992,7 @@ function createEncountersSection(encounters: Encounter[], locations: Location[])
return createSection("Encounters", encounterTableContents);
}

function createCoverageSection(coverages: Coverage[], organizations: Organization[]) {
export function createCoverageSection(coverages: Coverage[], organizations: Organization[]) {
if (!coverages) {
return "";
}
Expand Down Expand Up @@ -2195,7 +2126,7 @@ function mapResourceToId<ResourceType>(resources: Resource[]): Record<string, Re
}

// find condition with code Z00 in the past year
function getAnnualWellnessVisits(conditions: Condition[]) {
export function getAnnualWellnessVisits(conditions: Condition[]) {
const annualWellnessVisit = conditions.filter(condition => {
const code = getSpecificCode(condition.code?.coding ?? [], [ICD_10_CODE]);

Expand Down
35 changes: 34 additions & 1 deletion packages/utils/src/customer-requests/convert-html-to-csv.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add documentation explaining what it does and how to use it, please.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DOMParser } from "xmldom";

import { readFileSync, writeFileSync } from "fs";
import * as path from "path";
// This is temporary function that will eventually be introduced
// as another way to render patient data. (Will need refactoring)
export function convertHtmlTablesToCsv(html: string) {
Expand Down Expand Up @@ -110,3 +111,35 @@ export function convertHtmlTablesToCsv(html: string) {

return convertedCsv;
}

function main() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utility to call in the command line

const filePath = process.argv[2]; // Get the file path from command line arguments

if (!filePath) {
console.error("Please provide an HTML file path.");
process.exit(1);
}

try {
const htmlContent = readFileSync(filePath, "utf8"); // Read the HTML file content
const csvContent = convertHtmlTablesToCsv(htmlContent); // Convert HTML to CSV

// Construct the CSV file path by changing the extension
const csvFilePath = path.join(
path.dirname(filePath),
path.basename(filePath, path.extname(filePath)) + ".csv"
);

// Write the CSV content to the new file
writeFileSync(csvFilePath, csvContent);
console.log(`CSV file has been created at: ${csvFilePath}`);
// eslint-disable-next-line
} catch (error: any) {
console.error("Error processing the HTML file:", error.message);
}
}

// Call main if this script is run directly
if (require.main === module) {
main();
}
16 changes: 11 additions & 5 deletions packages/utils/src/customer-requests/medical-records-local.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add documentation explaining what it does and how to use it, please.

Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { bundleToHtml } from "@metriport/core/external/aws/lambda-logic/bundle-to-html";
import fs from "fs";

// get xml file from this folder and bundle to html
// Check if a file path is provided
if (process.argv.length < 3) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utility to call in command line

console.log("Usage: node medical-records-local.js <path-to-json-file>");
process.exit(1);
}

const bundle = fs.readFileSync("test-bundle.json", "utf8");
const filePath = process.argv[2];
const bundle = fs.readFileSync(filePath, "utf8");
const bundleParsed = JSON.parse(bundle);

// FHIR Bundle
const html = bundleToHtml(bundleParsed);

// Response from FHIR Converter
// const html = bundleToHtml(bundleParsed.fhirResource);
// Determine the output file name by replacing .json with .html
const outputFilePath = filePath.replace(".json", ".html");

fs.writeFileSync("test.html", html);
fs.writeFileSync(outputFilePath, html);
console.log(`HTML file created at ${outputFilePath}`);
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ async function main() {
}

await Promise.all(promises);

} catch (error) {
console.error("Error", error);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ dayjs.extend(duration);
* the ones seen in ./patients.
*/


const apiKey = getEnvVarOrFail("API_KEY");
const apiUrl = getEnvVarOrFail("API_URL");
const cxId = getEnvVarOrFail("CX_ID");
Expand Down Expand Up @@ -127,7 +126,9 @@ const validateDocQueryProgress = async (
console.log(
`The doc query was completed for all patients${
resetAndRunAgain ? " (resetAndRunAgain)" : ""
} - status were all queries successful: ${overallSuccess}. Errors: ${failedPatientQueries.length}`,
} - status were all queries successful: ${overallSuccess}. Errors: ${
failedPatientQueries.length
}`
);
};

Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add documentation explaining what it does and how to use it, please.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ import path from "path";
import { getCodeDetails } from "./term-server-api";

const processDirectory = async (directory: string) => {
// Check if the path is a directory or a file
const stat = fs.statSync(directory);
if (stat.isFile()) {
// If it's a file, process it directly
if (directory.endsWith(".json")) {
await processFile(directory);
}
return; // Exit the function as there's nothing more to do for a file
}

const items = fs.readdirSync(directory, { withFileTypes: true });

for (const item of items) {
Expand All @@ -11,7 +21,7 @@ const processDirectory = async (directory: string) => {
if (item.isDirectory()) {
// Recursively process the subdirectory
await processDirectory(sourcePath);
} else if (item.isFile() && item.name.endsWith(".xml.json")) {
} else if (item.isFile() && item.name.endsWith(".json")) {
await processFile(sourcePath);
}
}
Expand All @@ -22,8 +32,10 @@ const processFile = async (filePath: string) => {
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
const encounteredCodes = new Set();

for (let i = data.entry.length - 1; i >= 0; i--) {
const entry = data.entry[i];
const entries = data.bundle ? data.bundle.entry : data.entry;

for (let i = entries.length - 1; i >= 0; i--) {
const entry = entries[i];
const resource = entry.resource;
if (resource && resource.resourceType === "Condition") {
const codings = resource.code?.coding || [];
Expand All @@ -32,7 +44,7 @@ const processFile = async (filePath: string) => {
if (coding.system === snomedSystemUrl) {
if (encounteredCodes.has(coding.code)) {
console.log(`Removing duplicate code ${coding.code} from ${path.basename(filePath)}`);
data.entry.splice(i, 1);
entries.splice(i, 1);
} else {
encounteredCodes.add(coding.code);
const codeDetails = await getCodeDetails(coding.code, "SNOMEDCT_US");
Expand Down
Loading