Skip to content

Commit a4b58c1

Browse files
committed
Initial version of AD tab and support for DocumentReference AD resources
1 parent 1479cf3 commit a4b58c1

File tree

4 files changed

+444
-74
lines changed

4 files changed

+444
-74
lines changed

src/lib/AddFile.svelte

+8-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import FetchUrl from './FetchUrl.svelte';
2222
import FetchFile from './FetchFile.svelte';
2323
import FetchSoF from './FetchSoF.svelte';
24+
import FetchAD from './FetchAD.svelte';
2425
import ODHForm from './ODHForm.svelte';
2526
import ResourceSelector from './ResourceSelector.svelte';
2627
import { verify } from './shcDecoder.js';
@@ -346,10 +347,16 @@
346347
on:ips-retrieved={ async ({ detail }) => { stageRetrievedIPS(detail) } }>
347348
</FetchFile>
348349
</TabPane>
350+
<TabPane class="ad-tab" tabId="ad" style="padding-top:10px">
351+
<span class="ad-tab" slot="tab">Advance Directive Search</span>
352+
<FetchAD
353+
on:update-resources={ async ({ detail }) => { handleNewResources(detail) } }>
354+
</FetchAD>
355+
</TabPane>
349356
</TabContent>
350357
</AccordionItem>
351358
{#if resourcesToReview.length > 0}
352-
<AccordionItem active class="odh-data">
359+
<AccordionItem class="odh-data">
353360
<h5 slot="header" class="my-2">2. Add health-related occupational information</h5>
354361
<Label>It may be helpful to include information about the work you do in your medical summary</Label>
355362
<ODHForm bind:odhSection={odhData.section} bind:odhSectionResources={odhData.resources} />

src/lib/FetchAD.svelte

+315
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
<script lang="ts">
2+
import {
3+
Button,
4+
Col,
5+
FormGroup,
6+
Input,
7+
Label,
8+
Row,
9+
Spinner
10+
} from 'sveltestrap';
11+
12+
import type { ResourceRetrieveEvent } from './types';
13+
import { createEventDispatcher } from 'svelte';
14+
15+
const resourceDispatch = createEventDispatcher<{ 'update-resources': ResourceRetrieveEvent }>();
16+
17+
let defaultUrl = "https://fhir.ips-demo.dev.cirg.uw.edu/fhir";
18+
let processing = false;
19+
let fetchError = '';
20+
21+
let mrn = '';
22+
let first = '';
23+
let last = '';
24+
let dob = '';
25+
let address1 = '';
26+
let address2 = '';
27+
let city = '';
28+
let state = '';
29+
let zip = '';
30+
let phone = '';
31+
let gender:string = '';
32+
let genders: Record<string, any> = {
33+
"Female": 'female',
34+
"Male": 'male',
35+
"Other": 'other'
36+
};
37+
let states: Array<string> = [
38+
'AL','AK','AZ','AR','CA','CO','CT',
39+
'DC','DE','FL','GA','GU','HI','ID',
40+
'IL','IN','IA','KS','KY','LA','ME',
41+
'MD','MA','MI','MH','MN','MP','MS',
42+
'MO','MT','NE','NV','NH','NJ','NM',
43+
'NY','NC','ND','OH','OK','OR','PA',
44+
'PR','RI','SC','SD','TN','TX','UT',
45+
'VT','VA','VI','WA','WV','WI','WY'
46+
];
47+
48+
let result: ResourceRetrieveEvent = {
49+
resources: undefined
50+
};
51+
52+
let summaryUrlValidated: URL | undefined = undefined;
53+
$: {
54+
setSummaryUrlValidated(defaultUrl);
55+
}
56+
57+
function setSummaryUrlValidated(url: string) {
58+
try {
59+
summaryUrlValidated = new URL(url);
60+
} catch {
61+
summaryUrlValidated = undefined;
62+
}
63+
}
64+
65+
function constructPatient() {
66+
let patient = {
67+
resourceType: 'Patient',
68+
identifier: [
69+
{
70+
use: 'usual',
71+
type: {
72+
coding: [
73+
{
74+
system: 'http://terminology.hl7.org/CodeSystem/v2-0203',
75+
code: 'MR',
76+
display: 'Medical Record Number'
77+
}
78+
],
79+
text: 'Medical Record Number'
80+
},
81+
system: 'http://hospital.smarthealthit.org',
82+
value: mrn
83+
}
84+
],
85+
active: true,
86+
name: [
87+
{
88+
family: last,
89+
given: [first]
90+
}
91+
],
92+
telecom: [
93+
{
94+
system: 'phone',
95+
value: phone,
96+
use: 'home'
97+
}
98+
],
99+
gender: genders[gender],
100+
birthDate: dob,
101+
address: [
102+
{
103+
line: (address2 ? [address1, address2] : [address1]),
104+
city: city,
105+
state: state,
106+
postalCode: zip,
107+
country: 'US'
108+
}
109+
]
110+
};
111+
112+
return patient;
113+
}
114+
115+
function buildPatientSearchQuery() {
116+
let query = "?";
117+
query += dob ? `birthdate=${dob}&` : '';
118+
query += first ? `given=${first}&` : '';
119+
query += last ? `family=${last}&` : '';
120+
query += gender ? `gender=${genders[gender]}&` : '';
121+
query += mrn ? `identifier=${mrn}&` : '';
122+
query += phone ? `phone=${phone}&` : '';
123+
query += address1 || address2 ? `address=${(address1+' '+address2).trim().replaceAll(' ', '+')}&` : '';
124+
query += city ? `address-city=${city}&` : '';
125+
query += state ? `address-state=${state}&` : '';
126+
query += zip ? `address-postalcode=${zip}&` : '';
127+
return query.substring(0, query.length - 1);
128+
}
129+
130+
async function fetchPatient(patient: any) {
131+
let result;
132+
try {
133+
result = await fetch(`${defaultUrl}/Patient/$match`, {
134+
method: 'POST',
135+
headers: { accept: 'application/json' },
136+
body: JSON.stringify(patient)
137+
}).then(function (response: any) {
138+
if (!response.ok) {
139+
// make the promise be rejected if we didn't get a 2xx response
140+
throw new Error('Unable to fetch patient data', { cause: response });
141+
} else {
142+
return response;
143+
}
144+
});
145+
} catch (e) {
146+
let query = buildPatientSearchQuery();
147+
result = await fetch(`${defaultUrl}/Patient${query}`, {
148+
method: 'GET',
149+
headers: { accept: 'application/json' },
150+
}).then(function (response: any) {
151+
if (!response.ok) {
152+
// make the promise be rejected if we didn't get a 2xx response
153+
throw new Error('Unable to fetch patient data', { cause: response });
154+
} else {
155+
return response;
156+
}
157+
});
158+
}
159+
let body = await result.json();
160+
if (body.resourceType == 'Bundle' && (body.total == 0 || body.entry.length === 0)) {
161+
throw new Error('Unable to find patient');
162+
}
163+
let patient_response = body.entry[body.entry.length - 1].resource;
164+
return patient_response;
165+
}
166+
167+
function buildAdvanceDirectiveSearchQuery(patient_id: any) {
168+
let query = "?";
169+
query += patient_id ? `subject=${patient_id}&` : '';
170+
return query.substring(0, query.length - 1);
171+
}
172+
173+
async function fetchAdvanceDirective(patient: any) {
174+
let query = buildAdvanceDirectiveSearchQuery(patient);
175+
return result = await fetch(`${defaultUrl}/DocumentReference${query}`, {
176+
method: 'GET',
177+
headers: { accept: 'application/json' }
178+
}).then(function (response: any) {
179+
if (!response.ok) {
180+
// make the promise be rejected if we didn't get a 2xx response
181+
throw new Error('Unable to fetch advance directive data', { cause: response });
182+
} else {
183+
return response;
184+
}
185+
});
186+
}
187+
188+
async function prepareIps() {
189+
fetchError = '';
190+
processing = true;
191+
try {
192+
let content;
193+
let hostname;
194+
const patient = await fetchPatient(constructPatient());
195+
const contentResponse = await fetchAdvanceDirective(patient.id);
196+
content = await contentResponse.json();
197+
hostname = defaultUrl;
198+
processing = false;
199+
let resources = content.entry ? content.entry.map((e) => {
200+
return e.resource;
201+
}) : [];
202+
if (resources.length === 0) {
203+
console.warn("No advance directives found for patient "+patient.id);
204+
}
205+
resources.unshift(patient);
206+
207+
result = {
208+
resources: resources,
209+
source: hostname
210+
};
211+
console.log(resources);
212+
resourceDispatch('update-resources', result);
213+
} catch (e) {
214+
processing = false;
215+
console.log('Failed', e);
216+
fetchError = 'Error preparing IPS';
217+
}
218+
}
219+
</script>
220+
221+
<form on:submit|preventDefault={() => prepareIps()}>
222+
<FormGroup>
223+
<Row>
224+
<Row class="mx-2">
225+
<Input type="radio" bind:group={defaultUrl} value="https://fhir.ips-demo.dev.cirg.uw.edu/fhir" label="WA Verify+ Demo Server" />
226+
</Row>
227+
</Row>
228+
</FormGroup>
229+
{#if defaultUrl}
230+
<FormGroup>
231+
<Label>Enter your information to fetch an advance directive</Label>
232+
<p class="text-secondary"><em>WA Verify+ does not save this information</em></p>
233+
<Row cols={{ md: 2, sm: 1 }}>
234+
<Col>
235+
<Label>Name</Label>
236+
<FormGroup style="font-size:small" class="text-secondary" label="First">
237+
<Input type="text" bind:value={first} />
238+
</FormGroup>
239+
<FormGroup style="font-size:small" class="text-secondary" label="Last">
240+
<Input type="text" bind:value={last} />
241+
</FormGroup>
242+
<Label>Demographics</Label>
243+
<FormGroup style="font-size:small" class="text-secondary" label="Date of Birth">
244+
<Input type="date" bind:value={dob} placeholder={dob} style="width: 165px"/>
245+
</FormGroup>
246+
<FormGroup style="font-size:small" class="text-secondary" label="Gender">
247+
<!-- <Label>Gender</Label> -->
248+
<Input type="select" bind:value={gender} style="width: 100px">
249+
{#each Object.keys(genders) as full}
250+
<option value={full} style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">
251+
{full}
252+
</option>
253+
{/each}
254+
</Input>
255+
</FormGroup>
256+
<FormGroup>
257+
<Label>MRN</Label>
258+
<Input type="text" bind:value={mrn} style="width: 165px"/>
259+
</FormGroup>
260+
<Label>Contact Information</Label>
261+
<FormGroup style="font-size:small" class="text-secondary" label="Phone">
262+
<Input type="tel" bind:value={phone} style="width: 165px"/>
263+
</FormGroup>
264+
<Label>Address</Label>
265+
<FormGroup style="font-size:small" class="text-secondary" label="Address Line 1">
266+
<Input type="text" bind:value={address1} />
267+
</FormGroup>
268+
<FormGroup style="font-size:small" class="text-secondary" label="Address Line 2 (Optional)">
269+
<Input type="text" bind:value={address2} />
270+
</FormGroup>
271+
<FormGroup style="font-size:small" class="text-secondary" label="City">
272+
<Input type="text" bind:value={city} />
273+
</FormGroup>
274+
<Row>
275+
<Col xs="auto">
276+
<FormGroup style="font-size:small" class="text-secondary" label="State">
277+
<Input type="select" bind:value={state} style="width: 80px">
278+
{#each states as state}
279+
<option value={state} style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">
280+
{state}
281+
</option>
282+
{/each}
283+
</Input>
284+
</FormGroup>
285+
</Col>
286+
<Col>
287+
<FormGroup style="font-size:small" class="text-secondary" label="Zip">
288+
<Input type="text" bind:value={zip} style="width:90px"/>
289+
</FormGroup>
290+
</Col>
291+
</Row>
292+
</Col>
293+
</Row>
294+
</FormGroup>
295+
296+
<Row>
297+
<Col xs="auto">
298+
<Button color="primary" style="width:fit-content" disabled={processing} type="submit">
299+
{#if !processing}
300+
Fetch Data
301+
{:else}
302+
Fetching...
303+
{/if}
304+
</Button>
305+
</Col>
306+
{#if processing}
307+
<Col xs="auto" class="d-flex align-items-center px-0">
308+
<Spinner color="primary" type="border" size="md"/>
309+
</Col>
310+
{/if}
311+
</Row>
312+
{/if}
313+
</form>
314+
315+
<span class="text-danger">{fetchError}</span>

0 commit comments

Comments
 (0)