From 9d47ce9a0d4ea3845c9c749ad066ddb6004c495e Mon Sep 17 00:00:00 2001 From: Chi Bong Ho Date: Tue, 23 Jun 2026 14:30:35 -0400 Subject: [PATCH] HTML-886 add attribute to specify sorting of options for appointmentServiceObs tag --- .../element/AppointmentServiceObsElement.java | 48 +++++++++++++++---- .../AppointmentServiceObsElementTest.java | 31 ++++++++++++ 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/htmlformentry/element/AppointmentServiceObsElement.java b/api/src/main/java/org/openmrs/module/htmlformentry/element/AppointmentServiceObsElement.java index 07a5dc42d..9eb17d138 100644 --- a/api/src/main/java/org/openmrs/module/htmlformentry/element/AppointmentServiceObsElement.java +++ b/api/src/main/java/org/openmrs/module/htmlformentry/element/AppointmentServiceObsElement.java @@ -1,10 +1,13 @@ package org.openmrs.module.htmlformentry.element; import javax.servlet.http.HttpServletRequest; + import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import org.apache.commons.lang.StringUtils; @@ -12,7 +15,9 @@ import org.openmrs.Obs; import org.openmrs.api.context.Context; import org.openmrs.module.appointments.model.AppointmentServiceDefinition; +import org.openmrs.module.appointments.model.Speciality; import org.openmrs.module.appointments.service.AppointmentServiceDefinitionService; +import org.openmrs.module.appointments.service.SpecialityService; import org.openmrs.module.htmlformentry.BadFormDesignException; import org.openmrs.module.htmlformentry.FormEntryContext; import org.openmrs.module.htmlformentry.FormEntryContext.Mode; @@ -37,6 +42,14 @@ public class AppointmentServiceObsElement implements HtmlGeneratorElement, FormS private String tagControlId; + private static final String ATTRIBUTE_SPECIALITIES = "specialities"; + private static final String ATTRIBUTE_SORT_BY = "sortBy"; + + private static final String ATTRIBUTE_SORT_BY_SPECIALITIES_ORDER_THEN_SERVICE_NAME = "specialitiesOrderThenServiceName"; + + @SuppressWarnings("unused") + private static final String ATTRIBUTE_SORT_BY_SERVICE_NAME = "serviceName"; // default sort order + public AppointmentServiceObsElement(FormEntryContext context, Map parameters) throws BadFormDesignException{ String conceptId = parameters.get("conceptId"); if (conceptId == null) { @@ -68,20 +81,35 @@ public AppointmentServiceObsElement(FormEntryContext context, Map specialityFilter = TagUtil.parseListParameter(parameters, "specialities", String.class).stream() + List specialityFilterStr = TagUtil.parseListParameter(parameters, ATTRIBUTE_SPECIALITIES, String.class).stream() .map(String::toLowerCase).map(String::trim).collect(Collectors.toList()); + List allSpecialities = Context.getService(SpecialityService.class).getAllSpecialities(); + List specialityFilter = specialityFilterStr.stream() + .map(s -> allSpecialities.stream().filter(sp -> sp.getName().equalsIgnoreCase(s) || sp.getUuid().equalsIgnoreCase(s)).findFirst()) + .filter(Optional::isPresent) + .map(Optional::get) + .map(Speciality::getUuid) + .collect(Collectors.toList()); + + + Comparator compareServicesByServiceName = Comparator.comparing(AppointmentServiceDefinition::getName, String.CASE_INSENSITIVE_ORDER); + Comparator compareServicesBySpecialityOrder = Comparator.comparingInt(svc -> { + Speciality speciality = svc.getSpeciality(); + return speciality != null ? specialityFilter.indexOf(speciality.getUuid()) : -1; + }); List services = Context.getService(AppointmentServiceDefinitionService.class) .getAllAppointmentServices(false); - services.sort((a, b) -> a.getName().compareToIgnoreCase(b.getName())); - - if (!specialityFilter.isEmpty()) { - services = services.stream() - .filter(s -> s.getSpeciality() != null - && (specialityFilter.contains(s.getSpeciality().getName().toLowerCase().trim()) - || specialityFilter.contains(s.getSpeciality().getUuid().toLowerCase().trim()))) - .collect(Collectors.toList()); - } + + String sortBy = parameters.get(ATTRIBUTE_SORT_BY); + boolean sortBySpecialitiesOrder = ATTRIBUTE_SORT_BY_SPECIALITIES_ORDER_THEN_SERVICE_NAME.equalsIgnoreCase(sortBy); + services = services.stream() + .filter(service -> specialityFilterStr.isEmpty() + || (service.getSpeciality() != null && specialityFilter.contains(service.getSpeciality().getUuid()))) + .sorted(sortBySpecialitiesOrder + ? compareServicesBySpecialityOrder.thenComparing(compareServicesByServiceName) + : compareServicesByServiceName) + .collect(Collectors.toList()); serviceWidget = new DropdownWidget(); errorWidget = new ErrorWidget(); diff --git a/api/src/test/java/org/openmrs/module/htmlformentry/element/AppointmentServiceObsElementTest.java b/api/src/test/java/org/openmrs/module/htmlformentry/element/AppointmentServiceObsElementTest.java index 994ddb03c..d3cb65285 100644 --- a/api/src/test/java/org/openmrs/module/htmlformentry/element/AppointmentServiceObsElementTest.java +++ b/api/src/test/java/org/openmrs/module/htmlformentry/element/AppointmentServiceObsElementTest.java @@ -36,6 +36,7 @@ import org.openmrs.module.appointments.model.AppointmentServiceDefinition; import org.openmrs.module.appointments.model.Speciality; import org.openmrs.module.appointments.service.AppointmentServiceDefinitionService; +import org.openmrs.module.appointments.service.SpecialityService; import org.openmrs.module.htmlformentry.BadFormDesignException; import org.openmrs.module.htmlformentry.FormEntryContext; import org.openmrs.module.htmlformentry.FormEntryContext.Mode; @@ -60,6 +61,9 @@ public class AppointmentServiceObsElementTest { @Mock private AppointmentServiceDefinitionService appointmentServiceDefinitionService; + @Mock + private SpecialityService specialityService; + private FormSubmissionActions actions; private MockedStatic mockedContext; @@ -82,7 +86,12 @@ public void setup() throws Exception { mockedHtmlFormEntryUtil.when(() -> HtmlFormEntryUtil.createObs(any(Concept.class), any(), any(), any())).thenCallRealMethod(); mockedContext.when(() -> Context.getService(AppointmentServiceDefinitionService.class)) .thenReturn(appointmentServiceDefinitionService); + mockedContext.when(() -> Context.getService(SpecialityService.class)) + .thenReturn(specialityService); mockedContext.when(Context::getLocale).thenReturn(Locale.ENGLISH); + when(specialityService.getAllSpecialities()).thenReturn( + Arrays.asList(makeSpeciality("Cardiology", "spec-uuid-cardio"), + makeSpeciality("Orthopaedics", "spec-uuid-ortho"))); when(appointmentServiceDefinitionService.getAllAppointmentServices(false)) .thenReturn(Collections.singletonList(makeService("Cardiology", 1, SERVICE_UUID, null))); @@ -161,6 +170,28 @@ public void constructor_shouldMatchSpecialityFilterCaseInsensitively() throws Ex assertThat(html.contains("uuid-cardio"), is(true)); } + @Test + public void constructor_shouldSortServicesBySpecialityOrderThenByName() throws Exception { + Speciality cardiology = makeSpeciality("Cardiology", "spec-uuid-cardio"); + Speciality orthopaedics = makeSpeciality("Orthopaedics", "spec-uuid-ortho"); + when(specialityService.getAllSpecialities()).thenReturn(Arrays.asList(cardiology, orthopaedics)); + when(appointmentServiceDefinitionService.getAllAppointmentServices(false)).thenReturn( + Arrays.asList( + makeService("Cardio B", 1, "uuid-cardio-b", cardiology), + makeService("Ortho A", 2, "uuid-ortho-a", orthopaedics), + makeService("Cardio A", 3, "uuid-cardio-a", cardiology))); + + Map p = params("conceptId", CONCEPT_ID); + p.put("specialities", "Cardiology,Orthopaedics"); + p.put("sortBy", "specialitiesOrder"); + AppointmentServiceObsElement element = new AppointmentServiceObsElement(context, p); + stubFieldName(element, "field"); + String html = element.generateHtml(context); + + assertThat(html.indexOf("uuid-cardio-a") < html.indexOf("uuid-cardio-b"), is(true)); + assertThat(html.indexOf("uuid-cardio-b") < html.indexOf("uuid-ortho-a"), is(true)); + } + @Test public void constructor_shouldFilterServicesBySpecialityUuid() throws Exception { when(appointmentServiceDefinitionService.getAllAppointmentServices(false)).thenReturn(