Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SMART on FHIR UI Coding Exercise #267

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions javascript/SMART-on-FHIR/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
Plan for implementation

In order to keep this as simple and streamlined as possible,
I won't be using any frameworks, with the exception
of jQuery since it makes it much faster to build up elements
in the DOM, and Bootstrap as it makes it much easier to set up
styling. I will be using JavaScript's build in fetch method to
process Ajax requests just to make it clear I know what's going
on under the hood.

I will be accessing the REST api directly; it wasn't clear from
the test assignment whether I was expected to make use of the FHIR
JavaScript client but again, this is another layer of complexity
that will get in the way of this faster implementation (I could easily
spend half of my time just reading through the documentation).

Fortunatly the REST api provided by Cerner’s open sandbox is very
straightforward.

It appears that I can get all of the data I need just from the
Patient URL to search for matching patients:

https://fhir-open.sandboxcerner.com/dstu2/0b8a0111-e8e6-4c26-a91c-5069cbc6b1ca/Patient?name=<name>

The patient details are pulled from the patient resource URL:

https://fhir-open.sandboxcerner.com/dstu2/0b8a0111-e8e6-4c26-a91c-5069cbc6b1ca/Patient/<id>

And finally the conditions can be found by searching with the patient ID:

https://fhir-open.sandboxcerner.com/dstu2/0b8a0111-e8e6-4c26-a91c-5069cbc6b1ca/Condition?patient=<patientId>

Final implementation will be a simple search box for searching for a patient.
Patient results will be displayed in a list, clicking on a patient's name will pull up their record.

Things I would implement if I had more time:
- a routing systema and history, so that navigation could be preserved and each request would have a corresponding URI
- a more robust testing system to make sure everything behaves as expected
- a more elegant interface, probably making use of a framework
- caching to reduce the number of redundant request to the REST api
- paging records -- currently only first 20 results are displayed for a search
- better data validation -- right now only checking for records marked "entered-in-error"
- better name handling -- right now the system only recognizes the most recent, active name
184 changes: 184 additions & 0 deletions javascript/SMART-on-FHIR/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<title>Elsevier Web App</title>

<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" rel="stylesheet">

<style>
.show-search {
font-size: larger;
font-weight: bolder;
}
</style>


</head>

<body>


<div role="main" class="container">
<div class="searching">
<form id="patient-search">
<fieldset>
<legend>Patient Search</legend>
<div class="form-label-group">
<label for="patient-search-field" class="sr-only">Search by name:</label>
<input type="search" id="patient-search-field" class="form-control" placeholder="Last name" required="required" autofocus="true">
<button class="btn btn-lg btn-primary btn-block">Search</button>
</div>
</fieldset>
</form>

<div id="patient-search-results">
</div>
</div>

<div class="details">
<a href="#" class="show-search">&laquo; Back to search results</a>
<h1 id="patient-name"></h1>
<p>Birthdate <strong id="patient-birth-date"></strong></p>
<p>Gender <strong id="patient-gender"></strong></p>
<div class="card">
<div class="card-header">
Conditions
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th class="sort-header" data-sort="name">Condition</th>
<th class="sort-header" data-sort="dateRecorded">Date First Recorded</th>
</tr>
</thead>
<tbody id="condition-rows">
</tbody>
</table>
</div>
</div>
</div>

</div>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js"></script>
<script src="js/api.js"></script>
<script>
API.baseURI = 'https://fhir-open.sandboxcerner.com/dstu2/0b8a0111-e8e6-4c26-a91c-5069cbc6b1ca'
SEARCH_URL = 'https://www.ncbi.nlm.nih.gov/pubmed/?term='
$(document).on('submit', '#patient-search', (event) => {
var $results = $('#patient-search-results')
event.stopPropagation()
event.preventDefault()
$results.empty()
getPatients($('#patient-search-field').val()).then(patients => {
var $patientList = $('<ul>').addClass('list-group')
patients.forEach((patient) => {
$('<li>')
.addClass('list-group-item')
.append(
$('<a>')
.attr('href','#patient-' + patient.id)
.addClass('patient-link')
.data('patient', patient.id)
.text(patient.name)
)
.appendTo($patientList)
})
$results.append($patientList)
})
})
const showSearch = () => {
$('.details').hide()
$('.searching').show()
}
showSearch() // initial state on startup
$(document).on('click', '.show-search', (event) => {
event.stopPropagation()
event.preventDefault()
showSearch()
})

var titleCase = (inp) => inp.replace(/\b(\w)(\w+)/gi, (match, head, tail) => head.toUpperCase() + tail.toLowerCase())

const dateFormat = (dt) => {
return dt.toLocaleDateString(
'en-US',
{
timeZone: 'UTC',
month: 'long',
day: '2-digit',
year: 'numeric'
}
)
}

$(document).on('click', '.patient-link', (event) => {
var patient, conditions, input=event.target
event.stopPropagation();
event.preventDefault();
getPatient($(input).data('patient')).then(data => {
patient = data
displayPatient(patient)
return getActiveConditions(patient)
}).then(data => {
conditions = data
displayConditions(conditions)
$('.searching').hide()
$('.details').show()
})
})

$(document).on('click', '.sort-header', (event) => {
var $header = $(event.target),
sort = $header.data('sort'),
rows = $('#condition-rows').data('rows')
event.stopPropagation();
event.preventDefault();
let sorted = rows.sort((a, b) => {
if (a[sort] < b[sort]) {
return -1
} else if (a[sort] === b[sort]) {
return 0
}
return 1
})
displayConditions(sorted)
})

const displayPatient = (patient) => {
$('#patient-name').text(patient.displayName)
$('#patient-birth-date').text(patient.birthDate ? dateFormat(patient.birthDate):'n/a')
$('#patient-gender').text(patient.gender ? titleCase(patient.gender) : 'n/a')
}

const displayConditions = conditions => {
$('#condition-rows')
.empty()
.data('rows', conditions)
conditions.forEach((condition) => {
$('<tr>')
.append(
$('<td>')
.text(condition.name)
.append(' ')
.append(
$('<a>')
.attr('href', SEARCH_URL + encodeURIComponent(condition.name))
.addClass('search-link float-right')
.text('Search')
)
)
.append(
$('<td>')
.text(condition.dateRecorded ? dateFormat(condition.dateRecorded) : 'n/a')
)
.appendTo('#condition-rows')
})
}
</script>
</body>
</html>
117 changes: 117 additions & 0 deletions javascript/SMART-on-FHIR/js/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
var API = API || {};


// default values, can be overridden in
// main document

API.baseURI = 'https://example.com/path/to/api'

const apiFetch = path => {
return fetch(
API.baseURI + path,
{
headers: {'Accept': 'application/json-fhir'}
}
).then(obj => obj.json())
}

const getPatients = q => {
const path = '/Patient?name=' + encodeURIComponent(q);
return new Promise(
resolve => {
apiFetch(path).then(
obj => {
let patients = [];
if (obj.total === 0) {
resolve([])
} else {
obj.entry.forEach(patient => patients.push({
name: patient.resource.name[0].text,
id: patient.resource.id,
gender: patient.resource.gender,
birthDate: patient.resource.birthDate ? new Date(Date.parse(patient.resource.birthDate)) : null
}))
}
resolve(patients);
}
)
}
)
}

const getPatient = id => {
const path = '/Patient/' + id;
return new Promise(
resolve => {
apiFetch(path).then(
obj => {
let patient = {},
curName = obj.name[0]
patient.id = obj.id
patient.first = curName.given[0]
if (curName.given.length > 1) {
// current implementation handles just one middle name
patient.middle = curName.given[1]
}
patient.displayName = curName.text
patient.last = curName.family[0]
patient.birthDate = obj.birthDate ? new Date(Date.parse(obj.birthDate)) : null
patient.gender = obj.gender
resolve(patient)
}
)
}
)
}

const getConditions = patient => {
if (patient.id) {
patient = patient.id
}
let path = '/Condition?patient=' + patient
return new Promise(
resolve => {
apiFetch(path).then(
obj => {
var conditions = [];
if (!obj.total) {
resolve([])
} else {
obj.entry.forEach(condition => conditions.push({
name: condition.resource.code.text,
dateRecorded: condition.resource.dateRecorded ? new Date(Date.parse(condition.resource.dateRecorded)) : null,
verificationStatus: condition.resource.verificationStatus,
started: condition.resource.onsetDateTime ? new Date(Date.parse(condition.resource.onsetDateTime)) : null,
ended: condition.resource.abatementDateTime ? new Date(Date.parse(condition.resource.abatementDateTime)): null
}))
}
resolve(conditions);
}
)
}
)
}

// was never a real condition
const isValidCondition = (condition, index, array) => (!condition.verificationStatus || condition.verificationStatus !== 'entered-in-error')

// no longer active
const isActiveCondition = (condition, index, array) => { return !condition.ended }

const getActiveConditions = patient => {
return new Promise(
resolve => {
getConditions(patient).then(
conditions => {
let validConds = conditions.filter(isValidCondition)
let activeConds = validConds.filter(isActiveCondition)
resolve(
conditions.filter(isActiveCondition).filter(isValidCondition)
)
}
)
}
)
}