diff --git a/api/src/main/java/org/openmrs/module/queue/api/QueueEntryService.java b/api/src/main/java/org/openmrs/module/queue/api/QueueEntryService.java index 0b10286..cee5d9a 100644 --- a/api/src/main/java/org/openmrs/module/queue/api/QueueEntryService.java +++ b/api/src/main/java/org/openmrs/module/queue/api/QueueEntryService.java @@ -59,6 +59,16 @@ public interface QueueEntryService { */ QueueEntry transitionQueueEntry(@NotNull QueueEntryTransition queueEntryTransition); + /** + * Undos a transition by voiding the input queue entry and making its previous queue entry + * (which MUST not be null) active by setting the previous entry's end time to null. + * + * @param queueEntry the queue entry to undo transition from + * @return the previous queue entry, re-activated + * + */ + QueueEntry undoTransition(@NotNull QueueEntry queueEntry); + /** * Voids a queue entry * @@ -113,4 +123,13 @@ String generateVisitQueueNumber(@NotNull Location location, @NotNull Queue queue * @param sortWeightGenerator the SortWeightGenerator to set */ void setSortWeightGenerator(SortWeightGenerator sortWeightGenerator); + + /** + * Given a specified queue entry, return its previous queue entry (i.e. the queue entry the patient + * transition from to get to the specified one) + * + * @param queueEntry + * @return the previous queue entry, if uniquely identifable; null otherwise. + */ + QueueEntry getPreviousQueueEntry(@NotNull QueueEntry queueEntry); } diff --git a/api/src/main/java/org/openmrs/module/queue/api/dao/impl/QueueEntryDaoImpl.java b/api/src/main/java/org/openmrs/module/queue/api/dao/impl/QueueEntryDaoImpl.java index 97dd53c..98d4ac4 100644 --- a/api/src/main/java/org/openmrs/module/queue/api/dao/impl/QueueEntryDaoImpl.java +++ b/api/src/main/java/org/openmrs/module/queue/api/dao/impl/QueueEntryDaoImpl.java @@ -64,8 +64,10 @@ private Criteria createCriteriaFromSearchCriteria(QueueEntrySearchCriteria searc limitByCollectionProperty(c, "qe.queueComingFrom", searchCriteria.getQueuesComingFrom()); limitToGreaterThanOrEqualToProperty(c, "qe.startedAt", searchCriteria.getStartedOnOrAfter()); limitToLessThanOrEqualToProperty(c, "qe.startedAt", searchCriteria.getStartedOnOrBefore()); + limitToEqualsProperty(c, "qe.startedAt", searchCriteria.getStartedOn()); limitToGreaterThanOrEqualToProperty(c, "qe.endedAt", searchCriteria.getEndedOnOrAfter()); limitToLessThanOrEqualToProperty(c, "qe.endedAt", searchCriteria.getEndedOnOrBefore()); + limitToEqualsProperty(c, "qe.endedAt", searchCriteria.getEndedOn()); if (searchCriteria.getHasVisit() == Boolean.TRUE) { c.add(Restrictions.isNotNull("qe.visit")); } else if (searchCriteria.getHasVisit() == Boolean.FALSE) { diff --git a/api/src/main/java/org/openmrs/module/queue/api/impl/QueueEntryServiceImpl.java b/api/src/main/java/org/openmrs/module/queue/api/impl/QueueEntryServiceImpl.java index 3eb1426..c768236 100644 --- a/api/src/main/java/org/openmrs/module/queue/api/impl/QueueEntryServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/queue/api/impl/QueueEntryServiceImpl.java @@ -131,6 +131,31 @@ public QueueEntry transitionQueueEntry(QueueEntryTransition queueEntryTransition return getProxiedQueueEntryService().saveQueueEntry(queueEntryToStart); } + /** + * @see QueueEntryService#undoTransition(QueueEntry) + */ + @Override + @Transactional + public QueueEntry undoTransition(@NotNull QueueEntry queueEntry) { + if (queueEntry.getVoided()) { + throw new IllegalArgumentException("cannot undo transition on a voided queue entry"); + } + if (queueEntry.getEndedAt() != null) { + throw new IllegalArgumentException("cannot undo transition on an ended queue entry"); + } + QueueEntry prevQueueEntry = getPreviousQueueEntry(queueEntry); + if (prevQueueEntry == null) { + throw new IllegalArgumentException("specified queue entry does not have a previous queue entry"); + } + prevQueueEntry.setEndedAt(null); + prevQueueEntry = dao.createOrUpdate(prevQueueEntry); + + queueEntry.setVoided(true); + queueEntry.setVoidReason("undo transition"); + dao.createOrUpdate(queueEntry); + return prevQueueEntry; + } + /** * @see QueueEntryService#voidQueueEntry(QueueEntry, String) */ @@ -225,4 +250,18 @@ private void endQueueEntry(@NotNull QueueEntry queueEntry) { queueEntry.setEndedAt(new Date()); dao.createOrUpdate(queueEntry); } + + @Override + public QueueEntry getPreviousQueueEntry(@NotNull QueueEntry queueEntry) { + QueueEntrySearchCriteria criteria = new QueueEntrySearchCriteria(); + criteria.setPatient(queueEntry.getPatient()); + criteria.setVisit(queueEntry.getVisit()); + criteria.setEndedOn(queueEntry.getStartedAt()); + List prevQueueEntries = dao.getQueueEntries(criteria); + if (prevQueueEntries.size() == 1) { + return prevQueueEntries.get(0); + } else { + return null; + } + } } diff --git a/api/src/main/java/org/openmrs/module/queue/api/search/QueueEntrySearchCriteria.java b/api/src/main/java/org/openmrs/module/queue/api/search/QueueEntrySearchCriteria.java index b2ae983..7cf0dc4 100644 --- a/api/src/main/java/org/openmrs/module/queue/api/search/QueueEntrySearchCriteria.java +++ b/api/src/main/java/org/openmrs/module/queue/api/search/QueueEntrySearchCriteria.java @@ -61,11 +61,15 @@ public class QueueEntrySearchCriteria implements Serializable { private Date startedOnOrBefore; + private Date startedOn; + private Boolean isEnded = null; private Date endedOnOrAfter; private Date endedOnOrBefore; + private Date endedOn; + private boolean includedVoided = false; } diff --git a/api/src/test/java/org/openmrs/module/queue/api/QueueEntryServiceTest.java b/api/src/test/java/org/openmrs/module/queue/api/QueueEntryServiceTest.java index 743ada7..5812f06 100644 --- a/api/src/test/java/org/openmrs/module/queue/api/QueueEntryServiceTest.java +++ b/api/src/test/java/org/openmrs/module/queue/api/QueueEntryServiceTest.java @@ -16,6 +16,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Optional; @@ -285,6 +286,56 @@ public void shouldTransitionQueueEntry() { assertThat(queueEntry3.getStartedAt(), equalTo(date3)); assertNull(queueEntry3.getEndedAt()); } + + @Test + public void shouldUndoTransitionQueueEntry() { + Patient patient1 = new Patient(); + Visit visit1 = new Visit(); + visit1.setPatient(patient1); + Queue queue0 = new Queue(); + Queue queue1 = new Queue(); + Concept concept1 = new Concept(); + String string1 = "starting"; + double double1 = 5.0; + Location location1 = new Location(); + Provider provider1 = new Provider(); + Date date1 = DateUtils.addHours(new Date(), -12); + Date date2 = DateUtils.addHours(date1, 6); + + QueueEntry queueEntry1 = new QueueEntry(); + queueEntry1.setQueue(queue1); + queueEntry1.setPatient(patient1); + queueEntry1.setVisit(visit1); + queueEntry1.setPriority(concept1); + queueEntry1.setPriorityComment(string1); + queueEntry1.setStatus(concept1); + queueEntry1.setSortWeight(double1); + queueEntry1.setLocationWaitingFor(location1); + queueEntry1.setProviderWaitingFor(provider1); + queueEntry1.setQueueComingFrom(queue0); + queueEntry1.setStartedAt(date1); + assertNull(queueEntry1.getEndedAt()); + + // Mock the DAO to return the object being saved + when(dao.createOrUpdate(any())).thenAnswer(invocation -> invocation.getArguments()[0]); + + // Mock the DAO to searches for previous queue entry correctly + QueueEntrySearchCriteria criteria = new QueueEntrySearchCriteria(); + criteria.setPatient(patient1); + criteria.setVisit(visit1); + criteria.setEndedOn(date2); + when(dao.getQueueEntries(criteria)).thenReturn(Arrays.asList(queueEntry1)); + + // First transition test that no changes are required and all values will be pulled from existing queue entry + QueueEntryTransition transition1 = new QueueEntryTransition(); + transition1.setQueueEntryToTransition(queueEntry1); + transition1.setTransitionDate(date2); + QueueEntry queueEntry2 = queueEntryService.transitionQueueEntry(transition1); + + queueEntryService.undoTransition(queueEntry2); + assertThat(queueEntry2.getVoided(), equalTo(true)); + assertNull(queueEntry1.getEndedAt()); + } @Test public void shouldGenerateVisitQueueNumber() { diff --git a/omod/src/main/java/org/openmrs/module/queue/web/QueueEntryTransitionRestController.java b/omod/src/main/java/org/openmrs/module/queue/web/QueueEntryTransitionRestController.java index 70c5fa3..cd4bdba 100644 --- a/omod/src/main/java/org/openmrs/module/queue/web/QueueEntryTransitionRestController.java +++ b/omod/src/main/java/org/openmrs/module/queue/web/QueueEntryTransitionRestController.java @@ -10,15 +10,17 @@ package org.openmrs.module.queue.web; import java.util.Date; -import java.util.Map; import java.util.Optional; import org.openmrs.Concept; import org.openmrs.api.APIException; +import org.openmrs.module.queue.api.QueueEntryService; import org.openmrs.module.queue.api.QueueServicesWrapper; import org.openmrs.module.queue.model.Queue; import org.openmrs.module.queue.model.QueueEntry; import org.openmrs.module.queue.model.QueueEntryTransition; +import org.openmrs.module.queue.web.dto.QueueEntryTransitionRequest; +import org.openmrs.module.queue.web.dto.UndoQueueEntryTransitionRequest; import org.openmrs.module.webservices.rest.web.ConversionUtil; import org.openmrs.module.webservices.rest.web.RestConstants; import org.openmrs.module.webservices.rest.web.representation.Representation; @@ -34,21 +36,8 @@ * The main controller that exposes additional end points for order entry */ @Controller -@RequestMapping(value = "/rest/" + RestConstants.VERSION_1 + "/queue-entry-transition") public class QueueEntryTransitionRestController extends BaseRestController { - public static final String QUEUE_ENTRY_TO_TRANSITION = "queueEntryToTransition"; - - public static final String TRANSITION_DATE = "transitionDate"; - - public static final String NEW_QUEUE = "newQueue"; - - public static final String NEW_STATUS = "newStatus"; - - public static final String NEW_PRIORITY = "newPriority"; - - public static final String NEW_PRIORITY_COMMENT = "newPriorityComment"; - private final QueueServicesWrapper services; @Autowired @@ -56,58 +45,73 @@ public QueueEntryTransitionRestController(QueueServicesWrapper services) { this.services = services; } - @RequestMapping(method = { RequestMethod.PUT, RequestMethod.POST }) + @RequestMapping(value = "/rest/" + RestConstants.VERSION_1 + "/queue-entry/transition", method = { RequestMethod.PUT, + RequestMethod.POST }) @ResponseBody - public Object transitionQueueEntry(@RequestBody Map body) { + public Object transitionQueueEntry(@RequestBody QueueEntryTransitionRequest body) { QueueEntryTransition transition = new QueueEntryTransition(); // Queue Entry to Transition - String queueEntryUuid = body.get(QUEUE_ENTRY_TO_TRANSITION); + String queueEntryUuid = body.getQueueEntryToTransition(); QueueEntry queueEntry = services.getQueueEntryService().getQueueEntryByUuid(queueEntryUuid) - .orElseThrow(() -> new APIException(QUEUE_ENTRY_TO_TRANSITION + " is a required parameter")); + .orElseThrow(() -> new APIException("queueEntryToTransition not specified or found")); transition.setQueueEntryToTransition(queueEntry); // Transition Date Date transitionDate = new Date(); - if (body.containsKey(TRANSITION_DATE)) { - transitionDate = (Date) ConversionUtil.convert(body.get(TRANSITION_DATE), Date.class); + if (body.getTransitionDate() != null) { + transitionDate = (Date) ConversionUtil.convert(body.getTransitionDate(), Date.class); } if (transitionDate == null) { - throw new APIException("Invalid transition date specified: " + body.get(TRANSITION_DATE)); + throw new APIException("Invalid transition date specified: " + body.getTransitionDate()); } transition.setTransitionDate(transitionDate); // Queue - if (body.containsKey(NEW_QUEUE)) { - Optional queueOptional = services.getQueueService().getQueueByUuid(body.get(NEW_QUEUE)); + if (body.getNewQueue() != null) { + Optional queueOptional = services.getQueueService().getQueueByUuid(body.getNewQueue()); if (!queueOptional.isPresent()) { - throw new APIException("Invalid queue specified: " + body.get(NEW_QUEUE)); + throw new APIException("Invalid queue specified: " + body.getNewQueue()); } transition.setNewQueue(queueOptional.get()); } // Status - if (body.containsKey(NEW_STATUS)) { - Concept concept = services.getConcept(body.get(NEW_STATUS)); + if (body.getNewStatus() != null) { + Concept concept = services.getConcept(body.getNewStatus()); if (concept == null) { - throw new APIException("Invalid status specified: " + body.get(NEW_STATUS)); + throw new APIException("Invalid status specified: " + body.getNewStatus()); } transition.setNewStatus(concept); } // Priority - if (body.containsKey(NEW_PRIORITY)) { - Concept concept = services.getConcept(body.get(NEW_PRIORITY)); + if (body.getNewPriority() != null) { + Concept concept = services.getConcept(body.getNewPriority()); if (concept == null) { - throw new APIException("Invalid priority specified: " + body.get(NEW_PRIORITY)); + throw new APIException("Invalid priority specified: " + body.getNewPriority()); } transition.setNewPriority(concept); } - transition.setNewPriorityComment(body.get(NEW_PRIORITY_COMMENT)); + transition.setNewPriorityComment(body.getNewPriorityComment()); // Execute transition QueueEntry newQueueEntry = services.getQueueEntryService().transitionQueueEntry(transition); return ConversionUtil.convertToRepresentation(newQueueEntry, Representation.REF); } + + @RequestMapping(value = "/rest/" + RestConstants.VERSION_1 + "/queue-entry/undo-transition", method = { + RequestMethod.PUT, RequestMethod.POST }) + @ResponseBody + public Object undoTransition(@RequestBody UndoQueueEntryTransitionRequest body) { + QueueEntryService qes = services.getQueueEntryService(); + Optional queueEntry = qes.getQueueEntryByUuid(body.getQueueEntry()); + if (queueEntry.isPresent()) { + QueueEntry unEndedQueueEntry = services.getQueueEntryService().undoTransition(queueEntry.get()); + return ConversionUtil.convertToRepresentation(unEndedQueueEntry, Representation.REF); + } else { + throw new APIException("Invalid queue entry uuid: " + body); + } + } } diff --git a/omod/src/main/java/org/openmrs/module/queue/web/dto/QueueEntryTransitionRequest.java b/omod/src/main/java/org/openmrs/module/queue/web/dto/QueueEntryTransitionRequest.java new file mode 100644 index 0000000..946cc23 --- /dev/null +++ b/omod/src/main/java/org/openmrs/module/queue/web/dto/QueueEntryTransitionRequest.java @@ -0,0 +1,28 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.queue.web.dto; + +import lombok.Getter; + +@Getter +public class QueueEntryTransitionRequest { + + private String queueEntryToTransition; + + private String transitionDate; + + private String newQueue; + + private String newStatus; + + private String newPriority; + + private String newPriorityComment; +} diff --git a/omod/src/main/java/org/openmrs/module/queue/web/dto/UndoQueueEntryTransitionRequest.java b/omod/src/main/java/org/openmrs/module/queue/web/dto/UndoQueueEntryTransitionRequest.java new file mode 100644 index 0000000..390d0f1 --- /dev/null +++ b/omod/src/main/java/org/openmrs/module/queue/web/dto/UndoQueueEntryTransitionRequest.java @@ -0,0 +1,18 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.queue.web.dto; + +import lombok.Getter; + +@Getter +public class UndoQueueEntryTransitionRequest { + + private String queueEntry; +} diff --git a/omod/src/main/java/org/openmrs/module/queue/web/resources/QueueEntryResource.java b/omod/src/main/java/org/openmrs/module/queue/web/resources/QueueEntryResource.java index d22346e..78f476a 100644 --- a/omod/src/main/java/org/openmrs/module/queue/web/resources/QueueEntryResource.java +++ b/omod/src/main/java/org/openmrs/module/queue/web/resources/QueueEntryResource.java @@ -177,6 +177,11 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("locationWaitingFor", Representation.DEFAULT); description.addProperty("queueComingFrom", Representation.DEFAULT); description.addProperty("providerWaitingFor", Representation.DEFAULT); + + // gets the previous queue entry, but with REF representation so it doesn't recursively + // fetch more previous entries. + description.addProperty("previousQueueEntry", Representation.REF); + description.addLink("full", ".?v=" + RestConstants.REPRESENTATION_FULL); } else if (representation instanceof FullRepresentation) { addSharedResourceDescriptionProperties(description); @@ -197,6 +202,7 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("voided"); description.addProperty("voidReason"); description.addProperty("auditInfo"); + description.addProperty("previousQueueEntry", Representation.FULL); } else if (representation instanceof CustomRepresentation) { description = null; } @@ -214,6 +220,11 @@ public String getDisplay(QueueEntry queueEntry) { return (personName == null ? queueEntry.getPatient().toString() : personName.getFullName()); } + @PropertyGetter("previousQueueEntry") + public QueueEntry getPreviousQueueEntry(QueueEntry queueEntry) { + return services.getQueueEntryService().getPreviousQueueEntry(queueEntry); + } + @Override public String getResourceVersion() { return "2.3";