From 90e8cfd18cd72cafd6b28548e99059fd4296228b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Aug 2022 19:51:37 +0000 Subject: [PATCH 1/4] Bump async from 2.6.3 to 2.6.4 Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4. - [Release notes](https://github.com/caolan/async/releases) - [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md) - [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4) --- updated-dependencies: - dependency-name: async dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0540cf8a..1123cb93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2123,9 +2123,9 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==" }, "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "requires": { "lodash": "^4.17.14" } From 70ed937e5135e4e7a7927e93fc1b9bcb4ba35277 Mon Sep 17 00:00:00 2001 From: kghoreshi Date: Mon, 22 Aug 2022 17:26:26 -0400 Subject: [PATCH 2/4] Revert "Merge branch 'master' into master" This reverts commit c79173d110801ab6f9fcff31506cb17d32a9b580, reversing changes made to c195d2b973f9ff291e3753f8aed049bdefe63237. --- .dockerignore | 3 - .github/auto_assign.yml | 14 - .github/workflows/ForkSync.yml | 22 -- .github/workflows/docker-cd.yml | 2 +- src/App.jsx | 11 +- .../QuestionnaireForm/QuestionnaireForm.jsx | 195 +++----------- .../RemsInterface/RemsInterface.css | 149 ----------- .../RemsInterface/RemsInterface.jsx | 251 ------------------ .../RemsInterface/ResourceEntry.jsx | 33 --- .../buildPopulatedResourceBundle.js | 38 +-- 10 files changed, 54 insertions(+), 664 deletions(-) delete mode 100644 .dockerignore delete mode 100644 .github/auto_assign.yml delete mode 100644 .github/workflows/ForkSync.yml delete mode 100644 src/components/RemsInterface/RemsInterface.css delete mode 100644 src/components/RemsInterface/RemsInterface.jsx delete mode 100644 src/components/RemsInterface/ResourceEntry.jsx diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 5c114a26..00000000 --- a/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -.git -node_modules -logs \ No newline at end of file diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml deleted file mode 100644 index 9b78ad11..00000000 --- a/.github/auto_assign.yml +++ /dev/null @@ -1,14 +0,0 @@ -# Set to true to add reviewers to pull requests -addReviewers: true - -# Set to true to add assignees to pull requests -addAssignees: false - -# A list of reviewers to be added to pull requests (GitHub user name) -reviewers: - - zacharyrobin - - KeeyanGhoreshi - - smalho01 - -# A number of reviewers added to the pull request, Set to 0 to add all reviewers -numberOfReviewers: 0 diff --git a/.github/workflows/ForkSync.yml b/.github/workflows/ForkSync.yml deleted file mode 100644 index bb1ca4cc..00000000 --- a/.github/workflows/ForkSync.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Sync Fork - -on: - schedule: - - cron: '30 14 * * 1' # every Monday at 14:30 UCT - workflow_dispatch: # on button click for manual testing - -jobs: - sync: - - runs-on: ubuntu-latest - - steps: - - uses: tgymnich/fork-sync@v1.4 #1.4 is stable but may need to be updated - with: - owner: HL7-DaVinci - base: master # Upstream - head: master # local - ignore_fail: true - auto_merge: false - pr_title: Fork Auto Sync - pr_message: Upstream repository has been updated - Please review changes diff --git a/.github/workflows/docker-cd.yml b/.github/workflows/docker-cd.yml index fc246c67..229cebb3 100644 --- a/.github/workflows/docker-cd.yml +++ b/.github/workflows/docker-cd.yml @@ -23,4 +23,4 @@ jobs: with: context: . push: true - tags: codexrems/dtr:REMSvCurrent + tags: smalho01234/dtr:latest diff --git a/src/App.jsx b/src/App.jsx index dc86b8bd..4e7e8503 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -12,7 +12,6 @@ import Testing from "./components/ConsoleBox/Testing"; import UserMessage from "./components/UserMessage/UserMessage"; import TaskPopup from "./components/Popup/TaskPopup"; import PatientSelect from "./components/PatientSelect/PatientSelect"; -import RemsInterface from "./components/RemsInterface/RemsInterface"; // uncomment for testing UserMessage // let sampleError = { @@ -37,7 +36,6 @@ export default class App extends Component { questionnaire: null, response: null, priorAuthClaim: null, - specialtyRxBundle: null, cqlPrepopulationResults: null, deviceRequest: null, bundle: null, @@ -360,10 +358,6 @@ export default class App extends Component { this.setState({ priorAuthClaim: claimBundle }); } - setSpecialtyRxBundle(specialtyRxBundleParam) { - this.setState({ specialtyRxBundle: specialtyRxBundleParam }); - } - getQuestionByName(question) { //question should be the HTML node const temp = question.getElementsByClassName("lf-item-code ng-hide")[0].innerText.trim(); @@ -610,8 +604,8 @@ export default class App extends Component { > - {this.state.specialtyRxBundle ? ( - + {this.state.priorAuthClaim ? ( + ) : ( { + this.popupClear("Would you like to load a previously in-progress form?", "Cancel", false); this.processSavedQuestionnaireResponses(result, true); }, ((result) => { this.popupClear("Error: failed to load previous in-progress forms", "OK", true); @@ -226,8 +229,11 @@ export default class QuestionnaireForm extends Component { let count = 0; - partialResponses.entry.forEach(r => { - if (r.resource.questionnaire.includes(this.props.qform.id)) { + partialResponses.entry.forEach(bundleEntry => { + const questionnaireId = bundleEntry.resource.contained[0].id; + const questionaireIdUrl = bundleEntry.resource.questionnaire; + + if (this.props.qform.id === questionnaireId || this.props.qform.id.contains(questionaireIdUrl)) { count = count + 1; // add the option to the popupOptions let date = new Date(bundleEntry.resource.authored); @@ -1033,6 +1039,17 @@ export default class QuestionnaireForm extends Component { } // For HIMSS Demo with Mettle always use GCS as payor info + const insurer = { + resourceType: "Organization", + id: "org1234", + name: "GCS", + identifier: [ + { + system: "urn:ietf:rfc:3986", + value: "2.16.840.1.113883.13.34.110.1.150.2" + } + ] + }; const managingOrg = { resourceType: "Organization", id: "org1111", @@ -1076,6 +1093,7 @@ export default class QuestionnaireForm extends Component { if (priorAuthBundle && this.isPriorAuthBundleValid(priorAuthBundle)) { priorAuthBundle.entry.unshift({ resource: managingOrg }); priorAuthBundle.entry.unshift({ resource: facility }); + priorAuthBundle.entry.unshift({ resource: insurer }); priorAuthBundle.entry.unshift({ resource: this.props.deviceRequest }); priorAuthBundle.entry.unshift({ resource: qr }); @@ -1107,6 +1125,9 @@ export default class QuestionnaireForm extends Component { // TODO: make this organization reference: this.makeReference(priorAuthBundle, "Practitioner") }, + insurer: { + reference: this.makeReference(priorAuthBundle, "Organization") + }, facility: { reference: this.makeReference(priorAuthBundle, "Location") }, @@ -1185,19 +1206,6 @@ export default class QuestionnaireForm extends Component { } ] }; - - const signature = { - resourceType: "Signature", - type: [ - { - system: "urn:iso-astm:E1762-95:2013", - code: "1.2.840.10065.1.12.1.14", - display: "Source Signature" - } - ], - when: new Date(Date.now()).toISOString(), - who: this.makeReference(priorAuthBundle, "Practitioner") - } var sequence = 1; priorAuthBundle.entry.forEach(function (entry, index) { if (entry.resource.resourceType == "Condition") { @@ -1207,135 +1215,11 @@ export default class QuestionnaireForm extends Component { }); } }); - priorAuthBundle.timestamp = new Date(Date.now()).toISOString() - priorAuthBundle.language = "en"; - priorAuthBundle.id = shortid.generate(); - priorAuthBundle.meta = { - lastUpdated: Date.now() - } - priorAuthBundle.implicitRules = "http://build.fhir.org/ig/HL7/davinci-pas/StructureDefinition-profile-pas-request-bundle" - priorAuthBundle.identifier = { - use: "official", - system: "urn:uuid:mitre-drls", - value: shortid.generate() - } - priorAuthBundle.signature = signature; - priorAuthBundle.entry.unshift({ resource: priorAuthClaim }); - - const specialtyRxBundle = JSON.parse(JSON.stringify(priorAuthBundle)); - specialtyRxBundle.type = "message"; - if (this.makeReference(priorAuthBundle, "MedicationRequest")) { - const pharmacy = { - resourceType: "Organization", - id: "pharm0111", - identifier: [ - { - system: "http://hl7.org/fhir/sid/us-npi", - value: "1837247346" - }, - { - system: "http://terminology.hl7.org/CodeSystem/NCPDPProviderIdentificationNumber", - value: "838283882" - } - ], - telecom: [ - { - system : "phone", - value : "919-234-5174", - use : "work", - rank : "1", - } - ], - address: [ - { - use: "work", - state: "IL", - postalCode: "62864", - city: "Mount Vernon", - line: ["1500 Main St"] - } - ] - } - - const specialtyRxSearchResult = { - resourceType: "Bundle", - type: "searchset", - id: "bundle02", - total: 0, - link: [ - { - relation: "self", - url: "", - } - ], - entry: [] - } - - const specialtyRxParameters = { - resourceType: "Parameters", - id: "param0111", - parameter: [ - { - name: "source-patient", - reference: this.makeReference(priorAuthBundle, "Patient") - }, - { - name: "prescription", - reference: this.makeReference(priorAuthBundle, "MedicationRequest") - }, - { - name: "pharmacy", - reference: "Organization/pharm0111" - }, - { - name: "prescriber", - reference: this.makeReference(priorAuthBundle, "Practitioner") - }, - { - name: "search-result", - reference: "Bundle/bundle02" - }, - - ] - } - - const specialtyRxMessageHeader = { - resourceType: "MessageHeader", - id: "msghdr0111", - event: [ - { - eventCoding: { - system: "http://hl7.org/fhir/us/specialty-rx/CodeSystem/specialty-rx-event-type", - code: "query-response-unsolicited", - } - } - ], - focus: { - parameters: { - reference: "Parameters/param0111" - } - }, - source: { - // TODO: url should be dynamically created - // also if DTR expects to recieve a response it - // will need an endpoint to recieve it at - endpoint: "http://localhost:3005" - } - - } - - specialtyRxBundle.entry.unshift({ resource: specialtyRxSearchResult }); - specialtyRxBundle.entry.unshift({ resource: pharmacy }); - specialtyRxBundle.entry.unshift({ resource: specialtyRxParameters }); - specialtyRxBundle.entry.unshift({ resource: specialtyRxMessageHeader }); - - } - - console.log("specialtyRx", specialtyRxBundle); + console.log(priorAuthClaim); + priorAuthBundle.entry.unshift({ resource: priorAuthClaim }); this.props.setPriorAuthClaim(priorAuthBundle); - this.props.setSpecialtyRxBundle(specialtyRxBundle); } else { alert("Prior Auth Bundle is not available or does not contain enough resources for Prior Auth. Can't submit to prior auth.") } @@ -1444,15 +1328,12 @@ export default class QuestionnaireForm extends Component { const savedParentItem = findSavedParentItem(parentLinkId, savedItem); const replaceOrInsertItem = (newResponseItem, savedParentItem) => { - if(savedParentItem.item) { - const replaceIndex = savedParentItem.item.findIndex(item => item.linkId == newResponseItem.linkId); - if (replaceIndex != -1) { - savedParentItem.item[replaceIndex] = newResponseItem; - } else { - savedParentItem.item.push(newResponseItem); - } + const replaceIndex = savedParentItem.item.findIndex(item => item.linkId == newResponseItem.linkId); + if (replaceIndex != -1) { + savedParentItem.item[replaceIndex] = newResponseItem; + } else { + savedParentItem.item.push(newResponseItem); } - }; if (savedParentItem != undefined) { replaceOrInsertItem(newItem, savedParentItem); @@ -1482,11 +1363,14 @@ export default class QuestionnaireForm extends Component { + ) } @@ -1494,8 +1378,11 @@ export default class QuestionnaireForm extends Component { if (this.props.adFormCompleted) { return (
+
) diff --git a/src/components/RemsInterface/RemsInterface.css b/src/components/RemsInterface/RemsInterface.css deleted file mode 100644 index 05425e8e..00000000 --- a/src/components/RemsInterface/RemsInterface.css +++ /dev/null @@ -1,149 +0,0 @@ -body { - margin: 0; - padding: 0; - font-family: sans-serif; - - } - .left-form { - width: 48%; - float: left; - margin-top: 25px; - margin-left: 25px; - padding: 5px; - } - - .right-form { - width: 48%; - float: right; - margin-top: 25px; - margin-left: 25px; - padding: 5px; - } - - .resource-entry{ - clear: both; - border-left: 4px solid #ffcccb; - padding: 5px; - border-bottom: 1px solid grey; - background-color: #ededed; - padding-top: 20px; - padding-bottom:20px; - } - - .resource-entry:hover { - border-left: 4px solid #ff6663; - background-color: #fdfdfd; - } - - .resource-entry.active{ - border-left: 4px solid #ff6663; - background-color: #f5f5f5; - } - - .details{ - margin-left: 30px; - background-color: #ededed; - } - - -.submit-btn { - float: right; - margin-top: 6px; - } - - -.submit-btn:hover{ - border-width:1px 3px 1px 1px; - margin-left:2px; - margin-top:8px; -} - -.status-icon{ - width: 100%; - height:5px; -} - -.bundle-entry{ - margin: 5px; -} - -.renew-icon{ - cursor: pointer; - margin-left: 15px; -} -.refresh{ - cursor: pointer; - margin-left: 15px; - animation-name: spin; - animation-duration: 500ms; - animation-iteration-count: 2; - animation-timing-function: ease-in-out; -} - -.requestBody{ - overflow-y: scroll; - height:500px; -} -.jsonData{ - line-height:16px; - font-family: 'Courier New', Courier, monospace; - font-size: 14px; - - border-left: 2px solid #dddddd; - border-top:0px; - padding-left:6px; - - display:block; -} - -.elementBody{ - color:#adadad; -} -.elementKey{ - color:#343434 -} - -@keyframes spin { - from { - transform:rotate(0deg); - } - to { - transform:rotate(360deg); - } -} - - -/* ETASU styling */ -.resource-entry-hover{ - display:none; - padding-top:25px; - padding-left:25px;; -} - -.etasu-container{ - padding-bottom:10px; - padding-left: 10px; - padding-right:10px; -} - -.resource-entry-text{ - font-weight: bold; - float:left; - width:75%; -} - -.etasu-container:hover > .resource-entry-hover{ - clear: both; - display: block !important; -} - - -.resource-entry-icon{ - width:25%; - float:right; - text-align:right; -} - -.resource-child{ - margin-left:35px; -} \ No newline at end of file diff --git a/src/components/RemsInterface/RemsInterface.jsx b/src/components/RemsInterface/RemsInterface.jsx deleted file mode 100644 index be166c10..00000000 --- a/src/components/RemsInterface/RemsInterface.jsx +++ /dev/null @@ -1,251 +0,0 @@ -import React, { Component } from "react"; -import ResourceEntry from './ResourceEntry'; -import "./RemsInterface.css"; -import axios from "axios"; -import { SystemUpdateTwoTone } from "@material-ui/icons"; -import Paper from "@material-ui/core/Paper"; -import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; -import Button from '@material-ui/core/Button'; -import AutorenewIcon from '@material-ui/icons/Autorenew'; -const colorPicker = { - "Pending": "#f0ad4e", - "Approved": "#5cb85c", -} -export default class RemsInterface extends Component { - - constructor(props) { - super(props); - this.state = { - claimResponseBundle: null, - remsAdminResponse: null, - response: null, - spin: false, - spinPis: false, - viewResponse: false, - viewBundle: false, - viewPisBundle: false, - }; - - this.getAxiosOptions = this.getAxiosOptions.bind(this); - this.sendRemsMessage = this.sendRemsMessage.bind(this); - this.renderBundle = this.renderBundle.bind(this); - this.refreshBundle = this.refreshBundle.bind(this); - this.refreshPisBundle = this.refreshPisBundle.bind(this); - this.toggleBundle = this.toggleBundle.bind(this); - this.toggleResponse = this.toggleResponse.bind(this); - - this.togglePisBundle = this.togglePisBundle.bind(this); - } - - componentDidMount() { - this.sendRemsMessage(); - } - getAxiosOptions() { - const options = { - headers: { - Accept: "application/json", - "Content-Type": "application/json" - } - }; - return options; - } - - unfurlJson(jsonData) { - console.log(jsonData); - return jsonData.metRequirements.map(metReq => { - console.log(metReq); - return ( -
-
-
-
{metReq.requirement.name}
-
{metReq.completed ? "✅" : "❌"}
-
{metReq.requirement.description}
-
-
- { - metReq.childMetRequirements.map(subMetReq => -
-
-
{subMetReq.requirement.name}
-
{subMetReq.completed ? "✅" : "❌"}
-
{subMetReq.requirement.description}
-
-
- ) - } -
- ) - }); - - } - // if (jsonData) { - // return Object.keys(jsonData).map(element => { - // console.log(element); - // return ( - // //
- // // {element}: {jsonData[element] === null ? "null" : typeof jsonData[element] === "object" ? this.unfurlJson(jsonData[element], level + 1) : jsonData[element]} - // //
- //
- //
- //
TEST
- //
- //
- // ) - // }); - // } - - // } - - async sendRemsMessage() { - const remsAdminResponse = await axios.post("http://localhost:8090/rems", this.props.specialtyRxBundle, this.getAxiosOptions()); - this.setState({ remsAdminResponse }); - console.log(remsAdminResponse) - axios.post("http://localhost:3010/api/doctorOrder/$process-message", remsAdminResponse.data, this.getAxiosOptions()).then((response) => { - this.setState({ response }); - console.log(response); - console.log(response.data); - }); - - - } - - toggleBundle() { - this.setState((prevState) => { - return { ...prevState, viewBundle: !prevState.viewBundle } - }) - } - - toggleResponse() { - console.log(this.state.viewResponse); - this.setState((prevState) => { - return { ...prevState, viewResponse: !prevState.viewResponse } - }) - } - - togglePisBundle() { - this.setState((prevState) => { - return { ...prevState, viewPisBundle: !prevState.viewPisBundle } - }) - } - - renderBundle(bundle) { - return bundle.entry.map((entry) => { - const resource = entry.resource; - console.log(resource); - return ( -
- -
- ) - }) - } - - refreshPisBundle() { - this.setState({ spinPis: true }); - axios.get(`http://localhost:3010/api/doctorOrder/${this.state.response.data.doctorOrder._id}`).then((response) => { - this.setState({ response: response }); - }) - } - refreshBundle() { - this.setState({ spin: true }); - axios.get(`http://localhost:8090/rems/${this.state.remsAdminResponse.data.case_number}`).then((response) => { - this.setState({ remsAdminResponse: response }); - }) - } - render() { - const status = this.state.remsAdminResponse?.data?.status; - let color = "#f7f7f7" - if (status === "Approved") { - color = "#5cb85c" - } else if (status === "Pending") { - color = "#f0ad4e" - } - - let colorPis = "#f7f7f7" - const statusPis = this.state.response?.data?.doctorOrder?.dispenseStatus; - - if (statusPis === "Approved") { - colorPis = "#5cb85c" - } else if (statusPis === "Pending") { - colorPis = "#f0ad4e" - } else if (statusPis === "Picked Up") { - colorPis = "#0275d8" - } - - return ( -
-
-

REMS Admin Status

- -
-
- Case Number : {this.state.remsAdminResponse?.data?.case_number || "N/A"} -
-
- Status: {this.state.remsAdminResponse?.data?.status} -
-
- - - - {this.state.remsAdminResponse?.data?.case_number ? - this.setState({ spin: false })} - /> - : "" - } - -
- -
- {this.state.viewResponse ? -
- {this.unfurlJson(this.state.remsAdminResponse?.data, 0)} -
- : - ""} - {this.state.viewBundle ?
- {this.renderBundle(this.props.specialtyRxBundle)} -
: ""} - - - -
- -
-

Pharmacy Status

- -
-
- ID : {this.state.response?.data?.doctorOrder?._id || "N/A"} -
-
- Status: {this.state.response?.data?.doctorOrder?.dispenseStatus} -
-
- - {this.state.response?.data?.doctorOrder?._id ? - this.setState({ spinPis: false })} - /> - : "" - } - -
- -
- {this.state.viewPisBundle ?
- {this.renderBundle(this.props.specialtyRxBundle)} -
: ""} -
- {/* */} - -
- ) - } -} diff --git a/src/components/RemsInterface/ResourceEntry.jsx b/src/components/RemsInterface/ResourceEntry.jsx deleted file mode 100644 index acea9ce2..00000000 --- a/src/components/RemsInterface/ResourceEntry.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, {Component} from 'react' -import "./RemsInterface.css"; - -export default class ResourceEntry extends Component { - constructor(props) { - super(props); - this.state = { - viewDetails: null, - }; - this.openDetails = this.openDetails.bind(this); - - } - - - openDetails() { - - this.setState((prevState) => { - return { viewDetails: !prevState.viewDetails }; - }) - - - } - render() { - return ( -
-
-
{this.props.resource["resourceType"]}
-
- {this.state.viewDetails ?
{JSON.stringify(this.props.resource,null,'\t')}
: null} -
- ) - } -} \ No newline at end of file diff --git a/src/elmExecutor/buildPopulatedResourceBundle.js b/src/elmExecutor/buildPopulatedResourceBundle.js index eccc28db..feaee0e4 100644 --- a/src/elmExecutor/buildPopulatedResourceBundle.js +++ b/src/elmExecutor/buildPopulatedResourceBundle.js @@ -5,7 +5,6 @@ function doSearch(smart, type, fhirVersion, request, callback) { // setup the query for Practitioner and Coverage // TODO - handle other resource not associated with Patient? switch (type) { - case "PractitionerRole": case "Practitioner": let performer; if (request.resourceType === "DeviceRequest") { @@ -19,35 +18,18 @@ function doSearch(smart, type, fhirVersion, request, callback) { } q._id = performer; - if (type === "PractitionerRole") { - smart.request(`PractitionerRole?practitioner=${performer}`, {resolveReferences: ["location", "organization"], flat: true, graph:false}).then((result) => { - console.log(result); - let finalResult = [] - if(result && result.data){ - finalResult.push(result.data[0]) - if(result.references) { - Object.keys(result.references).forEach((e)=>{ - finalResult.push(result.references[e]) - }) - } - callback(finalResult); - } - }) - } else { - if( performer.includes("PractitionerRole")){ - q._id = null; // Not implemented - } else if( performer.includes("Practitioner")) { - q._id = performer.split("/")[1]; - } - smart.request(performer,{resolveReferences:"practitioner",flat:true}).then((result)=>{ - if(result && result.practitioner) { - request.performer = {"reference": "Practitioner/" + result.practitioner.id}; - callback([result.practitioner]); - } - - }); + if( performer.includes("PractitionerRole")){ + q._id = null; + } else if( performer.includes("Practitioner")) { + q._id = performer.split("/")[1]; } + smart.request(performer,{resolveReferences:"practitioner",flat:true}).then((result)=>{ + if(result && result.practitioner) { + request.performer = {"reference": "Practitioner/" + result.practitioner.id}; + callback([result.practitioner]); + } + }); usePatient = false; console.log(q._id); break; From 86ab1e0d08a9470db6be6a533f0721e6fc7dcf6f Mon Sep 17 00:00:00 2001 From: Keeyan Date: Mon, 12 Sep 2022 22:40:10 -0400 Subject: [PATCH 3/4] logging changes (#182) * logging changes * remove console log Co-authored-by: kghoreshi --- src/LogPage.jsx | 2 +- src/backend/database/impl.js | 13 +++++++++---- src/backend/routes/database.js | 11 ++++++++++- src/launch.js | 1 + src/util/util.js | 4 +++- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/LogPage.jsx b/src/LogPage.jsx index 119c5fcf..6fa6db4c 100644 --- a/src/LogPage.jsx +++ b/src/LogPage.jsx @@ -12,7 +12,7 @@ class LogPage extends Component { componentDidMount(){ const logRequest = new XMLHttpRequest(); - logRequest.open("GET", "../logs"); + logRequest.open("GET", "../api/logs"); logRequest.setRequestHeader("Content-Type", "application/json"); logRequest.onload = (e) => { this.setState({logs: JSON.parse(logRequest.responseText).sort((a,b)=>{return b.createdAt - a.createdAt})}); diff --git a/src/backend/database/impl.js b/src/backend/database/impl.js index 6d2f0d18..8382035d 100644 --- a/src/backend/database/impl.js +++ b/src/backend/database/impl.js @@ -31,18 +31,22 @@ function getCountAndIncrement() { } function postLog(log) { + log.createdAt = Date.now(); db.get("logs").push(log).write(); } function getLog(id) { return db.get("logs").find({id: id}).value(); } +function getLogs() { + return db.get("logs").value(); +} function putLog(id, log) { db.get("logs") - .find({ id: id }) - .assign(log) - .write(); + .find({ id: id }) + .assign(log).write(); + } function deleteClient(id) { @@ -56,6 +60,7 @@ module.exports = { deleteClient, postLog, getLog, + getLogs, putLog, getCountAndIncrement -}; \ No newline at end of file +}; diff --git a/src/backend/routes/database.js b/src/backend/routes/database.js index 7f3830ae..60ac4706 100644 --- a/src/backend/routes/database.js +++ b/src/backend/routes/database.js @@ -70,7 +70,7 @@ router.post("/logs", (req, res) => { router.put("/logs/:id", (req, res) => { const newLog = req.body; - db.putLog(req.params.id, newLog); + db.putLog(parseInt(req.params.id), newLog); res.sendStatus(200); }); @@ -84,4 +84,13 @@ router.get("/logs/:id", (req, res) => { } }); +router.get("/api/logs", (req, res) => { + const result = db.getLogs(); + if(result) { + res.send(result); + } else { + res.sendStatus(404) + } +}) + module.exports = router; \ No newline at end of file diff --git a/src/launch.js b/src/launch.js index f50c2af6..eb311e58 100644 --- a/src/launch.js +++ b/src/launch.js @@ -138,6 +138,7 @@ function callback(log) { "launch=" + encodeURIComponent(log.launchContextId); } + updateLog(log); window.location.href = authRedirect; } diff --git a/src/util/util.js b/src/util/util.js index 98f71172..3f03c6f3 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -75,7 +75,9 @@ function postToLogs(log, callback) { logRequest.open("POST", "../logs"); logRequest.setRequestHeader("Content-Type", "application/json"); logRequest.onload = function() { - callback(JSON.parse(logRequest.responseText)); + if(callback) { + callback(JSON.parse(logRequest.responseText)); + } }; logRequest.send(JSON.stringify(log)); } From fea86715888e757cd801d1cfef6462f6b07b9d76 Mon Sep 17 00:00:00 2001 From: Keeyan Date: Tue, 13 Sep 2022 12:58:11 -0400 Subject: [PATCH 4/4] App context changes (#184) * relaunch from appContext response * implement search by order * minor change to standalone * fix util function * check for standalone before prepop * adaptive form fixes * prepop adaptive form Co-authored-by: kghoreshi --- src/App.jsx | 112 ++++++---- .../QuestionnaireForm/QuestionnaireForm.jsx | 46 ++-- src/index.js | 2 - src/util/fetchArtifacts.js | 211 +++++++++++++++++- src/util/util.js | 6 + 5 files changed, 316 insertions(+), 61 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 4e7e8503..84bb2032 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom' import "./App.css"; import cqlfhir from "cql-exec-fhir"; import executeElm from "./elmExecutor/executeElm"; -import fetchArtifacts from "./util/fetchArtifacts"; +import {fetchArtifacts, fetchArtifactsOperation, fetchFromQuestionnaireResponse, searchByOrder} from "./util/fetchArtifacts"; import fetchFhirVersion from "./util/fetchFhirVersion"; import { buildFhirUrl } from "./util/util"; import PriorAuth from "./components/PriorAuth/PriorAuth"; @@ -37,7 +37,7 @@ export default class App extends Component { response: null, priorAuthClaim: null, cqlPrepopulationResults: null, - deviceRequest: null, + orderResource: null, bundle: null, filter: true, filterChecked: true, @@ -71,6 +71,7 @@ export default class App extends Component { this.standaloneLaunch = this.standaloneLaunch.bind(this); this.filter = this.filter.bind(this); this.onFilterCheckboxRefChange = this.onFilterCheckboxRefChange.bind(this); + this.fetchResourcesAndExecuteCql = this.fetchResourcesAndExecuteCql.bind(this); } componentDidMount() { @@ -80,15 +81,15 @@ export default class App extends Component { } standaloneLaunch(patient, response) { - const template = `Questionnaire/${response.questionnaire}`; fetchFhirVersion(this.props.smart.state.serverUrl) .then(fhirVersion => { this.fhirVersion = fhirVersion; - const questionnaireUrl = buildFhirUrl(template, this.props.FHIR_PREFIX, this.fhirVersion); + const questionnaireUrl = response.questionnaire; fetch(questionnaireUrl).then(r => r.json()) .then(questionnaire => { this.setState({ questionnaire: questionnaire }); this.setState({ response: response}); + this.setState({ isFetchingArtifacts: false}); }); }); } @@ -100,63 +101,91 @@ export default class App extends Component { }) } + ehrLaunch(isContainedQuestionnaire, questionnaire) { - // Temporary indication before full supports for relaunch is implemented - if(!this.appContext.request) { - alert("Supports for relaunch will be added in the near future!"); - this.consoleLog("Supports for relaunch will be added in the near future!", "errorClass"); - return; + let acOrder = this.appContext.order; + let acCoverage = this.appContext.coverage; + let acQuestionnaire = this.appContext.questionnaire; + let acResponse = this.appContext.response; + if(isContainedQuestionnaire && questionnaire) { + // TODO: This is a workaround for getting adaptive forms to work + // in its current form, adaptive forms do not operate with the + // package operation + const reloadQuestionnaire = questionnaire !== undefined; + this.setState({ + isFetchingArtifacts: true, + reloadQuestionnaire + }) + this.fetchResourcesAndExecuteCql(acOrder, acCoverage, acQuestionnaire, questionnaire); + + } else if(acOrder && acCoverage && !acQuestionnaire && !acResponse) { + searchByOrder(acOrder, this.smart).then((res) => { + // TODO: Don't know how to deal with multiple QRs + // Let user pick with a UI? Force orders to + // uniquely identify QRs? + // for now just pick the first one + acResponse = res[0].resource; + acQuestionnaire = acResponse.questionnaire; + this.setState({response: acResponse}); + this.fetchResourcesAndExecuteCql(acOrder, acCoverage, acQuestionnaire); + }); + } else if(acResponse) { + // start relaunch + // TODO: could potentially pass order to this function and avoid + // needing to search the QR context extension for it + // which would also support QRs without the extension. + fetchFromQuestionnaireResponse(acResponse, this.smart).then((relaunchContext) => { + this.setState({response: relaunchContext.response}) + this.fetchResourcesAndExecuteCql(relaunchContext.order, relaunchContext.coverage, relaunchContext.questionnaire); + }); + } else if(acQuestionnaire && acOrder && acCoverage){ + this.consoleLog("fetching artifacts", "infoClass"); + this.setState({ + isFetchingArtifacts: true + }) + const reloadQuestionnaire = questionnaire !== undefined; + this.setState({reloadQuestionnaire}); + this.fetchResourcesAndExecuteCql(acOrder, acCoverage, acQuestionnaire); + } else { + alert("invalid app context") } - const deviceRequest = JSON.parse(this.appContext.request.replace(/\\/g,"")); - this.consoleLog("fetching artifacts", "infoClass"); - this.setState({ - isFetchingArtifacts: true - }) - const reloadQuestionnaire = questionnaire !== undefined; - + } + + fetchResourcesAndExecuteCql(order, coverage, questionnaire, containedQuestionnaire) { fetchFhirVersion(this.props.smart.state.serverUrl) .then(fhirVersion => { this.fhirVersion = fhirVersion; - fetchArtifacts( - this.props.FHIR_PREFIX, - this.props.FILE_PREFIX, - !isContainedQuestionnaire ? this.appContext.template : questionnaire, - this.fhirVersion, - this.smart, - this.consoleLog, - isContainedQuestionnaire - ) + fetchArtifactsOperation(order, coverage, questionnaire, this.smart, this.consoleLog, containedQuestionnaire) .then(artifacts => { console.log("fetched needed artifacts:", artifacts); - + const orderResource = artifacts.order; let fhirWrapper = this.getFhirWrapper(this.fhirVersion); - this.setState({ questionnaire: artifacts.questionnaire }); - this.setState({ deviceRequest: deviceRequest }); + this.setState({ orderResource: orderResource }); this.setState({ isAdaptiveFormWithoutExtension: artifacts.questionnaire.meta && artifacts.questionnaire.meta.profile && artifacts.questionnaire.meta.profile.includes("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-adapt") && (artifacts.questionnaire.extension === undefined || !artifacts.questionnaire.extension.includes(e => e.url === "http://hl7.org/fhir/StructureDefinition/cqf-library")) }); this.setState({ }); // execute for each main library return Promise.all( artifacts.mainLibraryElms.map(mainLibraryElm => { let parameterObj; - if (deviceRequest.resourceType === "DeviceRequest") { + if (orderResource.resourceType === "DeviceRequest") { parameterObj = { - device_request: fhirWrapper.wrap(deviceRequest) + device_request: fhirWrapper.wrap(orderResource) }; } else if ( - deviceRequest.resourceType === "ServiceRequest" + orderResource.resourceType === "ServiceRequest" ) { parameterObj = { - service_request: fhirWrapper.wrap(deviceRequest) + service_request: fhirWrapper.wrap(orderResource) }; - } else if (deviceRequest.resourceType === "MedicationRequest") { + } else if (orderResource.resourceType === "MedicationRequest") { parameterObj = { - medication_request: fhirWrapper.wrap(deviceRequest) + medication_request: fhirWrapper.wrap(orderResource) }; - } else if (deviceRequest.resourceType === "MedicationDispense") { + } else if (orderResource.resourceType === "MedicationDispense") { parameterObj = { - medication_dispense: fhirWrapper.wrap(deviceRequest) + medication_dispense: fhirWrapper.wrap(orderResource) }; } @@ -192,7 +221,7 @@ export default class App extends Component { return executeElm( this.smart, this.fhirVersion, - deviceRequest, + orderResource, executionInputs, this.consoleLog ); @@ -245,13 +274,11 @@ export default class App extends Component { this.setState({ bundle: fullBundle, cqlPrepopulationResults: allLibrariesResults, - isFetchingArtifacts: false, - reloadQuestionnaire + isFetchingArtifacts: false }); }); }); } - getFhirWrapper(fhirVersion) { if (fhirVersion == "r4") { return cqlfhir.FHIRWrapper.FHIRv400(); @@ -609,8 +636,9 @@ export default class App extends Component { ) : ( { this.popupClear("Error: failed to load previous in-progress forms", "OK", true); this.popupLaunch(); @@ -189,7 +188,17 @@ export default class QuestionnaireForm extends Component { // retrieve next sets of questions loadNextQuestions() { - const url = this.props.FILE_PATH + "fhir" + "/" + this.fhirVersion + "/" + "Questionnaire/$next-question"; + // this is a temp fix for adaptive forms + // TODO: figure out what to do about next-question standardization. + let qformUrl = this.props.appContext.questionnaire; + if(qformUrl) { + const urlArray = qformUrl.split('/'); + urlArray.pop(); + qformUrl = urlArray.join('/'); + } else { + qformUrl = 'http://localhost:8090/fhir/r4/Questionnaire' + } + const url = `${qformUrl}/$next-question`; const currentQuestionnaireResponse = window.LForms.Util.getFormFHIRData('QuestionnaireResponse', this.fhirVersion, "#formContainer");; //const mergedResponse = this.mergeResponseForSameLinkId(currentQuestionnaireResponse); @@ -230,10 +239,14 @@ export default class QuestionnaireForm extends Component { let count = 0; partialResponses.entry.forEach(bundleEntry => { - const questionnaireId = bundleEntry.resource.contained[0].id; - const questionaireIdUrl = bundleEntry.resource.questionnaire; + let idMatch = false; + if(bundleEntry.resource.contained){ + const questionnaireId = bundleEntry.resource?.contained[0].id; + idMatch = this.props.qform.id === questionnaireId; + } + const questionnaireIdUrl = bundleEntry.resource.questionnaire; - if (this.props.qform.id === questionnaireId || this.props.qform.id.contains(questionaireIdUrl)) { + if ( idMatch || questionnaireIdUrl.includes(this.props.qform.id)) { count = count + 1; // add the option to the popupOptions let date = new Date(bundleEntry.resource.authored); @@ -370,7 +383,7 @@ export default class QuestionnaireForm extends Component { e.url == "http://hl7.org/fhir/StructureDefinition/cqf-expression" || e.url == "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression" ); - if (isGtable && containsValueExpression) { + if (isGtable && containsValueExpression && !this.props.standalone) { // check if the prepopulationResult contains any value // if yes, then need to add corresponding sub-items then provide the answer // need to figure out which value is provided from the prepopulationResult though @@ -911,7 +924,7 @@ export default class QuestionnaireForm extends Component { }; this.addAuthorToResponse(qr, this.getPractitioner()); - qr.questionnaire = `${this.FHIR_PREFIX}${this.fhirVersion}/Questionnaire/${this.props.qform.id}`; + qr.questionnaire = this.appContext.questionnaire?this.appContext.questionnaire:this.props.response.questionnaire; console.log("GetQuestionnaireResponse final QuestionnaireResponse: ", qr); const request = this.props.deviceRequest; @@ -976,7 +989,14 @@ export default class QuestionnaireForm extends Component { } console.log(requestOptions); - let url = this.FHIR_PREFIX + this.fhirVersion + "/QuestionnaireResponse"; + let url = this.props.appContext.questionnaire; + if(url) { + const urlArray = url.split('/'); + url = urlArray.slice(0, -2).join('/'); + } else { + url = 'http://localhost:8090/fhir/r4' + } + url = url + "/QuestionnaireResponse"; console.log(url); fetch(url, requestOptions).then(handleFetchErrors).then(r => { let msg = "QuestionnaireResponse sent to Payer"; diff --git a/src/index.js b/src/index.js index eaf04bc4..d6142274 100644 --- a/src/index.js +++ b/src/index.js @@ -99,8 +99,6 @@ tokenPost.onload = function() { // too badly. ReactDOM.render( { + parameters.parameter.push({"name": "coverage", "resource": coverage}); + const requestOptions = { + method: 'POST', + headers: { 'Content-Type': 'application/fhir+json' }, + body: JSON.stringify(parameters) + }; + fetch(`${questionnaire}/$questionnaire-package`, requestOptions) + .then(handleFetchErrors) + .then((e)=> {return e.json()}).then((result) => { + // TODO: Handle multiple questionnaires + const bundle = result.parameter[0].resource.entry; + let questionnaire; + + if (containedQuestionnaire) { + retVal.questionnaire = containedQuestionnaire; + questionnaire = containedQuestionnaire; + } else { + questionnaire = bundle.find((e) => e.resource.resourceType === "Questionnaire")?.resource; + retVal.questionnaire = questionnaire; + } + + retVal.isAdaptiveFormWithoutExtension = questionnaire.extension && questionnaire.extension.length > 0; + + findQuestionnaireEmbeddedCql(questionnaire.item); + searchBundle(questionnaire, bundle); + console.log(retVal); + resolve(retVal); + }) + }) + } + + function searchBundle(questionnaire, bundle) { + if (questionnaire.extension !== undefined) { + // grab all main elm urls + // R4 resources use cqf library. + var mainElmReferences = questionnaire.extension.filter(ext => ext.url == "http://hl7.org/fhir/StructureDefinition/cqf-library") + .map(lib => lib.valueCanonical); + bundle.forEach((entry) => { + var resource = entry.resource; + if(resource.resourceType === "Library") { + const base64elmData = resource.content.filter(c => c.contentType == "application/elm+json")[0].data; + // parse the json string + let elm = JSON.parse(Buffer.from(base64elmData, 'base64')); + if (mainElmReferences.find((mainElmReference) => { + return resource.url === mainElmReference + })){ + // set the elm where it needs to be + retVal.mainLibraryElms.push(elm); + elmLibraryMaps[elm.library.identifier.id] = resource; + retVal.mainLibraryMaps = elmLibraryMaps; + } else { + retVal.dependentElms.push(elm); + } + } else if(resource.resourceType === "ValueSet") { + retVal.valueSets.push(resource); + } + }) + // mainElmReferences.forEach((mainElmReference) => { + // console.log(mainElmReference); + // var libraryResource = bundle.find((e)=>{return e.resource.url === mainElmReference})?.resource; + // if(libraryResource) { + // const base64elmData = libraryResource.content.filter(c => c.contentType == "application/elm+json")[0].data; + // Buffer.from(base64elmData, 'base64'); + + // // parse the json string + // let elm = JSON.parse(elmString); + + // // set the elm where it needs to be + // retVal.mainLibraryElms.push(elm); + // elmLibraryMaps[elm.library.identifier.id] = libraryResource; + // retVal.mainLibraryMaps = elmLibraryMaps; + // } + + // }); + } + } + // recursively searches questionnaire for + // embedded cql and puts it in the main + // elm library list + function findQuestionnaireEmbeddedCql(inputItems) { + if(!inputItems) { + return; + } + inputItems.forEach(item => { + const itemExtensions = item.extension; + if(item.extension) { + let findEmbeddedCql = item.extension.find(ext => + ext.url === "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression" + && ext.valueExpression && ext.valueExpression.language === "application/elm+json"); + + if(findEmbeddedCql) { + const itemLibrary = JSON.parse(findEmbeddedCql.valueExpression.expression); + itemLibrary.library.identifier= { + id: "LibraryLinkId" + item.linkId, + version: "0.0.1" + }; + elmLibraryMaps[itemLibrary.library.identifier.id] = itemLibrary; + retVal.mainLibraryMaps = elmLibraryMaps; + retVal.mainLibraryElms.push(itemLibrary); + } + } + + if(item.item !== undefined && item.item.length > 0) { + findQuestionnaireEmbeddedCql(item.item); + } + }); + } + + if(isRequestReference(order)) { + smart.request(order).then((orderResource) => { + completeOperation(orderResource); + }) + } else { + const orderResource = JSON.parse(order.replace(/\\/g,"")); + completeOperation(orderResource) + } + + }) +} + +function fetchArtifacts(questionnaireReference, fhirVersion, smart, consoleLog, isContainedQuestionnaire) { return new Promise(function(resolve, reject) { function handleFetchErrors(response) { @@ -273,4 +427,55 @@ function fetchArtifacts(fhirPrefix, filePrefix, questionnaireReference, fhirVers }); } -export default fetchArtifacts; \ No newline at end of file +function fetchFromQuestionnaireResponse(response, smart) { + const relaunchContext = { + questionnaire: null, + order: null, + coverage: null, + response: null, + } + + return new Promise(function(resolve, reject) { + smart.request(response).then((res) => { + console.log(res); + relaunchContext.questionnaire = res.questionnaire; + relaunchContext.response = res; + if(res.extension) { + const extensions = res.extension.filter((ext) => ext.url === "http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/context") + extensions.forEach((ext) => { + if(ext.valueReference.type === "Coverage") { + relaunchContext.coverage = ext.valueReference.reference; + } else { + relaunchContext.order = ext.valueReference.reference; + } + }) + } + resolve(relaunchContext); + }) + }) + +} + +function searchByOrder(order, smart) { + let requestId; + if(isRequestReference(order)){ + requestId = order; + } else { + const orderResource = JSON.parse(order.replace(/\\/g,"")); + requestId = `${orderResource.resourceType}/${orderResource.id}` + } + return new Promise(function(resolve, reject) { + smart.request(`QuestionnaireResponse?context=${requestId}`).then((res) => { + if(res.entry) { + resolve(res.entry) + } + }) + }) +} + +export { + fetchArtifacts, + fetchArtifactsOperation, + fetchFromQuestionnaireResponse, + searchByOrder +}; \ No newline at end of file diff --git a/src/util/util.js b/src/util/util.js index 3f03c6f3..c544c96c 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -8,6 +8,11 @@ function findValueByPrefix(object, prefix) { } } +function isRequestReference(reference) { + const re = /^(?:DeviceRequest|MedicationRequest|ServiceRequest)\/.+$/ + return re.exec(reference)?true:false; +} + function buildFhirUrl(reference, fhirPrefix, fhirVersion) { if (reference.startsWith("http")) { var endIndex = reference.lastIndexOf("/"); @@ -157,6 +162,7 @@ function searchQuestionnaire(questionnaire, attestation) { } export { + isRequestReference, findValueByPrefix, getListOfChoices, postToLogs,