Summary Table

Categories Total Count
PII 0
URL 0
DNS 0
EKL 0
IP 0
PORT 0
VsID 0
CF 0
AI 0
VPD 0
PL 0
Other 0

File Content

import { Injectable } from '@angular/core';
import { throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { PatientIdentity, PatientQueryParams, QueryParams, FilterOps, Observation } from '../models/patientModels';
import { AppState } from '../models/state';

import { Store, select, Action } from '@ngrx/store';
import * as patientActions from '../actions/patient.action';
import * as sessionActions from '../actions/session.action';
import { environment } from '../../environments/environment';
import * as _ from 'lodash';
import * as DP from '../utilities/dataProcessing';
import { SummaryType } from '../models/queryParams';

@Injectable()
export class EhrService {

private patientID: PatientIdentity;
private patientParams: PatientQueryParams;
private requestQueue: string[];
private resources = { ...environment.ehrServices };
private operators = { eq: ':', ne: '!', co: '~', lt: '<', gt: '>', le: '<:', ge: '>:', in: '' };
private summaryRequestCount = 0;

// hash of data types to retrieve along with their associated ngrx action
private dataTypes = {
'details': { action: patientActions.SetDetails, resource: 'details' },
'allergies': { action: patientActions.SetAllergies, resource: 'allergies' },
'consults': { action: patientActions.SetConsults, resource: 'consults' },
'labs': { action: patientActions.SetLabs, resource: 'observations' },
'vitals': { action: patientActions.SetVitals, resource: 'observations' },
'immunizations': { action: patientActions.SetImmunizations, resource: 'immunizations' },
'progressNotes': { action: patientActions.SetProgressNotes, resource: 'progressNotes' },
'problemList': { action: patientActions.SetProblemList, resource: 'problemList' },
'vitalSummary': { action: patientActions.SetVitalSummary, resource: 'observations' },
'labSummary': { action: patientActions.SetLabSummary, resource: 'observations' },
'lastInpatient': { action: patientActions.SetEncounterSummary, resource: 'consults', key: 'inpatient'},
'lastOutpatient': { action: patientActions.SetEncounterSummary, resource: 'consults', key: 'outpatient'},
'cwad': { action: patientActions.SetCWAD, resource: 'progressNotes' },
'appointments': { action: patientActions.SetAppointments, resource: 'appointments' }
};

constructor(public http: HttpClient, private _store: Store<AppState>) {

// observe ngrx store's selected patient's ID and query parameters so we have the latest when making
// API queries
this._store.pipe(select(state => state.patient.selectedPatient.params)).subscribe(params => this.patientParams = params);
this._store.pipe(select(state => state.patient.selectedPatient.data.identity)).subscribe(data => this.patientID = data);

// observe the store's request queue for processing
this._store.pipe(select(state => state.patient.requestQueue)).subscribe(queue => this.requestQueue = queue);

// observe the store's request trigger to initiate launching pending requests from the queue
this._store.pipe(select(state => state.patient.requestTrigger)).subscribe(() => this.processQueue());

}

/**********************************************************************************************
* Triggered when pending request queue is modified in the store.
*/
private processQueue() {

this.requestQueue.forEach(dataType => {
if (dataType.match(/^(vital|lab)Summary/)) {
this.getSummaryPatientData(dataType);
} else {
this.getPatientData(dataType);
}
});

}

// straight from the angular docs, an error handler for HTTP...
private handleError(error: HttpErrorResponse) {

let retVal = '';

if (error.error instanceof ErrorEvent) {

// A client-side or network error occurred. Handle it accordingly.
retVal = 'A network error occurred, check your device\'s connectivity.';

} else if (error.message.indexOf('parsing') > -1) {

retVal = 'Data returned from the server was unreadable';

} else {

retVal = 'An error occurred while retrieving data from the EHR system. (' + error.status + ')';

}

// return an ErrorObservable with a user-facing error message
return throwError(retVal);
}

/**********************************************************************************************
* Perform an XHR GET request to the API service to retrieve a specific type of patient data
* @param dataType one of EhrService.dataTypes
*/
getPatientData(dataType: string) {

// retrieve the URL of the API endpoint for the specific request from the environment
// (via this.resources) and set the patient identifier in the URL based on selectedPatient..
const url = this.resources[this.dataTypes[dataType].resource].replace('<patientID>', this.patientID.patientId);

// If this datatype has parameters, set them.
const queryString = this.buildQueryStringFromParams(this.patientParams[dataType]);

// send off the request
this.doRequest(url + queryString, dataType, this.dataTypes[dataType].key, this.dataTypes[dataType].action);

}

/**********************************************************************************************
* Perform multiple XHR GET requests to a single endpoint using different query parameters
*/
getSummaryPatientData(dataType: string) {

// retrieve the URL of the API endpoint for the specific request from the environment
// (via this.resources) and set the patient identifier in the URL based on selectedPatient..
const url = this.resources[this.dataTypes[dataType].resource].replace('<patientID>', this.patientID.patientId);

this.summaryRequestCount = this.patientParams[dataType].length;

// summary data is an array of individual calls we loop through & fire off...
this.patientParams[dataType].forEach((summaryType: SummaryType) => {

// build the query parameters based on the summary item..
const queryParams = { page: 1, size: 1,
sort: {timeTaken: 'desc'}, filter: { name: { operator: FilterOps.eq, value: summaryType.key }} };

const queryString = this.buildQueryStringFromParams(queryParams);

// send off the request
this.doRequest(url + queryString, dataType, summaryType.key, this.dataTypes[dataType].action);

});

}

/**********************************************************************************************
* Run an XHR Call & handle the response
*/
doRequest(url: string, dataType: string, key: string, action: any) {

// if we don't have a patient ID, return
if (!this.patientID) { return; }

// set headers for the request - just return data type for now
const headers = new HttpHeaders().set('Accept', 'application/json');

const results = {data: null, err: null, queryDate: new Date()};
if (key) { results['key'] = key; }

this._store.dispatch(new sessionActions.AddPendingRequest((key) ? dataType + '.' + key : dataType));

// make the request
this.http.get<any>(url, { headers })
.pipe(catchError(this.handleError))
.subscribe(data => {

this._store.dispatch(new sessionActions.RemovePendingRequest((key) ? dataType + '.' + key : dataType));

if (dataType.match(/^(vital|lab)Summary/)) {
this.summaryRequestCount--;
if (this.summaryRequestCount === 0) {
this._store.dispatch(new patientActions.RemovePendingRequest(dataType));
}
} else {
this._store.dispatch(new patientActions.RemovePendingRequest(dataType));
}

try {
results.data = this.processPatientData((key) ? dataType + '.' + key : dataType, data);
} catch (ex) {
results.data = null;
results.err = ex;
this._store.dispatch(new sessionActions.AddUIMessage({ title: 'Error', body: ex }));
}

this._store.dispatch(new action(results));

},
error => {

if (dataType.match(/^(vital|lab)Summary/)) {
this.summaryRequestCount--;
if (this.summaryRequestCount === 0) {
this._store.dispatch(new patientActions.RemovePendingRequest(dataType));
}
} else {
this._store.dispatch(new patientActions.RemovePendingRequest(dataType));
}

this._store.dispatch(new sessionActions.RemovePendingRequest((key) ? dataType + '.' + key : dataType));
this._store.dispatch(new sessionActions.AddUIMessage({ title: 'Error', body: error }));
results.err = error;
results.data = null;
this._store.dispatch(new action(results));

});
}

/**********************************************************************************************
* convert QueryParams for an element into an API-compliant querystring.
*/
buildQueryStringFromParams(params: QueryParams) {

if (!params || _.isEmpty(params)) { return ''; }

let qs = '?';

if (params.page) { qs += '&page=' + params.page; }
if (params.size) { qs += '&size=' + params.size; }

// sort is sent in format field[ASC|DESC],...
if (params.sort && !_.isEmpty(params.sort)) {
let sortString = '';
Object.keys(params.sort).forEach((itm) => {
sortString += itm + '[' + params.sort[itm].toUpperCase() + '],';
});
qs += '&sort=' + sortString.replace(/,$/, '');
}

// filter is stored locally in format field: {operator: <operator>, value: <value>, value2: <value2>}
// API requires field<op>value, where op is defined in operator property above.
// see API spec for operator details..
if (params.filter && !_.isEmpty(params.filter)) {

let filter = '';

Object.keys(params.filter).forEach((itm) => {
// check if it's a range parameter (type 'in'). if so, add separate filters for the range boxing.
if (params.filter[itm].operator === FilterOps.in) {
if (params.filter[itm].value) { filter += encodeURIComponent(itm + this.operators.ge + params.filter[itm].value + ','); }
if (params.filter[itm].value2) { filter += encodeURIComponent(itm + this.operators.le + params.filter[itm].value2 + ','); }

// otherwise just add the parameter as field<op>value
} else {
// OR handled by plus sign between values. need to double quote each value and URL encode the content
const encValues = params.filter[itm].value.split('+');
filter += encodeURIComponent(itm + this.operators[params.filter[itm].operator]);
encValues.forEach(value => {
filter += encodeURIComponent('"' + value + '"') + '+';
});

// remove the last plus sign and add a comma
filter = filter.replace(/\+$/, '') + ',';
}

});

qs += '&search=' + filter.replace(/,$/, '');

}

return qs.replace(/\?&/, '?');

}
/**********************************************************************************************
* Initial transformations/validation for incoming data are executed here, prior to adding to redux store
*/
processPatientData(dataType: string, data: any) {

if (dataType === 'allergies') {
return DP.processAllergyData(data);

} else if (dataType.match(/^(vital|lab)Summary/)) {
return DP.processObservationSummary(data, dataType);

} else if (dataType.match(/^last.+patient/)) {
return DP.processEncounterSummary(data, dataType);

} else if (dataType === 'vitals') {
return DP.processVitalData(data, dataType, this.patientParams.vitals);

} else if (dataType === 'cwad' || dataType === 'progressNotes') {
return DP.processDocumentData(data, dataType);

} else {
return JSON.parse(JSON.stringify(data));
}

}
}