diff --git a/public/assets/data/patientEducationReferences.json b/public/assets/data/patientEducationReferences.json index 3108bdb3c..813c4ce67 100644 --- a/public/assets/data/patientEducationReferences.json +++ b/public/assets/data/patientEducationReferences.json @@ -41,7 +41,7 @@ "link": { "title": "Naloxone Information Sheet", "className": "education", - "url": "{process.env.PUBLIC_URL}/assets/files/NaloxoneInformationSheet.pdf", + "url": "/assets/files/NaloxoneInformationSheet.pdf", "type": "PDF", "size": "142 KB" } diff --git a/public/index.html b/public/index.html index 0a545d929..738c15d79 100644 --- a/public/index.html +++ b/public/index.html @@ -1,19 +1,20 @@ - + - - - - - + + + + + Washington Opioid Clinical Assessment - +
diff --git a/src/components/CopyButton.jsx b/src/components/CopyButton.jsx index 8818b694a..2a5f71871 100644 --- a/src/components/CopyButton.jsx +++ b/src/components/CopyButton.jsx @@ -67,6 +67,7 @@ export default class CopyButton extends Component { "button", "button-primary", "button-secondary", + "pagination" ]; return !exclusionClasses.some((classname) => node.classList?.contains(classname) diff --git a/src/components/Disclaimer.jsx b/src/components/Disclaimer.jsx index 3f263275e..a9b34015b 100644 --- a/src/components/Disclaimer.jsx +++ b/src/components/Disclaimer.jsx @@ -50,17 +50,24 @@ export default class Disclaimer extends Component { [show/hide]
- COSRI incorporates the Clinical Pain Management Summary application, - released as open-source software by CDS Connect project at the - Agency for Healthcare Research and Quality (AHRQ). We have extended - ARHQ's work to provide enhanced security, improved decision support, - integration with state Prescription Drug Monitoring Program - databases, standalone operation, and other features. For a - description of our open source release, contact{" "} - info@cosri.app. Support for the - development of COSRI was provided by the Washington State Department - of Health and the Washington State Health Care Authority through the - CMS Support Act. +
+ COSRI incorporates the Clinical Pain Management Summary + application, released as open-source software by CDS Connect + project at the Agency for Healthcare Research and Quality (AHRQ). + We have extended ARHQ's work to provide enhanced security, + improved decision support, integration with state Prescription + Drug Monitoring Program databases, standalone operation, and other + features. For a description of our open source release, contact{" "} + info@cosri.app. Support for + the development of COSRI was provided by the Washington State + Department of Health and the Washington State Health Care + Authority through the CMS Support Act. Additional COSRI + implementation and evaluation funding provided by the Centers for + Disease Control and Prevention (CDC) and the Assistant Secretary + for Technology Policy (ASTP) through contract # + GS-35F-0034W/75P00122F80168, awarded to Security Risk Solutions, + Inc. +
diff --git a/src/components/InfoModal.jsx b/src/components/InfoModal.jsx index 3de6b336d..ef8e1141e 100644 --- a/src/components/InfoModal.jsx +++ b/src/components/InfoModal.jsx @@ -18,7 +18,7 @@ export default class InfoModal extends Component { { Header: () => Name, accessor: "name", - minWidth: 225, + minWidth: "45%", disableSortBy: true, }, { diff --git a/src/components/Landing/index.jsx b/src/components/Landing/index.jsx index 8cf6652ed..9135bc730 100644 --- a/src/components/Landing/index.jsx +++ b/src/components/Landing/index.jsx @@ -7,6 +7,7 @@ import executeElm from "../../utils/executeELM"; import * as landingUtils from "./utility"; import { datishFormat } from "../../helpers/formatit"; import { + addMatomoTracking, getEnvSystemType, getEPICPatientIdFromSource, getPatientNameFromSource, @@ -98,6 +99,8 @@ export default class Landing extends Component { }); return; } + // add PIWIK tracking + addMatomoTracking(); writeToLog("application loaded", "info", this.getPatientLogParams()); //set FHIR results let result = {}; diff --git a/src/components/Report/components/ScoringSummary.jsx b/src/components/Report/components/ScoringSummary.jsx index a405abbcb..6b5ca3d01 100644 --- a/src/components/Report/components/ScoringSummary.jsx +++ b/src/components/Report/components/ScoringSummary.jsx @@ -264,6 +264,13 @@ export default class ScoringSummary extends Component { ); } diff --git a/src/components/Report/index.jsx b/src/components/Report/index.jsx index 46568337b..94f15955e 100644 --- a/src/components/Report/index.jsx +++ b/src/components/Report/index.jsx @@ -131,7 +131,7 @@ export default class Report extends Component { const flagObj = this.getSectionFlags(section); if (isEmptyArray(flagObj)) return null; return ( -
+
{flagObj.map((o, index) => { return (
{headerGroup.headers.map((column, colIndex) => { - const headerProps = column.getHeaderProps(column.getSortByToggleProps()); - const cellMinSize = column.minWidth ? column.minWidth: "auto"; - const cellSize = column.size - ? `${column.size}` - : "160px"; + const headerProps = column.getHeaderProps( + column.getSortByToggleProps() + ); + const cellMinSize = column.minWidth ? column.minWidth : "auto"; + const cellSize = column.size ? `${column.size}` : "160px"; return (
{row.cells.map((cell, cellIndex) => { const cellProps = cell.getCellProps(); - const cellMinSize = cell.column.minWidth ? cell.column.minWidth: "auto"; + const cellMinSize = cell.column.minWidth + ? cell.column.minWidth + : "auto"; const cellSize = cell.column.size ? `${cell.column.size}` : "160px"; @@ -217,7 +224,7 @@ export default function Table({ tableKey, columns, data, tableParams, tableClass ...cellProps.style, flex: `${cellSize} 0 auto`, width: cellSize, - minWidth: cellMinSize + minWidth: cellMinSize, }} > {cell.render("Cell")} @@ -228,11 +235,41 @@ export default function Table({ tableKey, columns, data, tableParams, tableClass ); })} {emptyRows > 0 && ( - - + {/* + > */} + {page[0].cells.map((cell, cellIndex) => { + const cellProps = cell.getCellProps(); + const cellMinSize = cell.column.minWidth + ? cell.column.minWidth + : "auto"; + const cellSize = cell.column.size + ? `${cell.column.size}` + : "160px"; + return ( + +   + + ); + })} )} diff --git a/src/config/report_config.jsx b/src/config/report_config.jsx index 714d1ac15..0a8559861 100644 --- a/src/config/report_config.jsx +++ b/src/config/report_config.jsx @@ -60,6 +60,7 @@ const reportConfig = [ { id: "CIRG-PainTracker-GE", key: GE_DATA_KEY, + useDefaultELMLib: true }, ], //status: "inactive", @@ -380,6 +381,7 @@ const reportConfig = [ { id: "CIRG-PainTracker-TRT", key: TRT_DATA_KEY, + useDefaultELMLib: true }, ], icon: (props) => ( @@ -403,10 +405,12 @@ const reportConfig = [ Name: { key : "Name", sortable: true, - size: "100%", minWidth: "35%" }, - "Ordering Department": "Location", + "Ordering Department": { + key : "Location", + minWidth: "22%" + }, "CPT Code": "CPT_CODE", }, }} @@ -434,7 +438,10 @@ const reportConfig = [ size: "100%", minWidth: "35%" }, - "Ordering Department": "Location", + "Ordering Department": { + key : "Location", + minWidth: "22%" + }, "CPT Code": "CPT_CODE", }, }} diff --git a/src/config/summary_config.json b/src/config/summary_config.json index 576142263..627f265ef 100644 --- a/src/config/summary_config.json +++ b/src/config/summary_config.json @@ -250,10 +250,6 @@ }, "tables": [ { - "defaultSorted": { - "id": "Dispensed", - "desc": true - }, "headers": { "Drug Description": { "key": "Name", @@ -288,7 +284,7 @@ "formatter": "datishFormat", "sorter": "dateCompare", "sortable": true, - "size": "120px" + "size": "140px" }, "Dispensed": { "key": "DispensedDate", @@ -393,10 +389,34 @@ }, { "flag": { - "ifOneOrMore": { - "table": "MedicationRequestsForNaloxoneConsideration", - "source": "RiskConsiderations" - } + "ifAnd": [ + { + "ifEqualTo": { + "header": "Class", + "targetValue": "opioid" + } + }, + { + "ifGreaterThanOrEqualTo": { + "header": "MME", + "targetValue": 50 + } + }, + { + "ifEqualTo": { + "header": "isActive", + "type": "boolean", + "targetValue": true + } + }, + { + "ifEqualTo": { + "header": "isBup", + "type": "boolean", + "targetValue": false + } + } + ] }, "flagText": "Current MME 50 or more, consider prescribing Naloxone", "flagClass": "info" @@ -883,7 +903,7 @@ "Drug Description": { "key": "Name", "sortable": true, - "minWidth": "180px" + "minWidth": "220px" }, "Quantity": { "key": "Quantity", @@ -979,7 +999,7 @@ "Drug Description": { "key": "Name", "sortable": true, - "minWidth": "180px" + "minWidth": "220px" }, "Quantity": { "key": "Quantity", @@ -1074,7 +1094,7 @@ "Drug Description": { "key": "Name", "sortable": true, - "minWidth": "180px" + "minWidth": "220px" }, "Quantity": { "key": "Quantity", @@ -1337,7 +1357,7 @@ "headers": { "Name": { "key": "Name", - "minWidth": "150px" + "minWidth": "50%" }, "Date": { "key": "Date", diff --git a/src/cql/r4/Factors_to_Consider_in_Managing_Chronic_Pain_FHIRv401.cql b/src/cql/r4/Factors_to_Consider_in_Managing_Chronic_Pain_FHIRv401.cql index 0e2d1954d..22991b549 100644 --- a/src/cql/r4/Factors_to_Consider_in_Managing_Chronic_Pain_FHIRv401.cql +++ b/src/cql/r4/Factors_to_Consider_in_Managing_Chronic_Pain_FHIRv401.cql @@ -825,7 +825,8 @@ define ReportPDMPMedicationRequests: CalculatedEnd: GetMedicationRequestEndDate(O), dispenseRequest: O.dispenseRequest, MME_Object: MMECalculator.MME(O), - c: (O.medication as FHIR.CodeableConcept) + c: (O.medication as FHIR.CodeableConcept), + RxnormCode: Coalesce((c.coding) c2 where c2.system.value ~ 'http://www.nlm.nih.gov/research/umls/rxnorm' return c2.code.value) return { Type: 'Request', Name: ConceptText(c), @@ -843,7 +844,9 @@ define ReportPDMPMedicationRequests: Prescriber: O.requester.display.value, Pharmacy: GetMedicationRequestPharmacy(O), NDC_Code: Coalesce((c.coding) c2 where c2.system.value ~ 'http://hl7.org/fhir/sid/ndc' return c2.code.value), - RXNorm_Code: Coalesce((c.coding) c2 where c2.system.value ~ 'http://www.nlm.nih.gov/research/umls/rxnorm' return c2.code.value), + RXNorm_Code: RxnormCode, + isBup: RxnormCode in BuprenorphineRxCUIs, + isActive: CalculatedEnd same or after Today(), Status: O.status.value, Class: getMedicationRequestDrugClass(O), AuthoredOn: O.authoredOn diff --git a/src/cql/r4/Factors_to_Consider_in_Managing_Chronic_Pain_FHIRv401.json b/src/cql/r4/Factors_to_Consider_in_Managing_Chronic_Pain_FHIRv401.json index b73825f94..15d6c44e3 100644 --- a/src/cql/r4/Factors_to_Consider_in_Managing_Chronic_Pain_FHIRv401.json +++ b/src/cql/r4/Factors_to_Consider_in_Managing_Chronic_Pain_FHIRv401.json @@ -5627,6 +5627,53 @@ "type" : "NamedTypeSpecifier" } } + }, { + "identifier" : "RxnormCode", + "expression" : { + "type" : "Coalesce", + "operand" : [ { + "type" : "Query", + "source" : [ { + "alias" : "c2", + "expression" : { + "path" : "coding", + "type" : "Property", + "source" : { + "name" : "c", + "type" : "QueryLetRef" + } + } + } ], + "relationship" : [ ], + "where" : { + "type" : "Equivalent", + "operand" : [ { + "path" : "value", + "type" : "Property", + "source" : { + "path" : "system", + "scope" : "c2", + "type" : "Property" + } + }, { + "valueType" : "{urn:hl7-org:elm-types:r1}String", + "value" : "http://www.nlm.nih.gov/research/umls/rxnorm", + "type" : "Literal" + } ] + }, + "return" : { + "expression" : { + "path" : "value", + "type" : "Property", + "source" : { + "path" : "code", + "scope" : "c2", + "type" : "Property" + } + } + } + } ] + } } ], "relationship" : [ ], "return" : { @@ -5905,48 +5952,30 @@ }, { "name" : "RXNorm_Code", "value" : { - "type" : "Coalesce", + "name" : "RxnormCode", + "type" : "QueryLetRef" + } + }, { + "name" : "isBup", + "value" : { + "type" : "In", "operand" : [ { - "type" : "Query", - "source" : [ { - "alias" : "c2", - "expression" : { - "path" : "coding", - "type" : "Property", - "source" : { - "name" : "c", - "type" : "QueryLetRef" - } - } - } ], - "relationship" : [ ], - "where" : { - "type" : "Equivalent", - "operand" : [ { - "path" : "value", - "type" : "Property", - "source" : { - "path" : "system", - "scope" : "c2", - "type" : "Property" - } - }, { - "valueType" : "{urn:hl7-org:elm-types:r1}String", - "value" : "http://www.nlm.nih.gov/research/umls/rxnorm", - "type" : "Literal" - } ] - }, - "return" : { - "expression" : { - "path" : "value", - "type" : "Property", - "source" : { - "path" : "code", - "scope" : "c2", - "type" : "Property" - } - } - } + "name" : "RxnormCode", + "type" : "QueryLetRef" + }, { + "name" : "BuprenorphineRxCUIs", + "type" : "ExpressionRef" + } ] + } + }, { + "name" : "isActive", + "value" : { + "type" : "SameOrAfter", + "operand" : [ { + "name" : "CalculatedEnd", + "type" : "QueryLetRef" + }, { + "type" : "Today" } ] } }, { diff --git a/src/helpers/flagit.js b/src/helpers/flagit.js index 47f11236d..65b63898b 100644 --- a/src/helpers/flagit.js +++ b/src/helpers/flagit.js @@ -75,7 +75,6 @@ export default function flagit(entry, subSection, summary) { function ifAnd(flagRulesArray, entry, subSection, summary) { for (let i = 0; i < flagRulesArray.length; ++i) { const flagRule = flagRulesArray[i]; - let match; if (typeof flagRule === "string") { match = functions[flagRule](entry, entry, subSection, summary); @@ -140,15 +139,18 @@ function ifGreaterThanOrEqualTo(value, entry, subSection, summary) { if (Array.isArray(targetEntry) && targetEntry.length) { targetEntry = targetEntry[0]; } - return parseInt(targetEntry[value.header], 10) >= value.value; + const valueToCompare = value.targetValue != null ? value.targetValue : value.value; + return parseInt(targetEntry[value.header], 10) >= valueToCompare; } /* * return true if an entry's value for a field matches the specified target value */ function ifEqualTo(value, entry, subSection, summary) { if (!entry) return false; - if (Array.isArray(entry[value.header])) - return entry[value.header].indexOf(value.targetValue) !== -1; + if (Array.isArray(entry[value.header])) return entry[value.header].indexOf(value.targetValue) !== -1; + if (value.type === "boolean") { + return Boolean(entry[value.header]) === Boolean(value.targetValue); + } return entry[value.header] === value.targetValue; } /* diff --git a/src/helpers/utility.js b/src/helpers/utility.js index 1e3abfb9d..9c03f1da4 100644 --- a/src/helpers/utility.js +++ b/src/helpers/utility.js @@ -2,6 +2,7 @@ import moment from "moment"; import { toBlob, toJpeg } from "html-to-image"; import { getEnv, ENV_VAR_PREFIX } from "../utils/envConfig"; import reportSummarySections from "../config/report_config"; +import { getTokenInfoFromStorage } from "./timeout"; /* * return number of days between two dates @@ -677,7 +678,7 @@ export function getSiteId() { export function isReportEnabled() { const siteId = getSiteId(); - return String(siteId).toLowerCase() === "uwmc"; + return ["uwmc", "demo"].indexOf(String(siteId).toLowerCase()) !== -1; } export function getEnvDashboardURL() { @@ -706,3 +707,45 @@ export function dedupArrObjects(arr, key) { return acc; }, []); } + +export function getMatomoTrackingSiteId() { + return getEnv(`${ENV_VAR_PREFIX}_MATOMO_SITE_ID`); +} + +export function getUserIdFromAccessToken() { + const accessToken = getTokenInfoFromStorage(); + if (!accessToken) return null; + if (accessToken.profile) return accessToken.profile; + if (accessToken.fhirUser) return accessToken.fhirUser; + return accessToken["preferred_username"]; +} + +export function addMatomoTracking() { + // already generated script, return + if (document.querySelector("#matomoScript")) return; + const userId = getUserIdFromAccessToken(); + // no user Id return + if (!userId) return; + const siteId = getMatomoTrackingSiteId(); + // no site Id return + if (!siteId) return; + // init global piwik tracking object + window._paq = []; + window._paq.push(["trackPageView"]); + window._paq.push(["enableLinkTracking"]); + window._paq.push(["setSiteId", siteId]); + window._paq.push(["setUserId", userId]); + + let u = "https://piwik.cirg.washington.edu/"; + window._paq.push(["setTrackerUrl", u + "matomo.php"]); + let d = document, + g = d.createElement("script"), + headElement = document.querySelector("head"); + g.type = "text/javascript"; + g.async = true; + g.defer = true; + g.setAttribute("src", u + "matomo.js"); + g.setAttribute("id", "matomoScript"); + headElement.appendChild(g); +} + diff --git a/src/styles/components/_Report.scss b/src/styles/components/_Report.scss index 537c0484c..eebd5aa9c 100644 --- a/src/styles/components/_Report.scss +++ b/src/styles/components/_Report.scss @@ -78,6 +78,12 @@ $selector-box-shadow: 0 2px 2px #383e40; margin-left: 24px; margin-right: 24px; margin-bottom: 16px; + display: flex; + flex-direction: column; + gap: 8px; + &.rows { + flex-direction: row; + } } } .sub-section { @@ -532,7 +538,9 @@ $selector-box-shadow: 0 2px 2px #383e40; background-color: #FFF; tr { &:not(:last-of-type) { - border-bottom: 1px solid $color-gray-lightest; + td { + border-bottom: 1px solid $color-gray-lightest; + } } } td { diff --git a/src/styles/components/_Summary.scss b/src/styles/components/_Summary.scss index 6d414c905..3dadf9d7d 100644 --- a/src/styles/components/_Summary.scss +++ b/src/styles/components/_Summary.scss @@ -28,8 +28,20 @@ .toc-list.is-collapsible { .toc-list-item { margin-bottom: 4px; + .flags-container { + display: flex; + flex-direction: row; + max-height: 56px; + flex-wrap: wrap; + gap: 8px; + svg { + font-size: 0.8em; + width: 16px; + margin-right: 0; + } + } .toc-link { - font-size: 0.8em; + font-size: 0.75em; padding: 4px 0 4px 24px; margin-left: 8px; line-height: 1.55; @@ -408,9 +420,10 @@ height: 100%; width: 100%; display: flex; - align-items: center; + align-items: flex-start; justify-content: center; font-size: 1.1em; + line-height: 1.35; } } @media (min-width: 992px) { @@ -952,7 +965,7 @@ border-left: 1px solid $color-gray-lightest; line-height: 1.5; background-color: #fff; - padding: 16px 24px; + padding: 8px 16px 16px; margin-top: 8px; } .close-button { diff --git a/src/styles/elements/_tables.scss b/src/styles/elements/_tables.scss index 8f8227136..896ee2f07 100644 --- a/src/styles/elements/_tables.scss +++ b/src/styles/elements/_tables.scss @@ -10,6 +10,7 @@ vertical-align: top; border: 0; padding: 4px 8px 4px 4px; + line-height: 1.4; &.flag-cell { max-width: 36px !important; text-align: center; @@ -23,7 +24,6 @@ th { white-space: nowrap; - max-width: 140px; &:empty, &:has(.flag__span:empty) { border: 0; @@ -76,7 +76,6 @@ } td { - max-width: 140px; &:empty:first-of-type { border-right: 0; border-left: 0; @@ -212,6 +211,16 @@ .table { overflow: auto; + .ReactTable { + th, td { + max-width: 140px; + } + &.single-column { + td, th { + max-width: 100%; + } + } + } &table, table { border-collapse: separate; border-spacing: 0; diff --git a/src/utils/executeELM.js b/src/utils/executeELM.js index 3d509e9c0..a13284ef4 100644 --- a/src/utils/executeELM.js +++ b/src/utils/executeELM.js @@ -245,7 +245,10 @@ async function executeELM(collector, paramResourceTypes) { ); resolve(evalResults); }); - }) + }).catch((e) => { + console.log("Error processing instrument ELM: ", e); + reject("error processing instrument ELM. See console for details."); + }); }, (e) => { @@ -269,12 +272,15 @@ async function executeELMForReport(bundle) { console.log("Issue occurred loading ELM lib for reoirt", e); r4ReportCommonELM = null; }); - + if (!r4ReportCommonELM) return null; - let reportLib = new cql.Library(r4ReportCommonELM, new cql.Repository({ - FHIRHelpers: r4HelpersELM, - })); + let reportLib = new cql.Library( + r4ReportCommonELM, + new cql.Repository({ + FHIRHelpers: r4HelpersELM, + }) + ); const reportExecutor = new cql.Executor( reportLib, new cql.CodeService(valueSetDB) @@ -339,26 +345,24 @@ function getLibraryForInstruments() { if (!INSTRUMENT_LIST) return null; return INSTRUMENT_LIST.map((item) => (async () => { - let elmJson; - try { - elmJson = await import( - `../cql/r4/survey_resources/${item.key.toUpperCase()}_LogicLibrary.json` - ) - .then((module) => module.default) - .catch((e) => { - console.log( - "Issue occurred loading ELM lib for " + - item.key + - ". Will use default lib.", - e - ); - elmJson = null; - }); - } - catch(e) { - console.log("Error loading library ", e); - elmJson = null; - } + let elmJson = null; + const libPrefix = item.useDefaultELMLib + ? "Default" + : item.key.toUpperCase(); + elmJson = await import( + `../cql/r4/survey_resources/${libPrefix}_LogicLibrary.json` + ) + .then((module) => module.default) + .catch((e) => { + console.log( + "Issue occurred loading ELM lib for " + + item.key + + ". Will use default lib.", + e + ); + elmJson = null; + }); + if (!elmJson) { elmJson = await import( `../cql/r4/survey_resources/Default_LogicLibrary.json` diff --git a/vite.config.mjs b/vite.config.mjs index e090122af..5145f505c 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -8,6 +8,7 @@ export default defineConfig({ // To deploy at the root path, use "/" or remove the "base" property entirely. base: "/", envPrefix: "REACT_", + plugins: [ react(), nodePolyfills({