diff --git a/omod/src/main/java/org/openmrs/module/queue/web/dto/QueueEntryMetricsResponse.java b/omod/src/main/java/org/openmrs/module/queue/web/dto/QueueEntryMetricsResponse.java new file mode 100644 index 00000000..d2b3cfad --- /dev/null +++ b/omod/src/main/java/org/openmrs/module/queue/web/dto/QueueEntryMetricsResponse.java @@ -0,0 +1,222 @@ +/* + * 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 java.util.Date; + +/** + * A lightweight DTO representing only the fields the Service Queues UI actually needs for each + * active queue entry. + *
+ * Replaces the heavyweight custom representation pattern: + * {@code v=custom:(uuid,display,patient:(uuid,display,person:(age,gender)), + * queue:(uuid,display,location),status:(uuid,display),priority:(uuid,display), + * startedAt,sortWeight,visitQueueNumber)} + *
+ * This DTO eliminates deep graph traversal (Patient → Person → Concept...) and returns only what
+ * the frontend renders.
+ */
+public class QueueEntryMetricsResponse {
+
+ // ── Queue entry identity ────────────────────────────────────────────────
+ private String uuid;
+
+ // ── Patient (flat — no nested object graph) ─────────────────────────────
+ private String patientUuid;
+
+ private String patientName;
+
+ private String patientIdentifier; // e.g. OpenMRS ID shown in the table
+
+ private Integer patientAge;
+
+ private String patientGender;
+
+ // ── Queue info ───────────────────────────────────────────────────────────
+ private String queueUuid;
+
+ private String queueName;
+
+ private String locationUuid;
+
+ private String locationName;
+
+ // ── Entry status & priority (display names only) ─────────────────────────
+ private String statusUuid;
+
+ private String statusDisplay;
+
+ private String priorityUuid;
+
+ private String priorityDisplay;
+
+ // ── Timing ───────────────────────────────────────────────────────────────
+ private Date startedAt;
+
+ /** Wait time in minutes, computed server-side to avoid extra round-trips */
+ private Long waitTimeMinutes;
+
+ // ── Ordering ─────────────────────────────────────────────────────────────
+ private Double sortWeight;
+
+ // ── Visit queue number (ticket shown to patient) ─────────────────────────
+ private String visitQueueNumber;
+
+ // ── Constructors ──────────────────────────────────────────────────────────
+
+ public QueueEntryMetricsResponse() {
+ }
+
+ // ── Getters & Setters ─────────────────────────────────────────────────────
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ public void setUuid(String uuid) {
+ this.uuid = uuid;
+ }
+
+ public String getPatientUuid() {
+ return patientUuid;
+ }
+
+ public void setPatientUuid(String patientUuid) {
+ this.patientUuid = patientUuid;
+ }
+
+ public String getPatientName() {
+ return patientName;
+ }
+
+ public void setPatientName(String patientName) {
+ this.patientName = patientName;
+ }
+
+ public String getPatientIdentifier() {
+ return patientIdentifier;
+ }
+
+ public void setPatientIdentifier(String patientIdentifier) {
+ this.patientIdentifier = patientIdentifier;
+ }
+
+ public Integer getPatientAge() {
+ return patientAge;
+ }
+
+ public void setPatientAge(Integer patientAge) {
+ this.patientAge = patientAge;
+ }
+
+ public String getPatientGender() {
+ return patientGender;
+ }
+
+ public void setPatientGender(String patientGender) {
+ this.patientGender = patientGender;
+ }
+
+ public String getQueueUuid() {
+ return queueUuid;
+ }
+
+ public void setQueueUuid(String queueUuid) {
+ this.queueUuid = queueUuid;
+ }
+
+ public String getQueueName() {
+ return queueName;
+ }
+
+ public void setQueueName(String queueName) {
+ this.queueName = queueName;
+ }
+
+ public String getLocationUuid() {
+ return locationUuid;
+ }
+
+ public void setLocationUuid(String locationUuid) {
+ this.locationUuid = locationUuid;
+ }
+
+ public String getLocationName() {
+ return locationName;
+ }
+
+ public void setLocationName(String locationName) {
+ this.locationName = locationName;
+ }
+
+ public String getStatusUuid() {
+ return statusUuid;
+ }
+
+ public void setStatusUuid(String statusUuid) {
+ this.statusUuid = statusUuid;
+ }
+
+ public String getStatusDisplay() {
+ return statusDisplay;
+ }
+
+ public void setStatusDisplay(String statusDisplay) {
+ this.statusDisplay = statusDisplay;
+ }
+
+ public String getPriorityUuid() {
+ return priorityUuid;
+ }
+
+ public void setPriorityUuid(String priorityUuid) {
+ this.priorityUuid = priorityUuid;
+ }
+
+ public String getPriorityDisplay() {
+ return priorityDisplay;
+ }
+
+ public void setPriorityDisplay(String priorityDisplay) {
+ this.priorityDisplay = priorityDisplay;
+ }
+
+ public Date getStartedAt() {
+ return startedAt;
+ }
+
+ public void setStartedAt(Date startedAt) {
+ this.startedAt = startedAt;
+ }
+
+ public Long getWaitTimeMinutes() {
+ return waitTimeMinutes;
+ }
+
+ public void setWaitTimeMinutes(Long waitTimeMinutes) {
+ this.waitTimeMinutes = waitTimeMinutes;
+ }
+
+ public Double getSortWeight() {
+ return sortWeight;
+ }
+
+ public void setSortWeight(Double sortWeight) {
+ this.sortWeight = sortWeight;
+ }
+
+ public String getVisitQueueNumber() {
+ return visitQueueNumber;
+ }
+
+ public void setVisitQueueNumber(String visitQueueNumber) {
+ this.visitQueueNumber = visitQueueNumber;
+ }
+}
diff --git a/omod/src/main/java/org/openmrs/module/queue/web/resources/controller/QueueEntryMetricsController.java b/omod/src/main/java/org/openmrs/module/queue/web/resources/controller/QueueEntryMetricsController.java
new file mode 100644
index 00000000..1d89ca8c
--- /dev/null
+++ b/omod/src/main/java/org/openmrs/module/queue/web/resources/controller/QueueEntryMetricsController.java
@@ -0,0 +1,130 @@
+/*
+ * 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.resources.controller;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import org.openmrs.Concept;
+import org.openmrs.ConceptName;
+import org.openmrs.Location;
+import org.openmrs.Patient;
+import org.openmrs.PatientIdentifier;
+import org.openmrs.api.LocationService;
+import org.openmrs.module.queue.api.QueueEntryService;
+import org.openmrs.module.queue.api.search.QueueEntrySearchCriteria;
+import org.openmrs.module.queue.model.QueueEntry;
+import org.openmrs.module.queue.web.dto.QueueEntryMetricsResponse;
+import org.openmrs.module.webservices.rest.web.RestConstants;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+@Controller
+@RequestMapping(value = "/rest/" + RestConstants.VERSION_1 + "/queue/metrics")
+public class QueueEntryMetricsController {
+
+ private final LocationService locationService;
+
+ private final QueueEntryService queueEntryService;
+
+ @Autowired
+ public QueueEntryMetricsController(LocationService locationService, QueueEntryService queueEntryService) {
+ this.locationService = locationService;
+ this.queueEntryService = queueEntryService;
+ }
+
+ @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseBody
+ public ResponseEntity> getQueueMetrics(
+ @RequestParam(value = "location", required = false) String locationUuid,
+ @RequestParam(value = "status", required = false) String statusUuid,
+ @RequestParam(value = "queue", required = false) String queueUuid) {
+
+ if (locationUuid == null || locationUuid.trim().isEmpty()) {
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
+ }
+
+ Location location = locationService.getLocationByUuid(locationUuid);
+ if (location == null) {
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
+ }
+
+ QueueEntrySearchCriteria criteria = new QueueEntrySearchCriteria();
+ criteria.setLocations(Collections.singletonList(location));
+ criteria.setIsEnded(false);
+
+ List
> response = controller.getQueueMetrics(null, null, null);
+ assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST));
+ }
+
+ @Test
+ public void getQueueMetrics_shouldReturn400WhenLocationUuidIsBlank() {
+ ResponseEntity
> response = controller.getQueueMetrics(" ", null, null);
+ assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST));
+ }
+
+ @Test
+ public void getQueueMetrics_shouldReturn404WhenLocationNotFound() {
+ when(locationService.getLocationByUuid("unknown-uuid")).thenReturn(null);
+ ResponseEntity
> response = controller.getQueueMetrics("unknown-uuid", null, null);
+ assertThat(response.getStatusCode(), is(HttpStatus.NOT_FOUND));
+ }
+
+ @Test
+ public void getQueueMetrics_shouldReturnEmptyListWhenNoEntries() {
+ when(queueEntryService.getQueueEntries(any(QueueEntrySearchCriteria.class))).thenReturn(Collections.emptyList());
+ ResponseEntity
> response = controller.getQueueMetrics(LOCATION_UUID, null, null);
+ assertThat(response.getStatusCode(), is(HttpStatus.OK));
+ assertThat(response.getBody(), is(empty()));
+ }
+
+ @Test
+ public void getQueueMetrics_shouldReturnFlatDtoWithCorrectFields() {
+ when(queueEntryService.getQueueEntries(any(QueueEntrySearchCriteria.class)))
+ .thenReturn(Collections.singletonList(mockEntry));
+
+ ResponseEntity
> response = controller.getQueueMetrics(LOCATION_UUID, null, null);
+
+ assertThat(response.getStatusCode(), is(HttpStatus.OK));
+ assertThat(response.getBody(), hasSize(1));
+ QueueEntryMetricsResponse dto = response.getBody().get(0);
+ assertThat(dto.getUuid(), is(ENTRY_UUID));
+ assertThat(dto.getPatientUuid(), is(PATIENT_UUID));
+ assertThat(dto.getPatientName(), is("John Doe"));
+ assertThat(dto.getPatientIdentifier(), is("OM-10045"));
+ assertThat(dto.getPatientGender(), is("M"));
+ assertThat(dto.getQueueName(), is("Triage"));
+ assertThat(dto.getLocationUuid(), is(LOCATION_UUID));
+ assertThat(dto.getLocationName(), is("Outpatient Clinic"));
+ assertThat(dto.getStatusDisplay(), is("Waiting"));
+ assertThat(dto.getPriorityDisplay(), is("Normal"));
+ assertThat(dto.getWaitTimeMinutes(), is(greaterThanOrEqualTo(29L)));
+ assertThat(dto.getWaitTimeMinutes(), is(lessThanOrEqualTo(31L)));
+ }
+
+ @Test
+ public void getQueueMetrics_shouldReturnMultipleEntries() {
+ QueueEntry second = new QueueEntry();
+ second.setUuid("entry-uuid-second");
+ second.setStartedAt(new Date());
+ when(queueEntryService.getQueueEntries(any(QueueEntrySearchCriteria.class)))
+ .thenReturn(Arrays.asList(mockEntry, second));
+ ResponseEntity
> response = controller.getQueueMetrics(LOCATION_UUID, null, null);
+ assertThat(response.getStatusCode(), is(HttpStatus.OK));
+ assertThat(response.getBody(), hasSize(2));
+ }
+
+ @Test
+ public void getQueueMetrics_shouldHandleNullPatientGracefully() {
+ mockEntry.setPatient(null);
+ when(queueEntryService.getQueueEntries(any(QueueEntrySearchCriteria.class)))
+ .thenReturn(Collections.singletonList(mockEntry));
+ ResponseEntity
> response = controller.getQueueMetrics(LOCATION_UUID, null, null);
+ assertThat(response.getStatusCode(), is(HttpStatus.OK));
+ assertThat(response.getBody().get(0).getPatientUuid(), is(nullValue()));
+ }
+}