diff --git a/src/PrefetchTemplate.js b/src/PrefetchTemplate.js index fb485a93..a429730a 100644 --- a/src/PrefetchTemplate.js +++ b/src/PrefetchTemplate.js @@ -6,83 +6,33 @@ export class PrefetchTemplate { const prefetchMap = new Map(); - const COVERAGE_PREFETCH_QUERY = new PrefetchTemplate( - "Coverage?patient={{context.patientId}}"); + const PRACTITIONER_PREFETCH = new PrefetchTemplate( + "{{context.userId}}"); - const DEVICE_REQUEST_BUNDLE = new PrefetchTemplate( - "DeviceRequest?_id={{context.draftOrders.DeviceRequest.id}}" - + "&_include=DeviceRequest:patient" - + "&_include=DeviceRequest:performer" - + "&_include=DeviceRequest:requester" - + "&_include=DeviceRequest:device" - + "&_include:iterate=PractitionerRole:organization" - + "&_include:iterate=PractitionerRole:practitioner"); + const REQUEST_PREFETCH = new PrefetchTemplate( + "MedicationRequest/{{context.medications.MedicationRequest.id}}"); + const PATIENT_PREFETCH = new PrefetchTemplate("{{context.patientId}}"); - const MEDICATION_REQUEST_BUNDLE = new PrefetchTemplate( - "MedicationRequest?_id={{context.medications.MedicationRequest.id}}" - + "&_include=MedicationRequest:patient" - + "&_include=MedicationRequest:intended-dispenser" - + "&_include=MedicationRequest:requester:PractitionerRole" - + "&_include=MedicationRequest:medication" - + "&_include:iterate=PractitionerRole:organization" - + "&_include:iterate=PractitionerRole:practitioner"); - - const MEDICATION_DISPENSE_BUNDLE = new PrefetchTemplate( - "MedicationDispense?_id={{context.medications.MedicationDispense.id}}" - + "&_include=MedicationDispense:patient" - + "&_include=MedicationDispense:intended-dispenser" - + "&_include=MedicationDispense:requester:PractitionerRole" - + "&_include=MedicationDispense:medication" - + "&_include:iterate=PractitionerRole:organization" - + "&_include:iterate=PractitionerRole:practitioner"); - - const NUTRITION_ORDER_BUNDLE = new PrefetchTemplate( - "NutritionOrder?_id={{context.draftOrders.NutritionOrder.id}}" - + "&_include=NutritionOrder:patient" - + "&_include=NutritionOrder:provider" - + "&_include=NutritionOrder:requester" - + "&_include=PractitionerRole:organization" - + "&_include=PractitionerRole:practitioner" - + "&_include=NutritionOrder:encounter" - + "&_include=Encounter:location"); - - const SERVICE_REQUEST_BUNDLE = new PrefetchTemplate( - "ServiceRequest?_id={{context.draftOrders.ServiceRequest.id}}" - + "&_include=ServiceRequest:patient" - + "&_include=ServiceRequest:performer" - + "&_include=ServiceRequest:requester" - + "&_include:iterate=PractitionerRole:organization" - + "&_include:iterate=PractitionerRole:practitioner"); - - const APPOINTMENT_BUNDLE = new PrefetchTemplate( - "appointmentBundle", - "Appointment?_id={{context.appointments.Appointment.id}}" - + "&_include=Appointment:patient" - + "&_include=Appointment:practitioner:PractitionerRole" - + "&_include:iterate=PractitionerRole:organization" - + "&_include:iterate=PractitionerRole:practitioner" - + "&_include=Appointment:location"); - - const ENCOUNTER_BUNDLE = new PrefetchTemplate( - "encounterBundle", - "Encounter?_id={{context.encounterId}}" - + "&_include=Encounter:patient" - + "&_include=Encounter:service-provider" - + "&_include=Encounter:practitioner" - + "&_include=Encounter:location"); - - prefetchMap.set("Coverage", COVERAGE_PREFETCH_QUERY); - prefetchMap.set("DeviceRequest", DEVICE_REQUEST_BUNDLE); - prefetchMap.set("MedicationRequest", MEDICATION_REQUEST_BUNDLE); - prefetchMap.set("MedicationDispense", MEDICATION_DISPENSE_BUNDLE); - prefetchMap.set("ServiceRequest", SERVICE_REQUEST_BUNDLE); - prefetchMap.set("Encounter", ENCOUNTER_BUNDLE); + // prefetchMap.set("Coverage", COVERAGE_PREFETCH_QUERY); + prefetchMap.set("request", REQUEST_PREFETCH); + prefetchMap.set("practitioner", PRACTITIONER_PREFETCH); + prefetchMap.set("patient", PATIENT_PREFETCH); + // prefetchMap.set("ServiceRequest", SERVICE_REQUEST_BUNDLE); + // prefetchMap.set("Encounter", ENCOUNTER_BUNDLE); return prefetchMap; } static generateParamElementMap() { const paramElementMap = new Map(); + // TODO - this should just be inferred based on context. Or rather + // the instructions from the hook about what context to fill in + // Quite literally, the "context" here refers to the "context" of the + // cds-hook, which as of now is just hard-coded in buildRequest.js + // Rather than do this, which searches the request resource for information, + // the cds-hook should be constructed and then the context used to actually make + // the appropriate requests. + paramElementMap.set('context.userId', ['requester', 'reference']) paramElementMap.set('context.draftOrders.DeviceRequest.id', ['id']); paramElementMap.set('context.medications.MedicationRequest.id', ['id']); paramElementMap.set('context.medications.MedicationDispense.id', ['id']); diff --git a/src/components/RequestBox/RequestBox.js b/src/components/RequestBox/RequestBox.js index 31f21b17..9ddc4f70 100644 --- a/src/components/RequestBox/RequestBox.js +++ b/src/components/RequestBox/RequestBox.js @@ -3,8 +3,9 @@ import FHIR from "fhirclient"; import SMARTBox from "../SMARTBox/SMARTBox"; import PatientBox from "../SMARTBox/PatientBox"; import CheckBox from '../Inputs/CheckBox'; -import { defaultValues, shortNameMap } from "../../util/data"; +import { types, defaultValues, shortNameMap } from "../../util/data"; import { getAge } from "../../util/fhir"; +import buildNewRxRequest from '../../util/buildScript.2017071.js'; import _ from "lodash"; import "./request.css"; import { PrefetchTemplate } from "../../PrefetchTemplate"; @@ -57,9 +58,15 @@ export default class RequestBox extends Component { prepPrefetch() { const preppedResources = new Map(); Object.keys(this.state.prefetchedResources).forEach((resourceKey) => { - const resourceList = this.state.prefetchedResources[resourceKey].map((resource) => { - return resource; - }) + let resourceList = [] + if(Array.isArray(this.state.prefetchedResources[resourceKey])){ + resourceList = this.state.prefetchedResources[resourceKey].map((resource) => { + return resource; + }) + } else { + resourceList = this.state.prefetchedResources[resourceKey] + } + preppedResources.set(resourceKey, resourceList); }); return preppedResources; @@ -92,7 +99,7 @@ export default class RequestBox extends Component { if(!prevState[elementName][key]){ prevState[elementName][key] = []; } - return {[elementName]: {...prevState[elementName], [key]: [...prevState[elementName][key], text]}}; + return {[elementName]: {...prevState[elementName], [key]: text}}; }); }; @@ -212,11 +219,16 @@ export default class RequestBox extends Component { var renderedPrefetches = new Map(); requestResources.forEach((resourceList, resourceKey) => { const renderedList = []; - resourceList.forEach((resource) => { - console.log("Request resources:" + JSON.stringify(requestResources)); - console.log("Request key:" + resourceKey); - renderedList.push(this.renderResource(resource)) - }); + if(Array.isArray(resourceList)){ + resourceList.forEach((resource) => { + console.log("Request resources:" + JSON.stringify(requestResources)); + console.log("Request key:" + resourceKey); + renderedList.push(this.renderResource(resource)) + }); + } else { + renderedList.push(this.renderResource(resourceList)) + } + renderedPrefetches.set(resourceKey, renderedList); }); console.log(renderedPrefetches); @@ -325,6 +337,64 @@ export default class RequestBox extends Component { }); } + /** + * Send the NewRxRequestMessage to the Pharmacy Information System (PIMS) + */ + sendRx = (e) => { + console.log("sendRx: " + this.props.pimsUrl); + + // build the NewRx Message + var newRx = buildNewRxRequest(this.state.prefetchedResources.patient, + this.state.prefetchedResources.practitioner, + this.state.request); + console.log(newRx); + const serializer = new XMLSerializer(); + + // send the message to the prescriber + this.props.consoleLog("Sending Rx to PIMS", types.info); + fetch(this.props.pimsUrl, { + method: 'POST', + //mode: 'no-cors', + headers: { + 'Accept': 'application/xml', + 'Content-Type': 'application/xml' + }, + body: serializer.serializeToString(newRx) + }) + .then(response => { + console.log("sendRx response: "); + console.log(response); + this.props.consoleLog("Successfully sent Rx to PIMS", types.info); + }) + .catch(error => { + console.log("sendRx error: "); + this.props.consoleLog("Server returned error sending Rx to PIMS: ", types.error); + this.props.consoleLog(error.message); + console.log(error); + }); + + } + + resetRemsAdmin = (e) => { + console.log("reset rems admin: " + "localhost:8090/etasu/reset"); + + fetch("http://localhost:8090/etasu/reset", { + method: 'POST', + }) + .then(response => { + console.log("Reset rems admin etasu: "); + console.log(response); + this.props.consoleLog("Successfully reset rems admin etasu", types.info); + }) + .catch(error => { + console.log("Reset rems admin error: "); + this.props.consoleLog("Server returned error when resetting rems admin etasu: ", types.error); + this.props.consoleLog(error.message); + console.log(error); + }); + + } + isOrderNotSelected() { return Object.keys(this.state.request).length === 0; } @@ -341,6 +411,7 @@ export default class RequestBox extends Component { } const disableSendToCRD = this.isOrderNotSelected() || this.props.loading ; const disableLaunchDTR = this.isOrderNotSelected() && Object.keys(this.state.response).length === 0; + const disableSendRx = this.isOrderNotSelected() || this.props.loading; return (
@@ -400,11 +471,17 @@ export default class RequestBox extends Component {
+ +
); diff --git a/src/components/SMARTBox/PatientBox.js b/src/components/SMARTBox/PatientBox.js index e638ac5d..468e4c26 100644 --- a/src/components/SMARTBox/PatientBox.js +++ b/src/components/SMARTBox/PatientBox.js @@ -38,7 +38,7 @@ export default class SMARTBox extends Component { } else if (request.resourceType === "ServiceRequest") { code = request.code.coding[0]; } else if (request.resourceType === "MedicationRequest" - || request.resourceType === "MedicationDispense") { + || request.resourceType === "MedicationDispense") { code = request.medicationCodeableConcept.coding[0]; } if (code) { @@ -77,7 +77,7 @@ export default class SMARTBox extends Component { this.props.callback("patient", patient); this.props.callback("openPatient", false); this.props.clearCallback(); - if(this.state.request !== "none" ) { + if (this.state.request !== "none") { const request = JSON.parse(this.state.request); if (request.resourceType === "DeviceRequest" || request.resourceType === "ServiceRequest" || request.resourceType === "MedicationRequest" || request.resourceType === "MedicationDispense") { this.updatePrefetchRequest(request); @@ -85,8 +85,8 @@ export default class SMARTBox extends Component { this.props.clearCallback(); } } - - if(this.state.response !== "none") { + + if (this.state.response !== "none") { const response = JSON.parse(this.state.response); this.updateQRResponse(patient, response); } @@ -98,7 +98,7 @@ export default class SMARTBox extends Component { updatePrefetchRequest(request) { this.props.callback(request.resourceType, request); - const queries = this.props.updatePrefetchCallback(request, request.resourceType, "Coverage"); + const queries = this.props.updatePrefetchCallback(request, "request", "patient", "practitioner"); queries.forEach((query, queryKey) => { const urlQuery = this.props.ehrUrl + '/' + query; fetch(urlQuery, { @@ -106,10 +106,8 @@ export default class SMARTBox extends Component { }).then((response) => { const responseJson = response.json() return responseJson; - }).then((bundle) => { - bundle['entry'].forEach((fullResource) => { - this.props.callbackMap("prefetchedResources", queryKey, fullResource); - }); + }).then((resource) => { + this.props.callbackMap("prefetchedResources", queryKey, resource); }); }); this.props.callback("request", request); @@ -163,7 +161,7 @@ export default class SMARTBox extends Component { flat: true, }) .then((result) => { - this.setState({ medicationDispenses: result}); + this.setState({ medicationDispenses: result }); }); } @@ -191,17 +189,17 @@ export default class SMARTBox extends Component { this.setState({ response: "none" }); - } else { + } else { this.setState({ response: data.value }); } } - + getRequests() { const client = FHIR.client( - this.props.params - ); + this.props.params + ); const patientId = this.props.patient.id; this.getDeviceRequest(patientId, client); this.getServiceRequest(patientId, client); @@ -235,7 +233,7 @@ export default class SMARTBox extends Component { const display = `${qr.questionnaire}: created at ${qr.authored}`; let option = { key: qr.id, - text: display, + text: display, value: JSON.stringify(qr), content: (
@@ -270,24 +268,25 @@ export default class SMARTBox extends Component { }); } if (this.state.medicationRequests.data) { - this.state.medicationRequests.data.forEach((e) => { + this.state.medicationRequests.data.forEach((e) => { this.makeOption(e, options); }); } if (this.state.medicationDispenses.data) { this.state.medicationDispenses.data.forEach((e) => { - this.makeOption(e, options); - })}; + this.makeOption(e, options); + }) + }; - if(this.state.questionnaireResponses.data) { + if (this.state.questionnaireResponses.data) { returned = true; this.state.questionnaireResponses.data.forEach(qr => this.makeQROption(qr, responseOptions)); } - + let noResults = 'No results found.' - if(!returned) { - noResults = 'Loading...'; + if (!returned) { + noResults = 'Loading...'; } return ( @@ -321,10 +320,10 @@ export default class SMARTBox extends Component { Request: - In Progress Form: - { return isNaN(foo) }), @@ -107,6 +108,11 @@ export default class RequestBuilder extends Component { } + timeout = (time) => { + let controller = new AbortController(); + setTimeout(()=>controller.abort(), time * 1000); + return controller; + } submit_info(prefetch, request, patient, hook, deidentifyRecords) { this.setState({loading: true}); @@ -128,21 +134,19 @@ export default class RequestBuilder extends Component { } let baseUrl = this.state.baseUrl; const jwt = "Bearer " + createJwt(this.state.keypair, baseUrl, cdsUrl); - console.log(jwt); var myHeaders = new Headers({ "Content-Type": "application/json", "authorization": jwt }); - this.consoleLog("Fetching response from " + cdsUrl, types.info); try { fetch(cdsUrl, { method: "POST", headers: myHeaders, - body: JSON.stringify(json_request) + body: JSON.stringify(json_request), + signal: this.timeout(10).signal //Timeout set to 10 seconds }).then(response => { - this.consoleLog("Received response", types.info); + clearTimeout(this.timeout) response.json().then((fhirResponse) => { - console.log(fhirResponse); if (fhirResponse && fhirResponse.status) { this.consoleLog("Server returned status " + fhirResponse.status + ": " @@ -153,16 +157,17 @@ export default class RequestBuilder extends Component { } this.setState({ loading: false }); }) - }).catch(() => this.consoleLog("No response recieved from the server", types.error)); + }).catch(() => { + this.consoleLog("No response received from the server", types.error); + this.setState({loading: false}); + }); } catch (error) { this.setState({ loading: false }); this.consoleLog("Unexpected error occured", types.error) - // this.consoleLog(e.,types.error); if (error instanceof TypeError) { this.consoleLog(error.name + ": " + error.message, types.error); } } - } takeSuggestion(resource) { @@ -248,6 +253,12 @@ export default class RequestBuilder extends Component { "value": this.state.responseExpirationDays, "key": "responseExpirationDays" }, + "pimsUrl": { + "type": "input", + "display": "PIMS Server", + "value": this.state.pimsUrl, + "key": "pimsUrl" + }, "includeConfig": { "type": "check", "display": "Include Configuration in CRD Request", @@ -302,8 +313,10 @@ export default class RequestBuilder extends Component { patientId={this.state.patient.id} launchUrl={this.state.launchUrl} responseExpirationDays={this.state.responseExpirationDays} + pimsUrl={this.state.pimsUrl} ref={this.requestBox} loading={this.state.loading} + consoleLog={this.consoleLog} />
diff --git a/src/properties.json b/src/properties.json index 725130a4..7295b544 100644 --- a/src/properties.json +++ b/src/properties.json @@ -5,13 +5,14 @@ "server": "http://localhost:8090", "ehr_server": "http://localhost:8080/test-ehr/r4", "ehr_base": "http://localhost:8080/test-ehr/r4", - "cds_service":"http://localhost:8090/r4/cds-services", - "order_sign": "order-sign-crd", - "order_select": "order-select-crd", + "cds_service":"http://localhost:8090/cds-services", + "order_sign": "rems-order-sign", + "order_select": "rems-order-select", "user": "alice", "password": "alice", "public_keys": "http://localhost:3001/public_keys", "alt_drug": true, "launch_url": "http://localhost:3005/launch", - "response_expiration_days": 30 + "response_expiration_days": 30, + "pims_server": "http://localhost:5051/doctorOrders/api/addRx" } diff --git a/src/util/buildRequest.js b/src/util/buildRequest.js index 7fb9fff3..1095d5d6 100644 --- a/src/util/buildRequest.js +++ b/src/util/buildRequest.js @@ -44,7 +44,7 @@ export default function buildRequest(request, patient, ehrUrl, token, prefetch, "subject": "cds-service4" }, "context": { - "userId": "Practitioner/example", + "userId": request.requester.reference, "patientId": patient.id, "encounterId": "enc89284" } @@ -85,41 +85,8 @@ export default function buildRequest(request, patient, ehrUrl, token, prefetch, if(includePrefetch){ r4json.prefetch = {}; - prefetch.forEach((resource, key) => { - if (key === 'DeviceRequest') { - r4json.prefetch.deviceRequestBundle = { - "resourceType": "Bundle", - "type": "collection", - "entry": resource - }; - } else if (key === 'ServiceRequest') { - r4json.prefetch.serviceRequestBundle = { - "resourceType": "Bundle", - "type": "collection", - "entry": resource - }; - } else if(key === 'MedicationRequest') { - r4json.prefetch.medicationRequestBundle = { - "resourceType": "Bundle", - "type": "collection", - "entry": resource - }; - } else if (key === 'MedicationDispense') { - r4json.prefetch.medicationDispenseBundle = { - "resourceType": "Bundle", - "type": "collection", - "entry": resource - }; - } else if (key === 'Coverage') { - r4json.prefetch.coverageBundle = { - "resourceType": "Bundle", - "type": "collection", - "entry": resource - }; - } else { - console.error("Invalid prefetch key used: " + key + "."); - } + r4json.prefetch[key] = resource }); } diff --git a/src/util/buildScript.2017071.js b/src/util/buildScript.2017071.js new file mode 100644 index 00000000..62ca37df --- /dev/null +++ b/src/util/buildScript.2017071.js @@ -0,0 +1,299 @@ +/* 2017071 NCPDP SCRIPT Support */ + + function xmlAddTextNode(xmlDoc, parent, sectionName, value) { + var section = xmlDoc.createElement(sectionName); + var textNode = xmlDoc.createTextNode(value); + section.appendChild(textNode); + parent.appendChild(section); + } + + function buildNewRxName(doc, nameResource) { + var name = doc.createElement("Name"); + xmlAddTextNode(doc, name, "LastName", nameResource.family); + xmlAddTextNode(doc, name, "FirstName", nameResource.given[0]); + return name; + } + + function buildNewRxAddress(doc, addressResource) { + // console.log(addressResource); + var address = doc.createElement("Address"); + xmlAddTextNode(doc, address, "AddressLine1", addressResource.line[0]); + xmlAddTextNode(doc, address, "City", addressResource.city); + xmlAddTextNode(doc, address, "StateProvince", addressResource.state); + xmlAddTextNode(doc, address, "PostalCode", addressResource.postalCode); + xmlAddTextNode(doc, address, "Country", "US"); // assume US for now + return address; + } + + function buildNewRxPatient(doc, patientResource) { + // console.log(patientResource); + var patient = doc.createElement("Patient"); + var humanPatient = doc.createElement("HumanPatient"); + + // Patient Name + const patientNameResource = patientResource.name[0]; + humanPatient.appendChild(buildNewRxName(doc, patientNameResource)); + + // Patient Gender and Sex + var gender = "U"; // unknown + var patientResourceGender = patientResource.gender.toLowerCase(); + if (patientResourceGender === "male") { + gender = "M"; // male + } else if (patientResourceGender === "female") { + gender = "F"; // female + } else if (patientResourceGender === "other") { + gender = "N"; // non-binary + } + xmlAddTextNode(doc, humanPatient, "Gender", gender); + + // Patient Birth Date + var dateOfBirth = doc.createElement("DateOfBirth"); + xmlAddTextNode(doc, dateOfBirth, "Date", patientResource.birthDate); + humanPatient.appendChild(dateOfBirth); + + // Patient Address + const patientAddressResource = patientResource.address[0]; + humanPatient.appendChild(buildNewRxAddress(doc, patientAddressResource)); + + patient.appendChild(humanPatient); + return patient; + } + + function buildNewRxPrescriber(doc, practitionerResource) { + // console.log(practitionerResource); + var prescriber = doc.createElement("Prescriber"); + var nonVeterinarian = doc.createElement("NonVeterinarian"); + + // Prescriber Identifier + for (let i = 0; i < practitionerResource.identifier.length; i++) { + let id = practitionerResource.identifier[i]; + if ((id.system) && (id.system.includes("us-npi"))) { + var identification = doc.createElement("Identification"); + xmlAddTextNode(doc, identification, "NPI", id.value); + nonVeterinarian.appendChild(identification); + } + } + + // Prescriber Name + const practitionerNameResource = practitionerResource.name[0]; + nonVeterinarian.appendChild(buildNewRxName(doc, practitionerNameResource)); + + // Prescriber Address + const practitionerAddressResource = practitionerResource.address[0]; + nonVeterinarian.appendChild(buildNewRxAddress(doc, practitionerAddressResource)); + + // Prescriber Phone Number and Email + var communicationNumbers = doc.createElement("CommunicationNumbers"); + for (let i = 0; i < practitionerResource.telecom.length; i++) { + const telecom = practitionerResource.telecom[i]; + if (telecom.system === "phone") { + var primaryTelephone = doc.createElement("PrimaryTelephone"); + xmlAddTextNode(doc, primaryTelephone, "Number", telecom.value); + communicationNumbers.appendChild(primaryTelephone); + } else if (telecom.system === "email") { + xmlAddTextNode(doc, communicationNumbers, "ElectronicMail", telecom.value); + } + } + nonVeterinarian.appendChild(communicationNumbers) + + prescriber.appendChild(nonVeterinarian); + return prescriber; + } + + function quantityUnitOfMeasureFromDrugFormCode(dispenseRequest) { + // Switch on Orderable Drug Form codes from: + // https://terminology.hl7.org/5.0.0/CodeSystem-v3-orderableDrugForm.html + // Return NCPDP QuantityUnitOfMeasure + if (dispenseRequest.quantity.system.toLowerCase().endsWith("v3-orderableDrugForm".toLowerCase())) { + // is a subset of the codes, not a complete list + switch (dispenseRequest.quantity.code.toUpperCase()) { + case "APPFUL": // Applicatorful + case "FOAMAPL": // Foam with Applicator + case "VAGFOAMAPL": // Vaginal Foam with Applicator + case "VAGCRMAPL": // Vaginal Cream with Applicator + case "OINTAPL": // Ointment with Applicator + case "VAGOINTAPL": // Vaginal Ointment with Applicator + case "GELAPL": // Gel with Applicator + case "VGELAPL": // Vaginal Gel with Applicator + return "C62412"; // Applicator + //case "": + // return "C54564" // Blister + case "CAPLET": // Caplet + return "C64696"; // Caplet + case 'CAP': // Capsule + return "C48480"; // Capsule + //case "": + // return "C64933" // Each + //case "": + // return "C53499" // Film + //case "": + // return "C48155" // Gram + case "GUM": // Chewing Gum + return "C69124"; // Gum + //case "": + // return "C48499" // Implant + //case "": + // return "C62276" // Insert + //case "": + // return "C48504" // Kit + //case "": + // return "C120263" // Lancet + case "ORTROCHE": // Lozenge/Oral Troche + return "C48506"; // Lozenge + //case "": + // return "C28254" // Milliliter + //case "": + // return "C48521" // Packet + case "PAD": // Pad + case "MEDPAD": // Medicated Pad + return "C65032"; // Pad + case "PATCH": // Patch + case "TPATCH": // Transdermal Patch + case "TPATH16": // 16 Hour Transdermal Patch + case "TPATH24": // 24 Hour Transdermal Patch + case "TPATH2WK": // Biweekly Transdermal Patch + case "TPATH72": // 72 Hour Transdermal Patch + case "TPATHWK": // Weekly Hour Transdermal Patch + return "C48524"; // Patch + //case "": + // return "C120216" // Pen Needle + //case "": + // return "C62609" // Ring + // case "": + // return "C53502" // Sponge + //case "": + // return "C53503" // Stick + //case "": + // return "C48538" // Strip + case "SUPP": // Suppository + case "RECSUPP": // Rectal Suppository + case "URETHSUPP": // Urethral Suppository + case "VAGSUPP": // Vaginal Suppository + return "C48539"; // Suppository + case "SWAB": // Swab + case "MEDSWAB": // Medicated Swab + return "C53504"; // Swab + case "TAB": // Tablet + case "ORTAB": // Oral Tablet + case "BUCTAB": // Buccal Tablet + case "SRBUCTAB": // Sustained Release Buccal Tablet + case "CHEWTAB": // Chewable Tablet + case "CPTAB": // Coated Particles Tablet + case "DISINTTAB": // Disintegrating Tablet + case "DRTAB": // Delayed Release Tablet + case "ECTAB": // Enteric Coated Tablet + case "ERECTTAB": // Extended Release Enteric Coated Tablet + case "ERTAB": // Extended Release Tablet + case "ERTAB12": // 12 Hour Extended Release Tablet + case "ERTAB24": // 24 Hour Extended Release Tablet + case "SLTAB": // Sublingual Tablet + case "VAGTAB": // Vaginal Tablet + return "C48542"; // Tablet + //case "": + // return "C48548" // Troche + case "WAFER": // Wafer + return "C48552"; // Wafer + default: + return "C38046"; // Unspecified + } + } + return "C38046"; // unspecified + } + + function buildNewRxMedication(doc, medicationRequestResource) { + // console.log(medicationRequestResource); + var medicationPrescribed = doc.createElement("MedicationPrescribed"); + + // Medication Product + var drugCoded = doc.createElement("DrugCoded"); + + // loop through the coding values and find the ndc code and the rxnorm code + const medicationCodingList = medicationRequestResource.medicationCodeableConcept.coding; + for (let i = 0; i < medicationCodingList.length; i++) { + const coding = medicationCodingList[i]; + const system = coding.system.toLowerCase(); + + if (system.endsWith("rxnorm")) { + // Medication Drug Description + xmlAddTextNode(doc, medicationPrescribed, "DrugDescription", coding.display); + + } else if (system.endsWith("ndc")) { + // Medication Drug Code + var productCode = doc.createElement("ProductCode"); + xmlAddTextNode(doc, productCode, "Code", coding.code); + xmlAddTextNode(doc, productCode, "Qualifier", "ND"); // National Drug Code (NDC) + drugCoded.appendChild(productCode); + } + } + + medicationPrescribed.appendChild(drugCoded); + + // Medication Quantity + const dispenseRequest = medicationRequestResource.dispenseRequest; + var quantity = doc.createElement("Quantity"); + xmlAddTextNode(doc, quantity, "Value", dispenseRequest.quantity.value); + xmlAddTextNode(doc, quantity, "CodeListQualifier", 38); // Original Quantity + var quantityUnitOfMeasure = doc.createElement("QuantityUnitOfMeasure"); + xmlAddTextNode(doc, quantityUnitOfMeasure, "Code", quantityUnitOfMeasureFromDrugFormCode(dispenseRequest)); + quantity.appendChild(quantityUnitOfMeasure); + medicationPrescribed.appendChild(quantity); + + // Medication Written Date + var writtenDate = doc.createElement("WrittenDate"); + xmlAddTextNode(doc, writtenDate, "Date", medicationRequestResource.authoredOn); + medicationPrescribed.appendChild(writtenDate); + + // Medication Substitutions (0 - None) + xmlAddTextNode(doc, medicationPrescribed, "Substitutions", 0); + + // Medication NumberOfRefills (0 - None) + xmlAddTextNode(doc, medicationPrescribed, "NumberOfRefills", dispenseRequest.numberOfRepeatsAllowed); + + // Medication Sig + var sig = doc.createElement("Sig"); + xmlAddTextNode(doc, sig, "SigText", medicationRequestResource.dosageInstruction[0].text); + medicationPrescribed.appendChild(sig); + + // Medication REMS + // A - Prescriber has checked REMS and the prescriber's actions have been completed. + // B - Prescriber has checked REMS and the prescriber's actions are not yet completed. + // N - Prescriber has not checked REMS. + xmlAddTextNode(doc, medicationPrescribed, "PrescriberCheckedREMS", "B"); + + return medicationPrescribed; + } + + export default function buildNewRxRequest(patientResource, practitionerResource, medicationRequestResource) { + // console.log(medicationRequestResource); + var doc = document.implementation.createDocument("", "", null); + var message = doc.createElement("Message"); + + // Header + var header = doc.createElement("Header"); + // generate the message id (just get the milliseconds since epoch and use that) + const d1 = new Date(); + const messageIdValue = d1.getTime(); + // console.log(messageIdValue); + xmlAddTextNode(doc, header, "MessageID", messageIdValue); + message.appendChild(header); + + // Body + var body = doc.createElement("Body"); + var newRx = doc.createElement("NewRx"); + + // Patient + newRx.appendChild(buildNewRxPatient(doc, patientResource)); + + // Prescriber + newRx.appendChild(buildNewRxPrescriber(doc, practitionerResource)); + + // Medication + newRx.appendChild(buildNewRxMedication(doc, medicationRequestResource)); + + body.appendChild(newRx); + message.appendChild(body); + + doc.appendChild(message); + + return doc; + } \ No newline at end of file diff --git a/src/util/buildScript.2022071.js b/src/util/buildScript.2022071.js new file mode 100644 index 00000000..3ec9c2b5 --- /dev/null +++ b/src/util/buildScript.2022071.js @@ -0,0 +1,310 @@ +/* 2022071 NCPDP SCRIPT Support */ + + function xmlAddTextNode(xmlDoc, parent, sectionName, value) { + var section = xmlDoc.createElement(sectionName); + var textNode = xmlDoc.createTextNode(value); + section.appendChild(textNode); + parent.appendChild(section); + } + + function buildNewRxName(doc, nameResource) { + var names = doc.createElement("Names"); + var name = doc.createElement("Name"); + xmlAddTextNode(doc, name, "LastName", nameResource.family); + xmlAddTextNode(doc, name, "FirstName", nameResource.given[0]); + names.appendChild(name); + return names; + } + + function buildNewRxAddress(doc, addressResource) { + // console.log(addressResource); + var address = doc.createElement("Address"); + xmlAddTextNode(doc, address, "AddressLine1", addressResource.line[0]); + xmlAddTextNode(doc, address, "City", addressResource.city); + xmlAddTextNode(doc, address, "StateProvince", addressResource.state); + xmlAddTextNode(doc, address, "PostalCode", addressResource.postalCode); + xmlAddTextNode(doc, address, "Country", "US"); // assume US for now + return address; + } + + function buildNewRxPatient(doc, patientResource) { + // console.log(patientResource); + var patient = doc.createElement("Patient"); + var humanPatient = doc.createElement("HumanPatient"); + + // Patient Name + const patientNameResource = patientResource.name[0]; + humanPatient.appendChild(buildNewRxName(doc, patientNameResource)); + + // Patient Gender and Sex + var genderAndSex = doc.createElement("GenderAndSex"); + var gender = "U"; // unknown + var patientResourceGender = patientResource.gender.toLowerCase(); + if (patientResourceGender === "male") { + gender = "M"; // male + } else if (patientResourceGender === "female") { + gender = "F"; // female + } else if (patientResourceGender === "other") { + gender = "N"; // non-binary + } + xmlAddTextNode(doc, genderAndSex, "AdministrativeGender", gender); + humanPatient.appendChild(genderAndSex); + + // Patient Birth Date + var dateOfBirth = doc.createElement("DateOfBirth"); + xmlAddTextNode(doc, dateOfBirth, "Date", patientResource.birthDate); + humanPatient.appendChild(dateOfBirth); + + // Patient Address + const patientAddressResource = patientResource.address[0]; + humanPatient.appendChild(buildNewRxAddress(doc, patientAddressResource)); + + patient.appendChild(humanPatient); + return patient; + } + + function buildNewRxPrescriber(doc, practitionerResource) { + // console.log(practitionerResource); + var prescriber = doc.createElement("Prescriber"); + var nonVeterinarian = doc.createElement("NonVeterinarian"); + + // Prescriber Identifier + for (let i = 0; i < practitionerResource.identifier.length; i++) { + let id = practitionerResource.identifier[i]; + if ((id.system) && (id.system.includes("us-npi"))) { + var identification = doc.createElement("Identification"); + xmlAddTextNode(doc, identification, "NPI", id.value); + nonVeterinarian.appendChild(identification); + } + } + + // Prescriber Name + const practitionerNameResource = practitionerResource.name[0]; + nonVeterinarian.appendChild(buildNewRxName(doc, practitionerNameResource)); + + // Prescriber Address + const practitionerAddressResource = practitionerResource.address[0]; + nonVeterinarian.appendChild(buildNewRxAddress(doc, practitionerAddressResource)); + + // Prescriber Phone Number and Email + var communicationNumbers = doc.createElement("CommunicationNumbers"); + for (let i = 0; i < practitionerResource.telecom.length; i++) { + const telecom = practitionerResource.telecom[i]; + if (telecom.system === "phone") { + var primaryTelephone = doc.createElement("PrimaryTelephone"); + xmlAddTextNode(doc, primaryTelephone, "Number", telecom.value); + communicationNumbers.appendChild(primaryTelephone); + } else if (telecom.system === "email") { + xmlAddTextNode(doc, communicationNumbers, "ElectronicMail", telecom.value); + } + } + nonVeterinarian.appendChild(communicationNumbers) + + prescriber.appendChild(nonVeterinarian); + return prescriber; + } + + function quantityUnitOfMeasureFromDrugFormCode(dispenseRequest) { + // Switch on Orderable Drug Form codes from: + // https://terminology.hl7.org/5.0.0/CodeSystem-v3-orderableDrugForm.html + // Return NCPDP QuantityUnitOfMeasure + if (dispenseRequest.quantity.system.toLowerCase().endsWith("v3-orderableDrugForm".toLowerCase())) { + // is a subset of the codes, not a complete list + switch (dispenseRequest.quantity.code.toUpperCase()) { + case "APPFUL": // Applicatorful + case "FOAMAPL": // Foam with Applicator + case "VAGFOAMAPL": // Vaginal Foam with Applicator + case "VAGCRMAPL": // Vaginal Cream with Applicator + case "OINTAPL": // Ointment with Applicator + case "VAGOINTAPL": // Vaginal Ointment with Applicator + case "GELAPL": // Gel with Applicator + case "VGELAPL": // Vaginal Gel with Applicator + return "C62412"; // Applicator + //case "": + // return "C54564" // Blister + case "CAPLET": // Caplet + return "C64696"; // Caplet + case 'CAP': // Capsule + return "C48480"; // Capsule + //case "": + // return "C64933" // Each + //case "": + // return "C53499" // Film + //case "": + // return "C48155" // Gram + case "GUM": // Chewing Gum + return "C69124"; // Gum + //case "": + // return "C48499" // Implant + //case "": + // return "C62276" // Insert + //case "": + // return "C48504" // Kit + //case "": + // return "C120263" // Lancet + case "ORTROCHE": // Lozenge/Oral Troche + return "C48506"; // Lozenge + //case "": + // return "C28254" // Milliliter + //case "": + // return "C48521" // Packet + case "PAD": // Pad + case "MEDPAD": // Medicated Pad + return "C65032"; // Pad + case "PATCH": // Patch + case "TPATCH": // Transdermal Patch + case "TPATH16": // 16 Hour Transdermal Patch + case "TPATH24": // 24 Hour Transdermal Patch + case "TPATH2WK": // Biweekly Transdermal Patch + case "TPATH72": // 72 Hour Transdermal Patch + case "TPATHWK": // Weekly Hour Transdermal Patch + return "C48524"; // Patch + //case "": + // return "C120216" // Pen Needle + //case "": + // return "C62609" // Ring + // case "": + // return "C53502" // Sponge + //case "": + // return "C53503" // Stick + //case "": + // return "C48538" // Strip + case "SUPP": // Suppository + case "RECSUPP": // Rectal Suppository + case "URETHSUPP": // Urethral Suppository + case "VAGSUPP": // Vaginal Suppository + return "C48539"; // Suppository + case "SWAB": // Swab + case "MEDSWAB": // Medicated Swab + return "C53504"; // Swab + case "TAB": // Tablet + case "ORTAB": // Oral Tablet + case "BUCTAB": // Buccal Tablet + case "SRBUCTAB": // Sustained Release Buccal Tablet + case "CHEWTAB": // Chewable Tablet + case "CPTAB": // Coated Particles Tablet + case "DISINTTAB": // Disintegrating Tablet + case "DRTAB": // Delayed Release Tablet + case "ECTAB": // Enteric Coated Tablet + case "ERECTTAB": // Extended Release Enteric Coated Tablet + case "ERTAB": // Extended Release Tablet + case "ERTAB12": // 12 Hour Extended Release Tablet + case "ERTAB24": // 24 Hour Extended Release Tablet + case "SLTAB": // Sublingual Tablet + case "VAGTAB": // Vaginal Tablet + return "C48542"; // Tablet + //case "": + // return "C48548" // Troche + case "WAFER": // Wafer + return "C48552"; // Wafer + default: + return "C38046"; // Unspecified + } + } + return "C38046"; // unspecified + } + + function buildNewRxMedication(doc, medicationRequestResource) { + // console.log(medicationRequestResource); + var medicationPrescribed = doc.createElement("MedicationPrescribed"); + + // Medication Product + var product = doc.createElement("Product"); + var drugCoded = doc.createElement("DrugCoded"); + + // loop through the coding values and find the ndc code and the rxnorm code + const medicationCodingList = medicationRequestResource.medicationCodeableConcept.coding; + for (let i = 0; i < medicationCodingList.length; i++) { + const coding = medicationCodingList[i]; + const system = coding.system.toLowerCase(); + + if (system.endsWith("rxnorm")) { + // Medication Drug Description + xmlAddTextNode(doc, medicationPrescribed, "DrugDescription", coding.display); + + // Medication Drug Code + var productCode = doc.createElement("ProductCode"); + xmlAddTextNode(doc, productCode, "Code", coding.code); + xmlAddTextNode(doc, productCode, "Qualifier", "SBD"); // RxNorm Semantic Branded Drug + drugCoded.appendChild(productCode); + + } else if (system.endsWith("ndc")) { + // Medication NDC + xmlAddTextNode(doc, drugCoded, "NDC", coding.code); // 10-digit number + } + } + + product.appendChild(drugCoded); + medicationPrescribed.appendChild(product); + + // Medication Quantity + const dispenseRequest = medicationRequestResource.dispenseRequest; + var quantity = doc.createElement("Quantity"); + xmlAddTextNode(doc, quantity, "Value", dispenseRequest.quantity.value); + xmlAddTextNode(doc, quantity, "CodeListQualifier", 38); // Original Quantity + var quantityUnitOfMeasure = doc.createElement("QuantityUnitOfMeasure"); + xmlAddTextNode(doc, quantityUnitOfMeasure, "Code", quantityUnitOfMeasureFromDrugFormCode(dispenseRequest)); + quantity.appendChild(quantityUnitOfMeasure); + medicationPrescribed.appendChild(quantity); + + // Medication Written Date + var writtenDate = doc.createElement("WrittenDate"); + xmlAddTextNode(doc, writtenDate, "Date", medicationRequestResource.authoredOn); + medicationPrescribed.appendChild(writtenDate); + + // Medication Substitutions (0 - None) + var substitutions = doc.createElement("Substitutions"); + xmlAddTextNode(doc, substitutions, "Substitutions", 0); + medicationPrescribed.appendChild(substitutions); + + // Medication NumberOfRefills (0 - None) + xmlAddTextNode(doc, medicationPrescribed, "NumberOfRefills", dispenseRequest.numberOfRepeatsAllowed); + + // Medication Sig + var sig = doc.createElement("Sig"); + xmlAddTextNode(doc, sig, "SigText", medicationRequestResource.dosageInstruction[0].text); + medicationPrescribed.appendChild(sig); + + // Medication REMS + // A - Prescriber has checked REMS and the prescriber's actions have been completed. + // B - Prescriber has checked REMS and the prescriber's actions are not yet completed. + // N - Prescriber has not checked REMS. + xmlAddTextNode(doc, medicationPrescribed, "PrescriberCheckedREMS", "B"); + + return medicationPrescribed; + } + + export default function buildNewRxRequest(patientResource, practitionerResource, medicationRequestResource) { + // console.log(medicationRequestResource); + var doc = document.implementation.createDocument("", "", null); + var message = doc.createElement("Message"); + + // Header + var header = doc.createElement("Header"); + // generate the message id (just get the milliseconds since epoch and use that) + const d1 = new Date(); + const messageIdValue = d1.getTime(); + // console.log(messageIdValue); + xmlAddTextNode(doc, header, "MessageID", messageIdValue); + message.appendChild(header); + + // Body + var body = doc.createElement("Body"); + var newRx = doc.createElement("NewRx"); + + // Patient + newRx.appendChild(buildNewRxPatient(doc, patientResource)); + + // Prescriber + newRx.appendChild(buildNewRxPrescriber(doc, practitionerResource)); + + // Medication + newRx.appendChild(buildNewRxMedication(doc, medicationRequestResource)); + + body.appendChild(newRx); + message.appendChild(body); + + doc.appendChild(message); + + return doc; + } \ No newline at end of file diff --git a/src/util/data.js b/src/util/data.js index 0b967f23..efd2ee0a 100644 --- a/src/util/data.js +++ b/src/util/data.js @@ -48,6 +48,11 @@ const headers = { "display": "In Progress Form Expiration Days", "value": (process.env.FORM_EXPIRATION_DAYS ? process.env.FORM_EXPIRATION_DAYS : config.response_expiration_days), "key": "responseExpirationDays" + }, + "pimsUrl" : { + "display": "PIMS Server", + "value": (process.env.PIMS_URL ? process.env.PIMS_URL : config.pims_server), + "key": "pimsUrl" } }