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 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
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,6 @@ public void shouldFindVisitEvenIfPatientHasMoreRecentVisitNoteWithoutAdmissionDi
.encounterType(emrApiProperties.getVisitNoteEncounterType())
.visit(visit)
.save();
testDataManager.obs()
.person(patient)
.encounter(secondVisitNoteEncounter)
.concept(dispositionDescriptor.getDispositionConcept())
.value(emrConceptService.getConcept("org.openmrs.module.emrapi:Death"))
.save();

VisitQueryResult result = visitQueryService.evaluate(query, null);
assertThat(result.getMemberIds().size(), is(1));
Expand Down
10 changes: 5 additions & 5 deletions api/src/main/java/org/openmrs/module/emrapi/adt/AdtService.java
Original file line number Diff line number Diff line change
Expand Up @@ -344,13 +344,13 @@ 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 patient awaiting transfer
* @param transferLocation - if non-null, only return matches for patients awaiting transfer to this location
* @return List<Visit> of the matching visits<
* 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<Visit> getVisitsAwaitingTransfer(Location transferLocation);
List<InpatientRequest> getInpatientRequests(InpatientRequestSearchCriteria criteria);
}
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 @@ -920,8 +924,90 @@ public List<Visit> getVisitsAwaitingAdmission(Location location, Collection<Inte

@Override
@Transactional(readOnly = true)
public List<Visit> getVisitsAwaitingTransfer(Location transferLocation) {
// TODO implement!
return Collections.emptyList();
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();
if (dispositions != null) {
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);
parameters.put("limitByDispositionLocation", dispositionLocationIds != null);
parameters.put("admitLocationConcept", descriptor.getAdmissionLocationConcept());
parameters.put("transferLocationConcept", descriptor.getInternalTransferLocationConcept());
parameters.put("patientIds", criteria.getPatientIds());
parameters.put("limitByPatient", criteria.getPatientIds() != null);
parameters.put("visitIds", criteria.getVisitIds());
parameters.put("limitByVisit", criteria.getVisitIds() != null);

List<?> reqs = emrApiDAO.executeHqlFromResource("hql/inpatient_request_dispositions.hql", parameters, List.class);
List<InpatientRequest> ret = new ArrayList<>();
for (Object req : reqs) {
Object[] o = (Object[]) req;
InpatientRequest r = new InpatientRequest();
r.setVisit((Visit)o[0]);
r.setPatient((Patient)o[1]);
r.setDispositionEncounter((Encounter)o[2]);
r.setDispositionObsGroup((Obs)o[3]);
Obs dispositionObs = (Obs)o[4];
if (dispositionObs != null) {
r.setDisposition(dispositionObs.getValueCoded());
r.setDispositionType(dispositionValuesToType.get(dispositionObs.getValueCoded()));
}
Obs locationObs = (Obs)(o[5] != null ? o[5] : o[6]);
if (locationObs != null) {
r.setDispositionLocation(locationService.getLocation(Integer.parseInt(locationObs.getValueText())));
}
ret.add(r);
}
return ret;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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
* Currently the assumption is that all requests returned are active, and this will be the default regardless
*/
@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;
private List<Integer> patientIds;
private List<Integer> visitIds;

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);
}

public void addPatientId(Integer patientId) {
if (patientIds == null) {
patientIds = new ArrayList<>();
}
patientIds.add(patientId);
}

public void addVisitId(Integer visitId) {
if (visitIds == null) {
visitIds = new ArrayList<>();
}
visitIds.add(visitId);
}
}
63 changes: 63 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,63 @@
select
visit,
dispoEncounter.patient,
dispoEncounter,
dispo.obsGroup,
dispo,
(select o from Obs o where o.obsGroup = dispo.obsGroup and o.voided = 0 and o.concept = :admitLocationConcept) as admitLocation,
(select o from Obs o where o.obsGroup = dispo.obsGroup and o.voided = 0 and o.concept = :transferLocationConcept) as transferLocation
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 Obs as laterDispoObs
where laterDispoObs.encounter.visit = visit
and laterDispoObs.voided = false
and laterDispoObs.concept = :dispositionConcept
and (
laterDispoObs.obsDatetime > dispo.obsDatetime or
(laterDispoObs.obsDatetime = dispo.obsDatetime and laterDispoObs.obsId > dispo.obsId)
)
) = 0
and (
select count(*)
from Encounter as adtEncounter
where adtEncounter.visit = visit
and adtEncounter.voided = false
and adtEncounter.encounterType in (:adtEncounterTypes)
and adtEncounter.encounterDatetime >= dispo.obsDatetime
) = 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 (
:limitByDispositionLocation = false or (
select count(*)
from Obs as locationObs
where locationObs.obsGroup = dispo.obsGroup
and locationObs.valueText in (:dispositionLocationIds)
) > 0
)
and (:limitByPatient is false or dispoEncounter.patient.patientId in (:patientIds))
and (:limitByVisit is false or visit.visitId in (:visitIds))
order by dispo.obsId
12 changes: 12 additions & 0 deletions api/src/main/resources/hql/visits_awaiting_admission.hql
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,24 @@ where
and (:visitIds is null or visit.visitId in :visitIds)
and person.dead = false
and visit.stopDatetime is null
and (
select count(*)
from Obs as laterDispoObs
where laterDispoObs.encounter.visit = visit
and laterDispoObs.voided = false
and laterDispoObs.concept = :dispositionConcept
and (
laterDispoObs.obsDatetime > dispo.obsDatetime or
(laterDispoObs.obsDatetime = dispo.obsDatetime and laterDispoObs.obsId > dispo.obsId)
)
) = 0
and (
select count(*)
from Encounter as admission
where admission.visit = visit
and admission.voided = false
and admission.encounterType = :admissionEncounterType
and admission.encounterDatetime >= dispo.obsDatetime
) = 0
and (
select count(*)
Expand Down
Loading
Loading