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

EA-188 - Improve Java and Rest API for retrieving inpatient admission requests #233

Merged
merged 17 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -344,9 +344,16 @@ VisitDomainWrapper createRetrospectiveVisit(Patient patient, Location location,
* @param visitIds - if non-null, only returns matches for visits with the given ids
* @return List<Visit></Visit> of the matching visits
*/
// TODO expand this to take in an admissionLocation parameter and limit to admissions at that location
@Deprecated
List<Visit> getVisitsAwaitingAdmission(Location location, Collection<Integer> patientIds, Collection<Integer> visitIds);

/**
* Returns all List of InpatientRequest that match the given search criteria
* @param criteria - represents the criteria by which inpatient requests are searched and returned
* @return List<InpatientRequest> of the matching InpatientRequests that match the criteria
*/
List<InpatientRequest> getInpatientRequests(InpatientRequestSearchCriteria criteria);

/**
* Returns all patient awaiting transfer
* @param transferLocation - if non-null, only return matches for patients awaiting transfer to this location
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import org.apache.commons.lang.time.DateUtils;
import org.joda.time.DateTime;
import org.openmrs.Concept;
import org.openmrs.Encounter;
import org.openmrs.EncounterRole;
import org.openmrs.EncounterType;
Expand Down Expand Up @@ -45,7 +46,9 @@
import org.openmrs.module.emrapi.concept.EmrConceptService;
import org.openmrs.module.emrapi.db.EmrApiDAO;
import org.openmrs.module.emrapi.disposition.Disposition;
import org.openmrs.module.emrapi.disposition.DispositionDescriptor;
import org.openmrs.module.emrapi.disposition.DispositionService;
import org.openmrs.module.emrapi.disposition.DispositionType;
import org.openmrs.module.emrapi.domainwrapper.DomainWrapperFactory;
import org.openmrs.module.emrapi.merge.PatientMergeAction;
import org.openmrs.module.emrapi.merge.VisitMergeAction;
Expand All @@ -58,6 +61,7 @@
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
Expand Down Expand Up @@ -918,6 +922,90 @@ public List<Visit> getVisitsAwaitingAdmission(Location location, Collection<Inte
return emrApiDAO.executeHqlFromResource("hql/visits_awaiting_admission.hql", parameters, Visit.class);
}

@Override
@Transactional(readOnly = true)
public List<InpatientRequest> getInpatientRequests(InpatientRequestSearchCriteria criteria) {

DispositionDescriptor descriptor = dispositionService.getDispositionDescriptor();

// Determine whether to filter visits at a particular location
Location visitLocation = null ;
if (criteria.getVisitLocation() != null ) {
visitLocation = getLocationThatSupportsVisits(criteria.getVisitLocation());
}

// Determine what type of dispositions to include. If none specified, default to all
List<DispositionType> dispositionTypes = criteria.getDispositionTypes();
if (dispositionTypes == null) {
dispositionTypes = Arrays.asList(DispositionType.values());
}

// Get all disposition concepts based on the given disposition type(s)
Map<Concept, DispositionType> dispositionValuesToType = new HashMap<>();
List<Disposition> dispositions = dispositionService.getDispositions();
for (Disposition d : dispositions) {
if (dispositionTypes.contains(d.getType())) {
dispositionValuesToType.put(emrConceptService.getConcept(d.getConceptCode()), d.getType());
}
}

// Get all encounter types that might cause a request to be fulfilled
List<EncounterType> adtEncounterTypes = new ArrayList<>();
if (dispositionTypes.contains(DispositionType.ADMIT)) {
adtEncounterTypes.add(emrApiProperties.getAdmissionEncounterType());
}
if (dispositionTypes.contains(DispositionType.TRANSFER)) {
adtEncounterTypes.add(emrApiProperties.getTransferWithinHospitalEncounterType());
}
if (dispositionTypes.contains(DispositionType.DISCHARGE)) {
adtEncounterTypes.add(emrApiProperties.getExitFromInpatientEncounterType());
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of a question mark for me @mogoodrich . The way I coded this up, if you ask for only ADMIT requests, then only Admission Encounters will be checked as those that might render them fulfilled. Same with TRANSFER and DISCHARGE. If we think that an admit disposition followed by a transfer or discharge encounter should lead to that admit request being fulfilled, then we may want to rethink this logic here, and just add all 3 encounter types in all cases. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More of a business logic thing but doubtful will get a answer, but my general thought is a request is "active" if 1) it is the most recent disposition in the visit, and 2) there hasn't been any successive "ADT encounter type" (so to your direct question, I would add all three encounter types) and 3) there is no successive "deny admission" obs (if the disposition is admit)

Thoughts? Am I missing anything? At the end of the day, since a lot of the stuff is edge cases, probably best to just come up with the most succinct description/logic?

FWIW we could come up with a better name than "active", but I think is better than "fulfilled"... ie in your above example (admit disposition followed by transfer or discharge), I don't think the request has been been "fulfilled", but it is no longer active, which is what we care about there?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would tend to agree, hence why I asked the question, but this is not what has been in place for all of these years. The existing functionality only looks at Admission Encounters. It does not concern itself with transfer or discharge encounters at all.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this isn't getting a fair amount/any use outside of PIH, so I think it's fine to change if we think it's more correct...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the interest in getting this committed, let's ticket this separately and decide what to do, since it affects both existing and new code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// Disposition Locations are stored as Obs where the valueText is the location id. Collect these values
List<String> dispositionLocationIds = null;
if (criteria.getDispositionLocations() != null) {
dispositionLocationIds = new ArrayList<>();
for (Location l : criteria.getDispositionLocations()) {
dispositionLocationIds.add(l.getLocationId().toString());
}
}

Map<String, Object> parameters = new HashMap<>();
parameters.put("dispositionConcept", descriptor.getDispositionConcept());
parameters.put("dispositionValues", dispositionValuesToType.keySet());
parameters.put("visitLocation", visitLocation);
parameters.put("adtEncounterTypes", adtEncounterTypes);
parameters.put("adtDecisionConcept", emrApiProperties.getAdmissionDecisionConcept());
parameters.put("denyConcept", emrApiProperties.getDenyAdmissionConcept());
parameters.put("dispositionLocationIds", dispositionLocationIds);

List<Obs> dispositionObs = emrApiDAO.executeHqlFromResource("hql/inpatient_request_dispositions.hql", parameters, Obs.class);
List<InpatientRequest> ret = new ArrayList<>();
for (Obs o : dispositionObs) {
InpatientRequest r = new InpatientRequest();
r.setVisit(o.getEncounter().getVisit());
r.setPatient(o.getEncounter().getPatient());
r.setDispositionEncounter(o.getEncounter());
r.setDispositionObsGroup(o.getObsGroup());
r.setDispositionType(dispositionValuesToType.get(o.getValueCoded()));
r.setDisposition(o.getValueCoded());

Location dispositionLocation = null;
if (r.getDispositionType() == DispositionType.ADMIT) {
dispositionLocation = descriptor.getAdmissionLocation(o.getObsGroup(), locationService);
}
else if (r.getDispositionType() == DispositionType.TRANSFER) {
dispositionLocation = descriptor.getInternalTransferLocation(o.getObsGroup(), locationService);
}
r.setDispositionLocation(dispositionLocation);

Date deathDate = descriptor.getDateOfDeath(o.getObsGroup());
r.setDispositionDate(deathDate == null ? o.getObsDatetime() : deathDate);
ret.add(r);
}
return ret;
}

@Override
@Transactional(readOnly = true)
public List<Visit> getVisitsAwaitingTransfer(Location transferLocation) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.openmrs.module.emrapi.adt;

import lombok.Data;
import org.openmrs.Concept;
import org.openmrs.Encounter;
import org.openmrs.Location;
import org.openmrs.Obs;
import org.openmrs.Patient;
import org.openmrs.Visit;
import org.openmrs.module.emrapi.disposition.DispositionType;

import java.util.Date;

/**
* Represents and Admission, Discharge, or Transfer request
*/
@Data
public class InpatientRequest {
private Visit visit;
private Patient patient;
private DispositionType dispositionType;
private Encounter dispositionEncounter;
private Obs dispositionObsGroup;
private Concept disposition;
private Location dispositionLocation;
private Date dispositionDate;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.openmrs.module.emrapi.adt;

import lombok.Data;
import org.openmrs.Location;
import org.openmrs.module.emrapi.disposition.DispositionType;

import java.util.ArrayList;
import java.util.List;

/**
* Represents criteria for searching for AdtRequests
*/
@Data
public class InpatientRequestSearchCriteria {
mseaton marked this conversation as resolved.
Show resolved Hide resolved

private Location visitLocation;
private List<Location> dispositionLocations;
private List<DispositionType> dispositionTypes;

public void addDispositionLocation(Location location) {
if (dispositionLocations == null) {
dispositionLocations = new ArrayList<>();
}
dispositionLocations.add(location);
}

public void addDispositionType(DispositionType dispositionType) {
if (dispositionTypes == null) {
dispositionTypes = new ArrayList<>();
}
dispositionTypes.add(dispositionType);
}
}
42 changes: 42 additions & 0 deletions api/src/main/resources/hql/inpatient_request_dispositions.hql
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
select
dispo
from
Obs as dispo
inner join dispo.encounter as dispoEncounter
inner join dispoEncounter.visit as visit
inner join dispo.person as person
where
dispo.voided = false
and dispoEncounter.voided = false
and visit.voided = false
and dispo.concept = :dispositionConcept
and dispo.valueCoded in :dispositionValues
and (:visitLocation is null or visit.location = :visitLocation)
and person.dead = false
and visit.stopDatetime is null
and (
select count(*)
from Encounter as adtEncounter
where adtEncounter.visit = visit
and adtEncounter.voided = false
and adtEncounter.encounterType in (:adtEncounterTypes)
) = 0
mseaton marked this conversation as resolved.
Show resolved Hide resolved
and (
select count(*)
from Obs as adtDecision
inner join adtDecision.encounter as encounterInVisit
where encounterInVisit.visit = visit
and encounterInVisit.voided = false
and adtDecision.voided = false
and adtDecision.concept = :adtDecisionConcept
and adtDecision.valueCoded = :denyConcept
and encounterInVisit.encounterDatetime > dispoEncounter.encounterDatetime
) = 0
and (
:dispositionLocationIds is null or (
select count(*)
from Obs as locationObs
where locationObs.obsGroup = dispo.obsGroup
and locationObs.valueText in (:dispositionLocationIds)
) > 0
)
Loading
Loading