Skip to content
Merged

Dev #170

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
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,38 @@ 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.

<div style="text-align:center">
<b>NOTICE</b>
</div>

This (software/technical data) was produced for the U. S. Government under Contract Number 75FCMC18D0047/75FCMC23D0004, and is subject to Federal Acquisition Regulation Clause 52.227-14, Rights in Data-General.


No other use other than that granted to the U. S. Government, or to those acting on behalf of the U. S. Government under that Clause is authorized without the express written permission of The MITRE Corporation.


For further information, please contact The MITRE Corporation, Contracts Management Office, 7515 Colshire Drive, McLean, VA 22102-7539, (703) 983-6000.

<div style="text-align:center">
<b>&copy;2025 The MITRE Corporation.</b>
</div>

<br />

Licensed under the Apache License, Version 2.0 (the "License"); use of this repository is permitted in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
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
5 changes: 4 additions & 1 deletion src/components/DisplayBox/DisplayBox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -355,10 +355,13 @@ const DisplayBox = props => {
{summarySection}
</Typography>

<div>
<Typography variant="div">{detailSection}</Typography>
</div>

{/* Forms */}
{linksSection.length !== 0 ? (
<div>
<Typography variant="div">{detailSection}</Typography>
<Typography color="text.secondary">Required Forms</Typography>
<List className={'links-section'}>{linksSection}</List>
</div>
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
Loading
Loading