From d2a8bfd7c5713c12fa5ce390bfe7e01ca41ba112 Mon Sep 17 00:00:00 2001 From: Amy Chen Date: Wed, 5 Jun 2024 10:31:27 -0700 Subject: [PATCH] refactor code, update README --- README.md | 2 +- src/components/sections/MedicalHistory.js | 2 +- src/config/sections_config.js | 2 +- src/context/QuestionnaireListProvider.js | 2 +- src/hooks/useFetchResources.js | 6 +- src/models/Observation.js | 6 +- src/util/fhirUtil.js | 226 +++++++++++++++++++ src/util/util.js | 259 ---------------------- 8 files changed, 238 insertions(+), 267 deletions(-) create mode 100644 src/util/fhirUtil.js diff --git a/README.md b/README.md index d03103f2..8bbf61c2 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Parameters for the app are stored in [environmental variables](http://man7.org/l | `REACT_APP_DISABLE_HEADER` | Define whether the header element should appear, default is `false` |`true`, `false` | `REACT_APP_DISABLE_NAV` | Define whether the navigation menu element should appear, default is `false` |`true`, `false` | `REACT_APP_MATOMO_SITE_ID` | Define the site id used to for tracking via MATOMO | Example: `24` - +| `REACT_APP_EPIC_QUERIES` | Define whether the site is quering data from EPIC | example: `true`, default is `false` | [`true`, `false`] ### Using with Public SMART Sandbox A public [SMART® App Launcher](https://launch.smarthealthit.org/index.html) is available for sandbox tesing of SMART on FHIR apps with synthetic data. diff --git a/src/components/sections/MedicalHistory.js b/src/components/sections/MedicalHistory.js index 5a9450b2..947338d1 100644 --- a/src/components/sections/MedicalHistory.js +++ b/src/components/sections/MedicalHistory.js @@ -26,7 +26,7 @@ export default function MedicalHistory(props) { item.condition = o.displayText || "--"; item.onsetDateTime = o.onsetDateTimeDisplayText; item.recordedDate = o.recordedDateTimeDisplayText; - item.status = o.status || "--"; + item.status = o.status ? String(o.status).toLowerCase() : "--"; return item; }) .sort((a, b) => { diff --git a/src/config/sections_config.js b/src/config/sections_config.js index 63684562..f974b480 100644 --- a/src/config/sections_config.js +++ b/src/config/sections_config.js @@ -6,7 +6,7 @@ import SummarizeIcon from "@mui/icons-material/Summarize"; import Box from "@mui/material/Box"; import CircularProgress from "@mui/material/CircularProgress"; import Stack from "@mui/material/Stack"; -import { getResourcesByResourceType } from "../util/util"; +import { getResourcesByResourceType } from "../util/fhirUtil"; const renderLoader = () => ( { + if (section.resources && section.resources.length) { + resourcesForSection = [...resourcesForSection, ...section.resources]; + } + }); + } + const combinedResources = [...envResourcesToLoad, ...resourcesForSection]; + const allResources = [...new Set(combinedResources)]; + const resources = allResources.length + ? [...new Set([...allResources, ...defaultList])] + : defaultList; + + //console.log("Resources to load : ", resources); + return resources; +} + +export function getFHIRResourceQueryParams(resourceType, options) { + if (!resourceType) return null; + let paramsObj = { + _sort: "-_lastUpdated", + _count: 200, + }; + const queryOptions = options ? options : {}; + switch (String(resourceType).toLowerCase()) { + case "careplan": + const envCategory = getEnv("REACT_APP_FHIR_CAREPLAN_CATEGORY"); + if (queryOptions.patientId) { + paramsObj["subject"] = `Patient/${queryOptions.patientId}`; + } + if (envCategory) { + paramsObj["category:text"] = envCategory; + } + break; + case "questionnaire": + if ( + queryOptions.questionnaireList && + Array.isArray(queryOptions.questionnaireList) && + queryOptions.questionnaireList.length + ) { + paramsObj[queryOptions.exactMatch ? "_id" : "name:contains"] = + queryOptions.questionnaireList.join(","); + } + break; + case "observation": + const envObCategories = getEnv("REACT_APP_FHIR_OBSERVATION_CATEGORIES"); + const observationCategories = envObCategories + ? envObCategories + : DEFAULT_OBSERVATION_CATEGORIES; + paramsObj["category"] = observationCategories; + if (queryOptions.patientId) { + paramsObj["patient"] = `Patient/${queryOptions.patientId}`; + } + break; + default: + if (queryOptions.patientId) { + paramsObj["patient"] = `Patient/${queryOptions.patientId}`; + } + } + return paramsObj; +} + +export function getFHIRResourcePaths(patientId, resourcesToLoad, options) { + if (!patientId) return []; + const resources = + resourcesToLoad && Array.isArray(resourcesToLoad) && resourcesToLoad.length + ? resourcesToLoad + : getFHIRResourcesToLoad(); + return resources.map((resource) => { + let path = `/${resource}`; + const paramsObj = getFHIRResourceQueryParams(resource, { + ...(options ? options : {}), + patientId: patientId, + }); + if (paramsObj) { + const searchParams = new URLSearchParams(paramsObj); + path = path + "?" + searchParams.toString(); + } + return { + resourceType: resource, + resourcePath: path, + }; + }); +} + +export function getResourcesByResourceType(patientBundle, resourceType) { + if (!patientBundle || !Array.isArray(patientBundle) || !patientBundle.length) + return null; + return patientBundle + .filter((item) => { + return ( + item.resource && + String(item.resource.resourceType).toLowerCase() === + String(resourceType).toLowerCase() + ); + }) + .map((item) => item.resource); +} + +export function getQuestionnairesByCarePlan(arrCarePlans) { + if (!arrCarePlans) return []; + let activities = []; + arrCarePlans.forEach((item) => { + if (item.resource.activity) { + activities = [...activities, ...item.resource.activity]; + } + }); + let qList = []; + activities.forEach((a) => { + if ( + a.detail && + a.detail.instantiatesCanonical && + a.detail.instantiatesCanonical.length + ) { + const qId = a.detail.instantiatesCanonical[0].split("/")[1]; + if (qId && qList.indexOf(qId) === -1) qList.push(qId); + } + }); + return qList; +} + +export function getFhirResourcesFromQueryResult(result) { + let bundle = []; + if (!result) return []; + if (result.resourceType === "Bundle" && result.entry) { + result.entry.forEach((o) => { + if (o && o.resource) bundle.push({ resource: o.resource }); + }); + } else if (Array.isArray(result)) { + result.forEach((o) => { + if (o.resourceType) bundle.push({ resource: o }); + }); + } else { + bundle.push({ resource: result }); + } + return bundle; +} + +export function getFhirItemValue(item) { + if (!item) return null; + if (hasValue(item.valueQuantity)) { + if (item.valueQuantity.unit) { + return [item.valueQuantity.value, item.valueQuantity.unit].join(" "); + } + return item.valueQuantity.value; + } + if (hasValue(item.valueString)) { + if (hasValue(item.valueString.value)) return String(item.valueString.value); + return item.valueString; + } + if (hasValue(item.valueBoolean)) { + if (hasValue(item.valueBoolean.value)) + return String(item.valueBoolean.value); + return String(item.valueBoolean); + } + if (hasValue(item.valueInteger)) { + if (hasValue(item.valueInteger.value)) return item.valueInteger.value; + return item.valueInteger; + } + if (hasValue(item.valueDecimal)) { + if (hasValue(item.valueDecimal.value)) return item.valueDecimal.value; + return item.valueDecimal; + } + if (item.valueDate) { + if (hasValue(item.valueDate.value)) return item.valueDate.value; + return item.valueDate; + } + if (hasValue(item.valueDateTime)) { + if (item.valueDateTime.value) return item.valueDateTime.value; + return item.valueDateTime; + } + if (item.valueCodeableConcept) { + if (item.valueCodeableConcept.text) { + return item.valueCodeableConcept.text; + } else if ( + item.valueCodeableConcept.coding && + Array.isArray(item.valueCodeableConcept.coding) && + item.valueCodeableConcept.coding.length + ) { + return item.valueCodeableConcept.coding[0].display; + } + return null; + } + // need to handle date/time value + + return null; +} +export function getFhirComponentDisplays(item) { + let displayText = getFhirItemValue(item); + if (!item || !item.component || !item.component.length) return displayText; + const componentDisplay = item.component + .map((o) => { + const textDisplay = o.code && o.code.text ? o.code.text : null; + const valueDisplay = getFhirItemValue(o); + if (hasValue(valueDisplay)) + return textDisplay + ? [textDisplay, valueDisplay].join(": ") + : valueDisplay; + return ""; + }) + .join(", "); + if (displayText && componentDisplay) { + return [displayText, componentDisplay].join(", "); + } + if (componentDisplay) return componentDisplay; + if (displayText) return displayText; + return null; +} diff --git a/src/util/util.js b/src/util/util.js index 37b3b450..cddb1130 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -8,7 +8,6 @@ import commonLibrary from "../cql/InterventionLogic_Common.json"; import valueSetJson from "../cql/valueset-db.json"; import defaultSections from "../config/sections_config.js"; import { - DEFAULT_OBSERVATION_CATEGORIES, DEFAULT_TOOLBAR_HEIGHT, } from "../consts/consts"; @@ -42,155 +41,6 @@ export async function getInterventionLogicLib(interventionId) { return [elmJson, valueSetJson]; } -export function getFHIRResourcesToLoad() { - const defaultList = [ - "Condition", - "Observation", - "Questionnaire", - "QuestionnaireResponse", - ]; - const resourcesToLoad = getEnv("REACT_APP_FHIR_RESOURCES"); - const sections = getSectionsToShow(); - const envResourcesToLoad = resourcesToLoad ? resourcesToLoad.split(",") : []; - let resourcesForSection = []; - if (sections && sections.length) { - sections.forEach((section) => { - if (section.resources && section.resources.length) { - resourcesForSection = [...resourcesForSection, ...section.resources]; - } - }); - } - const combinedResources = [...envResourcesToLoad, ...resourcesForSection]; - const allResources = [...new Set(combinedResources)]; - const resources = allResources.length - ? [...new Set([...allResources, ...defaultList])] - : defaultList; - - //console.log("Resources to load : ", resources); - return resources; -} - -export function getFHIRResourceQueryParams(resourceType, options) { - if (!resourceType) return null; - let paramsObj = { - _sort: "-_lastUpdated", - _count: 200, - }; - const queryOptions = options ? options : {}; - switch (String(resourceType).toLowerCase()) { - case "careplan": - const envCategory = getEnv("REACT_APP_FHIR_CAREPLAN_CATEGORY"); - if (queryOptions.patientId) { - paramsObj["subject"] = `Patient/${queryOptions.patientId}`; - } - if (envCategory) { - paramsObj["category:text"] = envCategory; - } - break; - case "questionnaire": - if ( - queryOptions.questionnaireList && - Array.isArray(queryOptions.questionnaireList) && - queryOptions.questionnaireList.length - ) { - paramsObj[queryOptions.exactMatch ? "_id" : "name:contains"] = - queryOptions.questionnaireList.join(","); - } - break; - case "observation": - const envObCategories = getEnv("REACT_APP_FHIR_OBSERVATION_CATEGORIES"); - const observationCategories = envObCategories - ? envObCategories - : DEFAULT_OBSERVATION_CATEGORIES; - paramsObj["category"] = observationCategories; - if (queryOptions.patientId) { - paramsObj["patient"] = `Patient/${queryOptions.patientId}`; - } - break; - default: - if (queryOptions.patientId) { - paramsObj["patient"] = `Patient/${queryOptions.patientId}`; - } - } - return paramsObj; -} - -export function getFHIRResourcePaths(patientId, resourcesToLoad, options) { - if (!patientId) return []; - const resources = - resourcesToLoad && Array.isArray(resourcesToLoad) && resourcesToLoad.length - ? resourcesToLoad - : getFHIRResourcesToLoad(); - return resources.map((resource) => { - let path = `/${resource}`; - const paramsObj = getFHIRResourceQueryParams(resource, { - ...(options ? options : {}), - patientId: patientId, - }); - if (paramsObj) { - const searchParams = new URLSearchParams(paramsObj); - path = path + "?" + searchParams.toString(); - } - return { - resourceType: resource, - resourcePath: path, - }; - }); -} - -export function getResourcesByResourceType(patientBundle, resourceType) { - if (!patientBundle || !Array.isArray(patientBundle) || !patientBundle.length) - return null; - return patientBundle - .filter((item) => { - return ( - item.resource && - String(item.resource.resourceType).toLowerCase() === - String(resourceType).toLowerCase() - ); - }) - .map((item) => item.resource); -} - -export function getQuestionnairesByCarePlan(arrCarePlans) { - if (!arrCarePlans) return []; - let activities = []; - arrCarePlans.forEach((item) => { - if (item.resource.activity) { - activities = [...activities, ...item.resource.activity]; - } - }); - let qList = []; - activities.forEach((a) => { - if ( - a.detail && - a.detail.instantiatesCanonical && - a.detail.instantiatesCanonical.length - ) { - const qId = a.detail.instantiatesCanonical[0].split("/")[1]; - if (qId && qList.indexOf(qId) === -1) qList.push(qId); - } - }); - return qList; -} - -export function getFhirResourcesFromQueryResult(result) { - let bundle = []; - if (!result) return []; - if (result.resourceType === "Bundle" && result.entry) { - result.entry.forEach((o) => { - if (o && o.resource) bundle.push({ resource: o.resource }); - }); - } else if (Array.isArray(result)) { - result.forEach((o) => { - if (o.resourceType) bundle.push({ resource: o }); - }); - } else { - bundle.push({ resource: result }); - } - return bundle; -} - export function getDisplayQTitle(questionnaireId) { if (!questionnaireId) return ""; return String(questionnaireId.replace(/cirg-/gi, "")).toUpperCase(); @@ -409,36 +259,6 @@ export function getEnvDashboardURL() { return getEnv("REACT_APP_DASHBOARD_URL"); } -// export function getIntroTextFromQuestionnaire(questionnaireJson) { -// if (!questionnaireJson) return ""; -// const commonmark = require("commonmark"); -// const reader = new commonmark.Parser({ smart: true }); -// const writer = new commonmark.HtmlRenderer({ -// linebreak: "
", -// softbreak: "
", -// }); -// let description = ""; -// if (questionnaireJson.description) { -// const parsedObj = reader.parse(questionnaireJson.description); -// description = questionnaireJson.description -// ? writer.render(parsedObj) -// : ""; -// } -// if (description) return description; -// const introductionItem = questionnaireJson.item -// ? questionnaireJson.item.find( -// (item) => String(item.linkId).toLowerCase() === "introduction" -// ) -// : null; -// if (introductionItem) { -// const textElement = introductionItem._text; -// if (!textElement || !textElement.extension || !textElement.extension[0]) -// return ""; -// return textElement.extension[0].valueString; -// } -// return ""; -// } - export function isNumber(target) { if (isNaN(target)) return false; if (typeof target === "number") return true; @@ -511,14 +331,6 @@ export function addMamotoTracking(userId) { headElement.appendChild(g); } -// export function getQuestionnaireName(questionnaireJson) { -// if (!questionnaireJson) return ""; -// const { id, title, name } = questionnaireJson; -// if (name) return name; -// if (title) return title; -// return `Questionnaire ${id}`; -// } - export function getLocaleDateStringFromDate(dateString, format) { if (!dateString) return ""; const dateFormat = format ? format : "YYYY-MM-DD hh:mm"; @@ -526,77 +338,6 @@ export function getLocaleDateStringFromDate(dateString, format) { return dayjs(dateString).format(dateFormat); } -export function getFhirItemValue(item) { - if (!item) return null; - if (hasValue(item.valueQuantity)) { - if (item.valueQuantity.unit) { - return [item.valueQuantity.value, item.valueQuantity.unit].join(" "); - } - return item.valueQuantity.value; - } - if (hasValue(item.valueString)) { - if (hasValue(item.valueString.value)) return String(item.valueString.value); - return item.valueString; - } - if (hasValue(item.valueBoolean)) { - if (hasValue(item.valueBoolean.value)) - return String(item.valueBoolean.value); - return String(item.valueBoolean); - } - if (hasValue(item.valueInteger)) { - if (hasValue(item.valueInteger.value)) return item.valueInteger.value; - return item.valueInteger; - } - if (hasValue(item.valueDecimal)) { - if (hasValue(item.valueDecimal.value)) return item.valueDecimal.value; - return item.valueDecimal; - } - if (item.valueDate) { - if (hasValue(item.valueDate.value)) return item.valueDate.value; - return item.valueDate; - } - if (hasValue(item.valueDateTime)) { - if (item.valueDateTime.value) return item.valueDateTime.value; - return item.valueDateTime; - } - if (item.valueCodeableConcept) { - if (item.valueCodeableConcept.text) { - return item.valueCodeableConcept.text; - } else if ( - item.valueCodeableConcept.coding && - Array.isArray(item.valueCodeableConcept.coding) && - item.valueCodeableConcept.coding.length - ) { - return item.valueCodeableConcept.coding[0].display; - } - return null; - } - // need to handle date/time value - - return null; -} -export function getFhirComponentDisplays(item) { - let displayText = getFhirItemValue(item); - if (!item || !item.component || !item.component.length) return displayText; - const componentDisplay = item.component - .map((o) => { - const textDisplay = o.code && o.code.text ? o.code.text : null; - const valueDisplay = getFhirItemValue(o); - if (hasValue(valueDisplay)) - return textDisplay - ? [textDisplay, valueDisplay].join(": ") - : valueDisplay; - return ""; - }) - .join(", "); - if (displayText && componentDisplay) { - return [displayText, componentDisplay].join(", "); - } - if (componentDisplay) return componentDisplay; - if (displayText) return displayText; - return null; -} - export function hasValue(value) { return value != null && value !== "" && typeof value !== "undefined"; }