Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,7 @@ VITE_USER = alice
VITE_HOOK_TO_SEND = patient-view
VITE_URL_FILTER = http://localhost:3000/*
VITE_USE_INTERMEDIARY = false
VITE_USE_PHARMACY_IN_PREFETCH = true
VITE_INTERMEDIARY = http://localhost:3003
VITE_DISABLE_MEDICATION_STATUS = false
VITE_PHARMACY_ID = pharm0111
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ Following are a list of modifiable paths:
| VITE_URL | `http://localhost:3000` | The base url of this app. Should be modified if the port or domain change. |
| VITE_USER | `alice` | The default user to login as when opening the app. |
| VITE_USE_INTERMEDIARY | false | When true, the app will send all CDS Hooks and REMS ETASU check calls to the intermediary defined in VITE_INTERMEDIARY. |
| VITE_INTERMEDIARY | `http://localhost:3030` | The base url of the intermediary. |
| VITE_INTERMEDIARY | `http:/localhost:3030` | The base url of the intermediary. |
| VITE_USE_PHARMACY_IN_PREFETCH | true | When true, the app will send pharmacy information to the rems admin in the CDS Hooks prefetch |
| VITE_PHARMACY_ID | `pharm0111` | The pharmacy ID to use in the CDS Hooks Prefetch |

# Data Rights
This repository has been forked from the [HL7-DaVinci/crd-request-generator](https://github.com/HL7-DaVinci/crd-request-generator) repository. As such, the following data rights apply to all changes made on this fork of the repository, starting with release 0.1 and onward.
Expand Down
74 changes: 51 additions & 23 deletions src/PrefetchTemplate.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
// Prefetch Template Source:
// https://build.fhir.org/ig/HL7/davinci-crd/hooks.html#prefetch
export class PrefetchTemplate {
static generatePrefetchMap() {
static generatePrefetchMap(settings = null) {
// If no settings provided, use defaults from data.js
const includePharmacy = settings?.includePharmacyInPreFetch ??
headerDefinitions.includePharmacyInPreFetch.default;
const pharmacyId = settings?.pharmacyId ??
headerDefinitions.pharmacyId.default;

const prefetchMap = new Map();

const PRACTITIONER_PREFETCH = new PrefetchTemplate('{{context.userId}}');

const REQUEST_PREFETCH = new PrefetchTemplate(
'MedicationRequest/{{context.medications.MedicationRequest.id}}'
);
const PATIENT_PREFETCH = new PrefetchTemplate('{{context.patientId}}');

const ALL_REQUESTS_PREFETCH = new PrefetchTemplate(
'MedicationRequest?subject={{context.patientId}}&_include=MedicationRequest:medication'
);

// prefetchMap.set("Coverage", COVERAGE_PREFETCH_QUERY);
// Core prefetch items (always included)
prefetchMap.set('request', REQUEST_PREFETCH);
prefetchMap.set('practitioner', PRACTITIONER_PREFETCH);
prefetchMap.set('patient', PATIENT_PREFETCH);
prefetchMap.set('medicationRequests', ALL_REQUESTS_PREFETCH);
// prefetchMap.set("ServiceRequest", SERVICE_REQUEST_BUNDLE);
// prefetchMap.set("Encounter", ENCOUNTER_BUNDLE);

// Optional pharmacy prefetch based on settings
if (includePharmacy && pharmacyId) {
const PHARMACY_PREFETCH = new PrefetchTemplate(`HealthcareService/${pharmacyId}`);
prefetchMap.set('pharmacy', PHARMACY_PREFETCH);
}

return prefetchMap;
}
Expand All @@ -47,27 +55,49 @@ export class PrefetchTemplate {
return paramElementMap;
}

static generateQueries(requestBundle, patientReference, userReference, ...prefetchKeys) {
static generateQueries(
requestBundle,
patientReference,
userReference,
settings = null,
...prefetchKeys
) {
const prefetchMap = PrefetchTemplate.generatePrefetchMap(settings);
const paramElementMap = PrefetchTemplate.generateParamElementMap();

var resolvedQueries = new Map();
for (var i = 0; i < prefetchKeys.length; i++) {
var prefetchKey = prefetchKeys[i];
if (!prefetchKey || !prefetchMap.has(prefetchKey)) continue;
var query = prefetchMap.get(prefetchKey).getQuery();
// Regex source: https://regexland.com/all-between-specified-characters/
var parametersToFill = query.match(/(?<={{).*?(?=}})/gs);
var resolvedQuery = query.slice();
for (var j = 0; j < parametersToFill.length; j++) {
var unresolvedParameter = parametersToFill[j];
var resolvedParameter;
if (requestBundle) {
resolvedParameter = PrefetchTemplate.resolveParameter(unresolvedParameter, requestBundle);
} else {
if (unresolvedParameter === 'context.patientId') {
resolvedParameter = patientReference;
} else if (unresolvedParameter === 'context.userId') {
resolvedParameter = userReference;

if (parametersToFill) {
for (var j = 0; j < parametersToFill.length; j++) {
var unresolvedParameter = parametersToFill[j];
var resolvedParameter;
if (requestBundle) {
resolvedParameter = PrefetchTemplate.resolveParameter(
unresolvedParameter,
requestBundle,
paramElementMap
);
} else {
if (unresolvedParameter === 'context.patientId') {
resolvedParameter = patientReference;
} else if (unresolvedParameter === 'context.userId') {
resolvedParameter = userReference;
}
}
if (resolvedParameter) {
resolvedQuery = resolvedQuery.replace(
'{{' + unresolvedParameter + '}}',
resolvedParameter
);
}
}
resolvedQuery = resolvedQuery.replace('{{' + unresolvedParameter + '}}', resolvedParameter);
}
resolvedQueries.set(prefetchKey, resolvedQuery);
}
Expand All @@ -89,8 +119,9 @@ export class PrefetchTemplate {
}
}

static resolveParameter(unresolvedParameter, requestBundle) {
static resolveParameter(unresolvedParameter, requestBundle, paramElementMap) {
const paramField = paramElementMap.get(unresolvedParameter);
if (!paramField) return null;
const resolvedParameter = PrefetchTemplate.getProp(requestBundle, paramField);
return resolvedParameter;
}
Expand All @@ -104,7 +135,4 @@ export class PrefetchTemplate {
getQuery() {
return this.query;
}
}

const prefetchMap = PrefetchTemplate.generatePrefetchMap();
const paramElementMap = PrefetchTemplate.generateParamElementMap();
}
18 changes: 16 additions & 2 deletions src/components/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,24 @@ const App = () => {
<Router>
<Routes>
<Route path="/launch" element={<Launch redirect={redirect} />} />
<Route path="/index" element={<ThemeProvider theme={theme}><Index/></ThemeProvider>} />
<Route
path="/index"
element={
<ThemeProvider theme={theme}>
<Index />
</ThemeProvider>
}
/>
<Route path="/register" element={<RegisterPage />} />
{/* forcibly enter backoffice workflow */}
<Route path="/index/backoffice" element={<ThemeProvider theme={theme}><Index backoffice={true}/></ThemeProvider> } />
<Route
path="/index/backoffice"
element={
<ThemeProvider theme={theme}>
<Index backoffice={true} />
</ThemeProvider>
}
/>
<Route
path="/patient-portal"
element={
Expand Down
43 changes: 25 additions & 18 deletions src/components/RequestBox/PatientSearchBar/PatientSearchBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,26 +45,33 @@ const PatientSearchBar = props => {
<Box className="search-box-container">
<Grid container>
<Grid item xs={9}>
<span className="search-header">
<p>Filter patient list</p>
<Autocomplete
className="search-box"
disablePortal
id="search-box"
onInputChange={(event, newInputValue) => {
setInput(newInputValue.toLowerCase());
}}
options={listOfPatients[0].map(item => item.name)}
renderInput={params => <TextField {...params} label="Search" />}
/>
<p>
Showing {getFilteredLength(input, listOfPatients)} of {props.searchablePatients.length}{' '}
records
</p>
</span>
<span className="search-header">
<p>Filter patient list</p>
<Autocomplete
className="search-box"
disablePortal
id="search-box"
onInputChange={(event, newInputValue) => {
setInput(newInputValue.toLowerCase());
}}
options={listOfPatients[0].map(item => item.name)}
renderInput={params => <TextField {...params} label="Search" />}
/>
<p>
Showing {getFilteredLength(input, listOfPatients)} of{' '}
{props.searchablePatients.length} records
</p>
</span>
</Grid>
<Grid item xs={3}>
<Button variant="contained" startIcon={<PeopleIcon />} onClick={() => { showAllPatients(); }} style={{padding:'10px','paddingLeft':'20px', 'paddingRight':'20px'}}>
<Button
variant="contained"
startIcon={<PeopleIcon />}
onClick={() => {
showAllPatients();
}}
style={{ padding: '10px', paddingLeft: '20px', paddingRight: '20px' }}
>
Select all Patients
</Button>
</Grid>
Expand Down
4 changes: 1 addition & 3 deletions src/components/RequestBox/RequestBox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,7 @@ const RequestBox = props => {

let userId = prefetchedResources?.practitioner?.id;
if (!userId) {
console.log(
'Practitioner not populated from prefetch, using user: ' + user
);
console.log('Practitioner not populated from prefetch, using user: ' + user);
userId = user;
}

Expand Down
61 changes: 31 additions & 30 deletions src/components/RequestDashboard/Home.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,36 +64,37 @@ const Home = props => {
}
return (
<div>
<Grid className={gridClass} item container justifyContent={'center'} alignItems={'center'}>
{section ? '' : <Grid item xs={3}></Grid>} {/* spacer */}
{renderMainButton(patientButton, <PersonIcon className={classes.mainIcon} />)}
{renderMainButton(taskButton, <AssignmentIcon className={classes.mainIcon} />)}
{renderMainButton(settingsButton, <SettingsIcon className={classes.mainIcon} />)}
{section ? (
<Grid className={classes.spacer} item xs={0}>
<span className={classes.titleIcon}>
<MedicalServicesIcon sx={{ fontSize: 48, verticalAlign: 'middle' }} />&nbsp;&nbsp;<strong>EHR</strong> Request Generator
</span>
</Grid>
) : (
<Grid item xs={3}></Grid>
)}
{/* spacer */}
{/** */}
{section ? (
<Grid className={classes.spacer} item xs={4}>
<span className={classes.loginIcon}>
<AccountBoxIcon sx={{ fontSize: 48, verticalAlign: 'middle' }} /> {token.name}
<Button variant="outlined" className={classes.whiteButton} onClick={logout}>
Logout
</Button>
</span>
</Grid>
) : (
<Grid item xs={3}></Grid>
)}
{/**/}
</Grid>
<Grid className={gridClass} item container justifyContent={'center'} alignItems={'center'}>
{section ? '' : <Grid item xs={3}></Grid>} {/* spacer */}
{renderMainButton(patientButton, <PersonIcon className={classes.mainIcon} />)}
{renderMainButton(taskButton, <AssignmentIcon className={classes.mainIcon} />)}
{renderMainButton(settingsButton, <SettingsIcon className={classes.mainIcon} />)}
{section ? (
<Grid className={classes.spacer} item xs={0}>
<span className={classes.titleIcon}>
<MedicalServicesIcon sx={{ fontSize: 48, verticalAlign: 'middle' }} />
&nbsp;&nbsp;<strong>EHR</strong> Request Generator
</span>
</Grid>
) : (
<Grid item xs={3}></Grid>
)}
{/* spacer */}
{/** */}
{section ? (
<Grid className={classes.spacer} item xs={4}>
<span className={classes.loginIcon}>
<AccountBoxIcon sx={{ fontSize: 48, verticalAlign: 'middle' }} /> {token.name}
<Button variant="outlined" className={classes.whiteButton} onClick={logout}>
Logout
</Button>
</span>
</Grid>
) : (
<Grid item xs={3}></Grid>
)}
{/**/}
</Grid>
</div>
);
};
Expand Down
7 changes: 6 additions & 1 deletion src/components/RequestDashboard/PatientSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ const PatientSection = props => {
return (
<div>
{state.startup ? (
<RequestBuilder globalState={state} dispatch={dispatch} client={props.client} userId={props.userId} />
<RequestBuilder
globalState={state}
dispatch={dispatch}
client={props.client}
userId={props.userId}
/>
) : (
<>Loading...</>
)}
Expand Down
35 changes: 19 additions & 16 deletions src/components/RequestDashboard/SettingsSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import { SettingsContext } from '../../containers/ContextProvider/SettingsProvid
const ENDPOINT = [ORDER_SIGN, ORDER_SELECT, PATIENT_VIEW, ENCOUNTER_START, REMS_ETASU];

const SettingsSection = props => {
const [state, dispatch, updateSetting, readSettings, saveSettings] = React.useContext(SettingsContext);
const [state, dispatch, updateSetting, readSettings, saveSettings] =
React.useContext(SettingsContext);

const fieldHeaders = Object.keys(headerDefinitions)
.map(key => ({ ...headerDefinitions[key], key }))
Expand Down Expand Up @@ -94,7 +95,7 @@ const SettingsSection = props => {
});
};

const clearResource =
const clearResource =
({ ehrUrl, access_token }, type) =>
() => {
console.log('Clear ' + type + 's from the EHR: ' + ehrUrl);
Expand Down Expand Up @@ -151,19 +152,19 @@ const SettingsSection = props => {
display: 'Clear EHR In-Progress Forms',
key: 'clearQuestionnaireResponses',
reset: clearResource,
parameter: 'QuestionnaireResponse'
parameter: 'QuestionnaireResponse'
},
{
display: 'Clear EHR Dispense Statuses',
key: 'clearMedicationDispenses',
reset: clearResource,
parameter: 'MedicationDispense'
parameter: 'MedicationDispense'
},
{
display: 'Clear EHR Tasks',
key: 'clearTasks',
reset: clearResource,
parameter: 'Task'
parameter: 'Task'
},
{
display: 'Reconnect EHR',
Expand All @@ -184,17 +185,19 @@ const SettingsSection = props => {
case 'input':
return (
<Grid key={key} item xs={6}>
{ ( (state['useDefaultUser'] && key === 'defaultUser') || key != 'defaultUser' ) ? (
<div>
<TextField
label={display}
variant="outlined"
value={state[key]}
onChange={event => updateSetting(key, event.target.value)}
sx={{ width: '100%' }}
/>
</div>
) : ('') }
{(state['useDefaultUser'] && key === 'defaultUser') || key != 'defaultUser' ? (
<div>
<TextField
label={display}
variant="outlined"
value={state[key]}
onChange={event => updateSetting(key, event.target.value)}
sx={{ width: '100%' }}
/>
</div>
) : (
''
)}
</Grid>
);
case 'check':
Expand Down
Loading