Skip to content

Commit

Permalink
Merge branch 'main' into resource-store-refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
daniellrgn committed Oct 17, 2024
2 parents 98ad91a + 5838716 commit 5a36e63
Show file tree
Hide file tree
Showing 13 changed files with 317 additions and 36 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ RUN npm run build

RUN cp build/404.html build/index.html

CMD ["sh", "-c", "npm run build && cp build/404.html build/index.html && npm run start"]
CMD npm run build && cp build/404.html build/index.html && npm run start
9 changes: 9 additions & 0 deletions default.env
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ COMPOSE_FILE=docker-compose.yaml:docker-compose.static-ingress.yaml
# Fully qualified domain name; used to configure traefik ingress
# SERVER_NAME=foo.cirg.uw.edu

VITE_VERSION_STRING=

# Debug option for non-prod deployments using /build output
# (Works best in Chrome)
# DEBUG=1

###
### Client environment variables:
### Variables with VITE_ prefix will be available to the client
Expand All @@ -37,6 +43,9 @@ COMPOSE_FILE=docker-compose.yaml:docker-compose.static-ingress.yaml
VITE_EPIC_CLIENT_ID=
VITE_CERNER_CLIENT_ID=

# Bearer auth tokens
VITE_MEDITECH_BEARER_TOKEN=

# HIMSS 2024 only:
#VITE_EPIC_HIMSS_CLIENT_ID=
#VITE_ECW_HIMSS_CLIENT_ID=
Expand Down
2 changes: 2 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ interface ImportMetaEnv {
readonly VITE_ECW_HIMSS_CLIENT_ID: string
readonly VITE_EPIC_CLIENT_ID: string
readonly VITE_CERNER_CLIENT_ID: string
readonly VITE_MEDITECH_BEARER_TOKEN: string
readonly VITE_API_BASE: string
readonly VITE_VIEWER_BASE: string
readonly VITE_INTERMEDIATE_FHIR_SERVER_BASE: string
readonly VITE_VERSION_STRING: string
readonly DEV_SERVER_PORT: number
}

Expand Down
124 changes: 115 additions & 9 deletions src/lib/FetchAD.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
Spinner
} from 'sveltestrap';
import { createEventDispatcher } from 'svelte';
import type { ResourceRetrieveEvent } from './types';
import type { Attachment, DocumentReference } from 'fhir/r4';
import type { DocumentReferencePOLST, ResourceRetrieveEvent } from './types';
import type { Attachment, BundleEntry, CodeableConcept, Resource, ServiceRequest } from 'fhir/r4';
export let sectionKey: string = "Advance Directive";
Expand Down Expand Up @@ -258,6 +258,22 @@
}
}
async function fetchResourceByUrl(url: string) {
let result = await fetch(url, {
method: 'GET',
headers: { accept: 'application/json' }
}).then(function (response: any) {
if (!response.ok) {
// make the promise be rejected if we didn't get a 2xx response
//throw new Error('Unable to fetch ', { cause: response });
console.error(`Failed to fetch resource from ${url}`);
} else {
return response;
}
});
return result;
}
async function prepareIps() {
fetchError = '';
processing = true;
Expand All @@ -269,23 +285,113 @@
content = await contentResponse.json();
hostname = sources[selectedSource].url;
processing = false;
let resources = content.entry ? content.entry.map((e) => {
let resources: Array<DocumentReferencePOLST> = content.entry ? content.entry.map((e: BundleEntry) => {
return e.resource;
}) : [];
if (resources.length === 0) {
console.warn("No advance directives found for patient "+patient.id);
}
// If resource.category doesn't exist, ignore the DR - DR's w/out that are simply signature DR's.
// Lambda function to check if resource.category exists
const nonSignatureDR = (dr: DocumentReference) => dr.category !== undefined;
// Filter out resources that don't have a category
// Filter out DR's with 'status' == 'superseded'. In May '24 we included these,
// but they are just noise since IPS doesn't want to address the complexity of
// showing these in the UI behind something like a "history" element.
// Lambda function to check this:
const nonSupersededDR = (dr: DocumentReferencePOLST) => dr.status !== 'superseded';
// Filter out signature resources
resources = resources.filter(nonSupersededDR);
// Iterate over DR's that are for signatures.
// Get the date from when it was signed, and show that in the card for the DocumentReference
// that it applies to.
// That date will be in content.attachment.creation (Lisa to add the evening of 2024-07-17).
resources.forEach((dr: DocumentReferencePOLST) => {
if (dr.relatesTo && dr.relatesTo[0] && dr.relatesTo[0].code && dr.relatesTo[0].code == 'signs'
&& dr.relatesTo[0].target && dr.relatesTo[0].target.reference) {
// e.g. "DocumentReference/9fad9465-5c95-49cf-a8ff-c3b8d782894d"
let target = dr.relatesTo[0].target.reference;
target = target.substring(18);
let pdfSignDate = '(missing from content.attachment.creation in signature DocumentReference)'; // placeholder until Lisa's change
if (dr.content && dr.content[0] && dr.content[0].attachment && dr.content[0].attachment.creation){
pdfSignDate = dr.content[0].attachment.creation;
}
let resourceSigned = resources.find((item) => item.id == target);
if (resourceSigned) {
resourceSigned.pdfSignedDate = pdfSignDate; // pdfSignedDate is an ad-hoc property name
}
}
});
// July '24: unlike the May '24 connectathon, signature DR's now have resource.category defined.
// The ADI team is planning to add a code for these later, but for the time being they suggest
// that we identify these by: "description": "JWS of the FHIR Document",
// FIXME may be better to do this when we iterate for the TODO above.
const nonSignatureDR = (dr: DocumentReferencePOLST) => dr.description !== 'JWS of the FHIR Document';
// Filter out signature resources
resources = resources.filter(nonSignatureDR);
// if one of the DR's `content` elements has attachment.contentType = 'application/pdf', download if possible, put base64 of pdf in DR.content.attachment.data
const hasPdfContent = (dr: DocumentReference) => dr.content && dr.content.some(content => content.attachment && content.attachment.contentType === 'application/pdf' && !content.attachment.data);
const hasPdfContent = (dr: DocumentReferencePOLST) => dr.content && dr.content.some(content => content.attachment && content.attachment.contentType === 'application/pdf' && !content.attachment.data);
// if one of the DR's
const isPolst = (dr: DocumentReferencePOLST) => dr.type && dr.type.coding && dr.type.coding.some(coding => coding.system === 'http://loinc.org' && coding.code === '100821-8');
resources.forEach(async (dr: DocumentReferencePOLST) => {
// If this DR is a POLST, add the following chain of queries:
if (isPolst(dr)){
dr.isPolst = true;
// In the POLST find the content[] with format.code = "urn:hl7-org:pe:adipmo-structuredBody:1.1" (ADIPMO Structured Body Bundle),
const contentAdipmoBundleRef = dr.content.find(content => content.format && content.format.code && content.format.code === 'urn:hl7-org:pe:adipmo-structuredBody:1.1' && content.attachment && content.attachment.url && content.attachment.url.includes('Bundle'));
// look in that content's attachment.url, that will point at a Bundle (e.g. https://qa-rr-fhir2.maxmddirect.com/Bundle/10f4ff31-2c24-414d-8d70-de3a86bed808?_format=json)
const adipmoBundleUrl = contentAdipmoBundleRef?.attachment.url;
if (adipmoBundleUrl) {
// Pull that Bundle.
let adipmoBundle = await fetchResourceByUrl(adipmoBundleUrl);
let adipmoBundleJson = await adipmoBundle.json();
let serviceRequests = adipmoBundleJson.entry.filter((entry:BundleEntry) => entry.resource?.resourceType === 'ServiceRequest');
// TODO The next 4 sections should be generalized into an iteration, just need carve out for "detail" for 2.
// That bundle will include ServiceRequest resources; look for the one for CPR (loinc 100822-6)
const serviceRequestCpr = serviceRequests.find((resource: ServiceRequest) => {
return resource.category?.[0].coding?.[0].code === '100822-6';
});
dr.isCpr = serviceRequestCpr !== undefined;
dr.doNotPerformCpr = serviceRequestCpr.resource.doNotPerform;
// That bundle will include ServiceRequest resources; look for the one for "Initial portable medical treatment orders" (loinc 100823-4) aka Comfort Treatments
const serviceRequestComfortTreatments = serviceRequests.find((resource: ServiceRequest) => {
return resource.category?.[0].coding?.[0].code === '100823-4';
});
dr.isComfortTreatments = false;
if (serviceRequestComfortTreatments !== undefined) dr.isComfortTreatments = true;
dr.doNotPerformComfortTreatments = serviceRequestComfortTreatments.resource.doNotPerform && serviceRequestComfortTreatments.resource.doNotPerform == true;
resources.forEach(async (dr: DocumentReference) => {
dr.detailComfortTreatments = serviceRequestComfortTreatments.resource.note[0].text;
// That bundle will include ServiceRequest resources; look for the one for "Additional..." (loinc 100824-2)
const serviceRequestAdditionalTx = serviceRequests.find((resource: ServiceRequest) => {
return resource.category?.[0].coding?.[0].code === '100824-2';
});
dr.isAdditionalTx = false;
if (serviceRequestAdditionalTx !== undefined) dr.isAdditionalTx = true;
dr.doNotPerformAdditionalTx = serviceRequestAdditionalTx.resource.doNotPerform && serviceRequestAdditionalTx.resource.doNotPerform == true;
dr.detailAdditionalTx = serviceRequestAdditionalTx.resource.orderDetail[0].text;
// That bundle will include ServiceRequest resources; look for the one for "Medically assisted nutrition orders" (loinc 100825-9)
const serviceRequestMedicallyAssisted = serviceRequests.find((resource: ServiceRequest) => {
return resource.category?.[0].coding?.[0].code === '100825-9';
});
dr.isMedicallyAssisted = false;
if (serviceRequestMedicallyAssisted !== undefined) dr.isMedicallyAssisted = true;
dr.doNotPerformMedicallyAssisted = serviceRequestMedicallyAssisted.resource.doNotPerform && serviceRequestMedicallyAssisted.resource.doNotPerform == true;
dr.detailMedicallyAssisted = serviceRequestMedicallyAssisted.resource.orderDetail[0].text;
}
}
});
resources.forEach(async (dr: DocumentReferencePOLST) => {
if (hasPdfContent(dr)) {
const pdfContent = dr.content.find(content => content.attachment && content.attachment.contentType === 'application/pdf');
if (pdfContent && pdfContent.attachment && pdfContent.attachment.url) {
Expand Down
26 changes: 14 additions & 12 deletions src/lib/FetchTEFCA.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@
const resourceDispatch = createEventDispatcher<{ 'update-resources': ResourceRetrieveEvent }>();
let sources = {
Meld: {selected: false, destination: "Meld", url: "https://gw.interop.community/HeliosConnectathonSa/open"},
MeldOpen: {selected: false, destination: "MeldOpen", url: "https://gw.interop.community/HeliosConnectathonSa/open"},
JMCHelios: {selected: false, destination: "JMCHelios", url: "https://gw.interop.community/JMCHeliosSTISandbox/open"},
PublicHapi: {selected: false, destination: "PublicHapi", url: "http://hapi.fhir.org/baseR4"},
// Patient no longer exists
// PublicHapi: {selected: false, destination: "PublicHapi", url: "http://hapi.fhir.org/baseR4"},
OpenEpic: {selected: false, destination: "OpenEpic", url: ""},
CernerHelios: {selected: false, destination: "CernerHelios", url: ""}
}
let baseUrl = "https://concept01.ehealthexchange.org:52780/fhirproxy/r4";
let selectedSource = "Meld";
let selectedSource = "MeldOpen";
let method = 'destination'; // url or destination
let processing = false;
let fetchError = '';
Expand Down Expand Up @@ -74,7 +75,7 @@
zip = '';
phone = '';
gender = '';
if (selectedSource === 'Meld') {
if (selectedSource === 'MeldOpen') {
last = "BLACKSTONE";
first = "VERONICA";
gender = "Female";
Expand All @@ -95,6 +96,7 @@
gender = "Female";
dob = "2023-08-29";
} else if (selectedSource === 'PublicHapi') {
// Patient/test data no longer available
last = "Sanity";
first = "TestforPatientR4";
gender = "Male";
Expand All @@ -107,7 +109,7 @@
$: {
if (method) {
if (method === 'url' && selectedSource &&sources[selectedSource].url === "") {
if (method === 'url' && selectedSource && sources[selectedSource].url === "") {
selectedSource = "";
}
}
Expand Down Expand Up @@ -191,9 +193,9 @@
if (method === 'url') {
url = sources[selectedSource].url;
} else if (method === 'destination') {
headers['X-Request-Id'] = '5c92758f-79c8-4137-b104-9c0064205407',
headers['X-DESTINATION'] = selectedSource,
headers['X-POU'] = 'PUBHLTH'
headers['X-Request-Id'] = '21143678-7bd5-4caa-bdae-ee35a409d4f2';
headers['X-DESTINATION'] = selectedSource;
headers['X-POU'] = (selectedSource === 'OpenEpic' ? 'TREAT' : 'PUBHLTH');
}
let query = buildPatientSearchQuery();
Expand Down Expand Up @@ -227,9 +229,9 @@
if (method === 'url') {
url = sources[selectedSource].url;
} else if (method === 'destination') {
headers['X-Request-Id'] = '5c92758f-79c8-4137-b104-9c0064205407',
headers['X-DESTINATION'] = selectedSource,
headers['X-POU'] = 'PUBHLTH'
headers['X-Request-Id'] = '21143678-7bd5-4caa-bdae-ee35a409d4f2';
headers['X-DESTINATION'] = selectedSource;
headers['X-POU'] = (selectedSource === 'OpenEpic' ? 'TREAT' : 'PUBHLTH');
}
let results = await Promise.allSettled(
Expand Down Expand Up @@ -259,7 +261,7 @@
let resources = resultJson.filter(x => x.status == "fulfilled").map(x => x.value);
resources = resources.map((r) => {
if (r.resourceType === "Bundle") {
if (r.total == 0) {
if (r.total == 0 || !r.entry) {
return [];
} else {
return r.entry.map(e => e.resource);
Expand Down
8 changes: 6 additions & 2 deletions src/lib/FetchUrl.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
Row,
Spinner } from 'sveltestrap';
import { PATIENT_IPS, EXAMPLE_IPS, IPS_DEFAULT } from './config';
import { PATIENT_IPS, EXAMPLE_IPS, IPS_DEFAULT, BEARER_AUTHORIZATION } from './config';
import type { SHCRetrieveEvent, IPSRetrieveEvent } from './types';
import { createEventDispatcher } from 'svelte';
Expand Down Expand Up @@ -53,8 +53,12 @@
try {
let content;
let hostname;
let headers: any = { accept: 'application/fhir+json' };
if (summaryUrlValidated?.toString().includes('meditech')) {
headers['authorization'] = `Bearer ${BEARER_AUTHORIZATION['Meditech']}`
}
const contentResponse = await fetch(summaryUrlValidated!, {
headers: { accept: 'application/fhir+json' }
headers: headers
}).then(function(response) {
if (!response.ok) {
// make the promise be rejected if we didn't get a 2xx response
Expand Down
1 change: 1 addition & 0 deletions src/lib/IPSResourceCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const allowableResourceTypes = [
'DiagnosticReport',
'DocumentReference',
'Encounter',
'Flag',
'Immunization',
'Location',
'Media',
Expand Down
6 changes: 3 additions & 3 deletions src/lib/ResourceSelectorStores.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { ResourceHelper } from './ResourceHelper.js';
import type { IPSResourceCollection } from './IPSResourceCollection.js';
import type { IPSRetrieveEvent } from './types.js';
import type { CompositionSection } from 'fhir/r4';
import type { CompositionSection, BundleEntry } from 'fhir/r4';
import AdvanceDirective from './resource-templates/AdvanceDirective.svelte';
import AllergyIntolerance from './resource-templates/AllergyIntolerance.svelte';
Expand Down Expand Up @@ -137,8 +137,8 @@
if (content) {
content = resourceCollection.extendIPS(content);
}
content.entry.map(entry => {
if (entry.resource.extension) {
content.entry.map((entry: BundleEntry) => {
if (entry.resource && 'extension' in entry.resource && entry.resource.extension) {
entry.resource.extension = entry.resource.extension.filter(function(item) {
return item.url !== "http://hl7.org/fhir/StructureDefinition/narrativeLink";
});
Expand Down
12 changes: 10 additions & 2 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export const API_BASE = import.meta.env.VITE_API_BASE;

export const INTERMEDIATE_FHIR_SERVER_BASE = import.meta.env.VITE_INTERMEDIATE_FHIR_SERVER_BASE;

export const VERSION_STRING = import.meta.env.VITE_VERSION_STRING;

export const SOF_HOSTS = [
// {
// id: "epic-himss",
Expand Down Expand Up @@ -42,6 +44,10 @@ export const SOF_HOSTS = [
note: "Credentials provided"
}
];

export const BEARER_AUTHORIZATION = {
'Meditech': import.meta.env.VITE_MEDITECH_BEARER_TOKEN
}
export const SOF_REDIRECT_URI = '/create';
export const SOF_RESOURCES = [
'Patient',
Expand Down Expand Up @@ -91,12 +97,14 @@ export const VIEWER_BASE = new URL(
window.location.href
).toString();
export const PATIENT_IPS = {
'Dave deBronkart': 'https://fhir.ips-demo.dev.cirg.uw.edu/fhir/Patient/16501/$summary'
'Dave deBronkart': 'https://fhir.ips-demo.dev.cirg.uw.edu/fhir/Patient/16501/$summary',
'Peter Kieth Jordan': 'https://terminz.azurewebsites.net/fhir/Patient/$summary?profile=http://hl7.org/fhir/uv/ips/StructureDefinition/Bundle-uv-ips&identifier=https://standards.digital.health.nz/ns/nhi-id|NNJ9186&_format=json'
}
export const EXAMPLE_IPS = {
'Maria SEATTLE Gravitate': 'https://fhir.ips-demo.dev.cirg.uw.edu/fhir/Patient/14599/$summary',
'Martha Mum': 'https://hl7-ips-server.hl7.org/fhir/Patient/15/$summary',
'Peter Keith Jones': 'https://fhir.ips-demo.dev.cirg.uw.edu/fhir/Patient/11013/$summary',
'Meditech 1': 'https://dev-mtx-interop.meditech.com:443/v2/ips/STU1/Patient/f3b430be-1f8a-53d3-8261-4ffbafa05a61/$summary',
// 'Meditech 2': 'https://dev-mtx-interop.meditech.com:443/v2/ips/STU1/Patient/9bad7dc5-47ad-5022-82e7-0cb0aab13ee9/$summary', // Error returned
'Angela Roster': 'https://fhir.ips-demo.dev.cirg.uw.edu/fhir/Patient/10965/$summary',
'Horace Skelly': 'https://fhir.ips-demo.dev.cirg.uw.edu/fhir/Patient/11142/$summary',
'Anonymous': 'https://fhir.ips-demo.dev.cirg.uw.edu/fhir/Patient/10999/$summary',
Expand Down
Loading

0 comments on commit 5a36e63

Please sign in to comment.