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 8 commits
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
109 changes: 23 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 @@ -11,6 +11,7 @@ import {
Location,
Medication,
MedicationStatement,
MedicationAdministration,
Observation,
Organization,
Patient,
Expand All @@ -36,33 +37,19 @@ 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" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="https://www.w3.org/1999/xhtml" xml:lang="en">
Expand Down Expand Up @@ -233,27 +220,10 @@ 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 All @@ -272,6 +242,7 @@ function extractFhirTypesFromBundle(bundle: Bundle): {
practitioners: Practitioner[];
medications: Medication[];
medicationStatements: MedicationStatement[];
medicationAdministrations: MedicationAdministration[];
conditions: Condition[];
allergies: AllergyIntolerance[];
locations: Location[];
Expand Down Expand Up @@ -308,6 +279,7 @@ function extractFhirTypesFromBundle(bundle: Bundle): {
const tasks: Task[] = [];
const coverages: Coverage[] = [];
const organizations: Organization[] = [];
const medicationAdministrations: MedicationAdministration[] = [];

if (bundle.entry) {
for (const entry of bundle.entry) {
Expand All @@ -316,6 +288,8 @@ function extractFhirTypesFromBundle(bundle: Bundle): {
patient = resource as Patient;
} else if (resource?.resourceType === "MedicationStatement") {
medicationStatements.push(resource as MedicationStatement);
} else if (resource?.resourceType === "MedicationAdministration") {
medicationAdministrations.push(resource as MedicationAdministration);
} else if (resource?.resourceType === "Medication") {
medications.push(resource as Medication);
} else if (resource?.resourceType === "Condition") {
Expand Down Expand Up @@ -376,6 +350,7 @@ function extractFhirTypesFromBundle(bundle: Bundle): {
diagnosticReports,
medications,
medicationStatements,
medicationAdministrations,
conditions,
allergies,
locations,
Expand Down Expand Up @@ -435,12 +410,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 +424,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 +458,7 @@ type EncounterSection = {
};
};

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

function createDiagnosticReportsSection(
export function createDiagnosticReportsSection(
diagnosticReports: DiagnosticReport[],
practitioners: Practitioner[],
aweVisits: Condition[],
Expand Down Expand Up @@ -969,9 +906,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 +1223,7 @@ type RenderObservation = {
lastDate: string;
};

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

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

function createObservationLaboratorySection(observations: Observation[]) {
export function createObservationLaboratorySection(observations: Observation[]) {
if (!observations) {
return "";
}
Expand Down Expand Up @@ -1607,7 +1544,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 +1665,7 @@ function renderClassDisplay(encounter: Encounter) {
}
}

function createImmunizationSection(immunizations: Immunization[]) {
export function createImmunizationSection(immunizations: Immunization[]) {
if (!immunizations) {
return "";
}
Expand Down Expand Up @@ -1787,7 +1724,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 +1804,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 +1874,7 @@ function renderRelatedPersonAddresses(relatedPerson: RelatedPerson) {
});
}

function createTaskSection(tasks: Task[]) {
export function createTaskSection(tasks: Task[]) {
if (!tasks) {
return "";
}
Expand Down Expand Up @@ -1999,7 +1936,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 +1998,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 +2132,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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{{>Resources/Medication.hbs medication=medAdm.substanceAdministration.consumable.manufacturedProduct.manufacturedMaterial ID=(generateUUID (toJsonString medAdm.substanceAdministration.consumable.manufacturedProduct.manufacturedMaterial))}},
{{>References/MedicationAdministration/medicationReference.hbs ID=(generateUUID (toJsonString medAdm.substanceAdministration)) REF=(concat 'Medication/' (generateUUID (toJsonString medAdm.substanceAdministration.consumable.manufacturedProduct.manufacturedMaterial)))}},

{{#with (evaluate 'Utils/GenerateOrganizationId.hbs' obj=edAdm.substanceAdministration.performer.assignedEntity.representedOrganization) as |orgId|}}
{{#with (evaluate 'Utils/GenerateOrganizationId.hbs' obj=medAdm.substanceAdministration.performer.assignedEntity.representedOrganization) as |orgId|}}
Copy link
Member Author

Choose a reason for hiding this comment

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

bug

Copy link
Member

Choose a reason for hiding this comment

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

Ouch, this is stuff that our tooling should catch for us. Isn't there a way we can parse/compile those? At least to check if the variable names are correct and not misspelled?

Also, is there a way we can have unit testing for this? We can def create a ticket to set it up.

Just because w/ unit tests we can also create new tests as we find bugs that would catch those if they were in place.

{{>Resources/Organization.hbs org=medAdm.substanceAdministration.performer.assignedEntity.representedOrganization ID=orgId.Id}},
{{>References/MedicationAdministration/performer.actor.hbs ID=(generateUUID (toJsonString medAdm.substanceAdministration)) REF=(concat 'Organization/' orgId.Id)}},
{{/with}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
{{/with}}
{{/if}}
{{/if}}

{{/each}}
{{/if}}

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();
}
42 changes: 42 additions & 0 deletions packages/utils/src/customer-requests/medical-records-and-csvs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { bundleToHtml } from "@metriport/core/external/aws/lambda-logic/bundle-to-html";
import fs from "fs";
import { convertHtmlTablesToCsv } from "./convert-html-to-csv";
import * as path from "path";

// Function to process each JSON file
function processFile(filePath: string) {
const bundle = fs.readFileSync(filePath, "utf8");
const bundleParsed = JSON.parse(bundle);

// Convert JSON to HTML
const html = bundleToHtml(bundleParsed);
const htmlOutputFilePath = filePath.replace(".json", ".html");
fs.writeFileSync(htmlOutputFilePath, html);
console.log(`HTML file created at ${htmlOutputFilePath}`);

// Convert HTML to CSV
const csvContent = convertHtmlTablesToCsv(html);
const csvOutputFilePath = filePath.replace(".json", ".csv");
fs.writeFileSync(csvOutputFilePath, csvContent);
console.log(`CSV file created at ${csvOutputFilePath}`);
}

// Main function to iterate through the directory
function main() {
const targetPath = process.argv[2];

if (!targetPath) {
console.log("Usage: node medical-records-local.js <path-to-directory>");
process.exit(1);
}

const files = fs.readdirSync(targetPath);
files.forEach(file => {
const fullPath = path.join(targetPath, file);
if (path.extname(fullPath) === ".json") {
processFile(fullPath);
}
});
}

main();
Loading
Loading