From f0840c8026a9ca2b437ef15b95d414846ac94ba7 Mon Sep 17 00:00:00 2001 From: Mark Goodrich Date: Tue, 21 May 2024 17:42:17 -0400 Subject: [PATCH] HTML-842: Add Appointments Tag --- .../htmlformentry/AppointmentTagTest.java | 46 +++++++--------- .../htmlformentry/FormEntryContext.java | 10 ++-- .../htmlformentry/FormEntrySession.java | 6 +-- .../htmlformentry/HtmlFormEntryUtil.java | 30 +++++++++++ .../element/AppointmentsElement.java | 21 ++++++-- .../handler/AppointmentsTagHandler.java | 2 +- .../widget/AppointmentsWidget.java | 54 +++++++++++-------- .../htmlformentry/widget/ProviderWidget.java | 5 +- api/src/main/resources/messages.properties | 2 + 9 files changed, 111 insertions(+), 65 deletions(-) diff --git a/api-tests/src/test/java/org/openmrs/module/htmlformentry/AppointmentTagTest.java b/api-tests/src/test/java/org/openmrs/module/htmlformentry/AppointmentTagTest.java index 676b73d35..838b18eae 100644 --- a/api-tests/src/test/java/org/openmrs/module/htmlformentry/AppointmentTagTest.java +++ b/api-tests/src/test/java/org/openmrs/module/htmlformentry/AppointmentTagTest.java @@ -61,10 +61,10 @@ public String[] widgetLabels() { @Override public void setupRequest(MockHttpServletRequest request, Map widgets) { - request.addParameter(widgets.get("Date:"), dateAsString(date)); - request.addParameter(widgets.get("Location:"), "2"); - request.addParameter(widgets.get("Provider:"), "502"); - request.addParameter(widgets.get("Appointments:"), "05f2ad92-1cc8-4cec-bf54-9cac0200746d"); + request.setParameter(widgets.get("Date:"), dateAsString(date)); + request.setParameter(widgets.get("Location:"), "2"); + request.setParameter(widgets.get("Provider:"), "502"); + request.setParameter(widgets.get("Appointments:"), "05f2ad92-1cc8-4cec-bf54-9cac0200746d"); } @Override @@ -103,11 +103,11 @@ public String[] widgetLabels() { @Override public void setupRequest(MockHttpServletRequest request, Map widgets) { - request.addParameter(widgets.get("Date:"), dateAsString(date)); - request.addParameter(widgets.get("Location:"), "2"); - request.addParameter(widgets.get("Provider:"), "502"); - request.addParameter(widgets.get("Appointments:"), "05f2ad92-1cc8-4cec-bf54-9cac0200746d"); - request.addParameter(widgets.get("Appointments:").replace("_1", "_2"), + request.setParameter(widgets.get("Date:"), dateAsString(date)); + request.setParameter(widgets.get("Location:"), "2"); + request.setParameter(widgets.get("Provider:"), "502"); + request.setParameter(widgets.get("Appointments:"), "05f2ad92-1cc8-4cec-bf54-9cac0200746d"); + request.setParameter(widgets.get("Appointments:").replace("_1", "_2"), "75504r42-3ca8-11e3-bf2b-0800271c1111"); } @@ -149,10 +149,10 @@ public String[] widgetLabels() { @Override public void setupRequest(MockHttpServletRequest request, Map widgets) { - request.addParameter(widgets.get("Date:"), dateAsString(date)); - request.addParameter(widgets.get("Location:"), "2"); - request.addParameter(widgets.get("Provider:"), "502"); - request.addParameter(widgets.get("Appointments:"), "05f2ad92-1cc8-4cec-bf54-9cac0200746d"); + request.setParameter(widgets.get("Date:"), dateAsString(date)); + request.setParameter(widgets.get("Location:"), "2"); + request.setParameter(widgets.get("Provider:"), "502"); + request.setParameter(widgets.get("Appointments:"), "05f2ad92-1cc8-4cec-bf54-9cac0200746d"); } @Override @@ -166,8 +166,8 @@ public String[] widgetLabelsForEdit() { } public void setupEditRequest(MockHttpServletRequest request, Map widgets) { - request.addParameter(widgets.get("Appointments:"), ""); - request.addParameter(widgets.get("Appointments:").replace("_1", "_2"), + request.setParameter(widgets.get("Appointments:"), ""); + request.setParameter(widgets.get("Appointments:").replace("_1", "_2"), "75504r42-3ca8-11e3-bf2b-0800271c1111"); } @@ -203,7 +203,6 @@ public void testEditedResults(SubmissionResults results) { Appointment appointment1 = Context.getService(AppointmentsService.class) .getAppointmentByUuid("05f2ad92-1cc8-4cec-bf54-9cac0200746d"); Assert.assertEquals(AppointmentStatus.CheckedIn, appointment1.getStatus()); // Note that we are NOT changing the status back to Scheduled - // TODO debug after testing IRL Assert.assertEquals(0, appointment1.getFulfillingEncounters().size()); // but encounter should be removed Appointment appointment2 = Context.getService(AppointmentsService.class) @@ -232,10 +231,10 @@ public String[] widgetLabels() { @Override public void setupRequest(MockHttpServletRequest request, Map widgets) { - request.addParameter(widgets.get("Date:"), dateAsString(date)); - request.addParameter(widgets.get("Location:"), "2"); - request.addParameter(widgets.get("Provider:"), "502"); - request.addParameter(widgets.get("Appointments:"), "05f2ad92-1cc8-4cec-bf54-9cac0200746d"); + request.setParameter(widgets.get("Date:"), dateAsString(date)); + request.setParameter(widgets.get("Location:"), "2"); + request.setParameter(widgets.get("Provider:"), "502"); + request.setParameter(widgets.get("Appointments:"), "05f2ad92-1cc8-4cec-bf54-9cac0200746d"); } @Override @@ -250,7 +249,7 @@ public String[] widgetLabelsForEdit() { public void setupEditRequest(MockHttpServletRequest request, Map widgets) { // upon edit keep the same checked - request.addParameter(widgets.get("Appointments:"), "05f2ad92-1cc8-4cec-bf54-9cac0200746d"); + request.setParameter(widgets.get("Appointments:"), "05f2ad92-1cc8-4cec-bf54-9cac0200746d"); } @Override @@ -280,8 +279,3 @@ public void testEditedResults(SubmissionResults results) { }.run(); } } - -// TODOS: make sure test to remove works -// TODOs: review specs -// TODOs: add to check in form for SL and format/test IRL! -// TODOs ticket separate functionality to pass in appointment to automatically mark as checked in diff --git a/api/src/main/java/org/openmrs/module/htmlformentry/FormEntryContext.java b/api/src/main/java/org/openmrs/module/htmlformentry/FormEntryContext.java index 59acbe7eb..7a828d498 100644 --- a/api/src/main/java/org/openmrs/module/htmlformentry/FormEntryContext.java +++ b/api/src/main/java/org/openmrs/module/htmlformentry/FormEntryContext.java @@ -146,12 +146,14 @@ public Mode getMode() { private Integer sequenceNextVal = 1; /** - * Registers a widget within the Context + * Registers a widget within the Context If a field id is passed in, register with that field id, + * otherwise generate a unique field id using a sequence number and appending 'w' to the front + * (Generally you do not want to pass in a field id, but rather defer to this method to generate one + * for you) * * @param widget the widget to register * @return the field id used to identify this widget in the HTML Form */ - // TODO update documentation public String registerWidget(Widget widget, String fieldName) { if (fieldNames.containsKey(widget)) throw new IllegalArgumentException("This widget is already registered"); @@ -171,7 +173,9 @@ public String registerWidget(Widget widget, String fieldName) { return fieldName; } - // TODO document + /** + * Registeres a widget within the Context, generating a unique field id + */ public String registerWidget(Widget widget) { return registerWidget(widget, null); } diff --git a/api/src/main/java/org/openmrs/module/htmlformentry/FormEntrySession.java b/api/src/main/java/org/openmrs/module/htmlformentry/FormEntrySession.java index 1dc08d95b..1b5618ba6 100644 --- a/api/src/main/java/org/openmrs/module/htmlformentry/FormEntrySession.java +++ b/api/src/main/java/org/openmrs/module/htmlformentry/FormEntrySession.java @@ -652,7 +652,7 @@ public void applyActions() throws BadFormDesignException { } else { appointment.setFulfillingEncounters(Collections.singleton(encounter)); } - Context.getService(AppointmentsService.class).validateAndSave(appointment); + Context.getService(AppointmentsService.class).validateAndSave(() -> appointment); } } @@ -660,7 +660,7 @@ public void applyActions() throws BadFormDesignException { for (Appointment appointment : submissionActions.getAppointmentsToDisassociateFromEncounter()) { if (appointment.getFulfillingEncounters() != null) { appointment.getFulfillingEncounters().remove(encounter); - Context.getService(AppointmentsService.class).validateAndSave(appointment); + Context.getService(AppointmentsService.class).validateAndSave(() -> appointment); } } } @@ -1015,7 +1015,7 @@ public void applyActions() throws BadFormDesignException { exitFromCareProperty.getReasonExitConcept()); } } - + // handle any custom actions (for an example of a custom action, see: https://github.com/PIH/openmrs-module-appointmentschedulingui/commit/e2cda8de1caa8a45d319ae4fbf7714c90c9adb8b) if (submissionActions.getCustomFormSubmissionActions() != null) { for (CustomFormSubmissionAction customFormSubmissionAction : submissionActions diff --git a/api/src/main/java/org/openmrs/module/htmlformentry/HtmlFormEntryUtil.java b/api/src/main/java/org/openmrs/module/htmlformentry/HtmlFormEntryUtil.java index 55f9139e1..a4c7f9b0b 100644 --- a/api/src/main/java/org/openmrs/module/htmlformentry/HtmlFormEntryUtil.java +++ b/api/src/main/java/org/openmrs/module/htmlformentry/HtmlFormEntryUtil.java @@ -2313,6 +2313,19 @@ public static Date clearTimeComponent(Date date) { return cal.getTime(); } + /** + * @return a SimpleDateFormat object for the current locale, using the global property + */ + public static SimpleDateFormat getDateTimeFormat() { + String df = Context.getAdministrationService().getGlobalProperty(HtmlFormEntryConstants.GP_FORMATTER_DATETIME, + "yyyy-MM-dd, HH:mm:ss"); + if (StringUtils.isNotBlank(df)) { + return new SimpleDateFormat(df, Context.getLocale()); + } else { + return Context.getDateTimeFormat(); + } + } + /** * @param date the date to check * @return true if the given date is not at midnight @@ -2696,6 +2709,22 @@ public static Provider getProvider(String id) { return provider; } + /** + * Given a provider, returns the provider name by first looking at the name of any associated person + * and, if none, falling back to provider.getName() + * + * @param provider + * @return + */ + public static String getProviderName(Provider provider) { + if (provider != null) { + return provider.getPerson() != null + ? HtmlFormEntryUtil.getFullNameWithFamilyNameFirst(provider.getPerson().getPersonName()) + : provider.getName(); + } + return ""; + } + /** * Convenience method to get all the names of this PersonName and concatonating them together with * family name compoenents first, separated by a comma from given and middle names. If any part of @@ -2858,4 +2887,5 @@ private static List getProviderFieldsToSearch(Provider provider) { } return ret; } + } diff --git a/api/src/main/java/org/openmrs/module/htmlformentry/element/AppointmentsElement.java b/api/src/main/java/org/openmrs/module/htmlformentry/element/AppointmentsElement.java index 1227ea7a5..7f962e2be 100644 --- a/api/src/main/java/org/openmrs/module/htmlformentry/element/AppointmentsElement.java +++ b/api/src/main/java/org/openmrs/module/htmlformentry/element/AppointmentsElement.java @@ -6,6 +6,7 @@ import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.Map; import org.joda.time.DateTime; import org.openmrs.Patient; @@ -26,26 +27,36 @@ public class AppointmentsElement implements HtmlGeneratorElement, FormSubmission private List appointments = new ArrayList<>(); - public AppointmentsElement(FormEntryContext context) { + private String clazz; + + public AppointmentsElement(FormEntryContext context, Map parameters) { + + if (parameters.get("class") != null) { + clazz = parameters.get("class"); + } + Patient patient = context.getExistingPatient(); if (patient != null) { // first, get all scheduled appointments for this patient AppointmentSearchRequest request = new AppointmentSearchRequest(); request.setPatientUuid(patient.getUuid()); - request.setStartDate(new DateTime().minusYears(1000).toDate()); // TODO hack, we want all appts for patient regardless of start date, but start date is required, this will start to fail in a thousand years + request.setStartDate(new DateTime().minusYears(1000).toDate()); // TODO hack, we want all appts for patient regardless of start date, but the search method always returns null if start date is null; this will start to fail in a thousand years appointments = Context.getService(AppointmentsService.class).search(request); appointments.sort(Comparator.comparing(Appointment::getStartDateTime).reversed()); - appointments.removeIf(appointment -> appointment.getStatus() != AppointmentStatus.Scheduled + + // in VIEW mode, only show appointments linked to encounter; in EDIT mode show those linked to encounter and all scheduled appts + appointments.removeIf(appointment -> (context.getMode() == FormEntryContext.Mode.VIEW + || appointment.getStatus() != AppointmentStatus.Scheduled) && (appointment.getFulfillingEncounters() == null - && !appointment.getFulfillingEncounters().contains(context.getExistingEncounter()))); + || !appointment.getFulfillingEncounters().contains(context.getExistingEncounter()))); } } @Override public String generateHtml(FormEntryContext context) { - appointmentsWidget = new AppointmentsWidget(appointments, context); + appointmentsWidget = new AppointmentsWidget(appointments, context, clazz); return appointmentsWidget.generateHtml(context); } diff --git a/api/src/main/java/org/openmrs/module/htmlformentry/handler/AppointmentsTagHandler.java b/api/src/main/java/org/openmrs/module/htmlformentry/handler/AppointmentsTagHandler.java index 15eb2ee19..141089a8b 100644 --- a/api/src/main/java/org/openmrs/module/htmlformentry/handler/AppointmentsTagHandler.java +++ b/api/src/main/java/org/openmrs/module/htmlformentry/handler/AppointmentsTagHandler.java @@ -20,7 +20,7 @@ protected List createAttributeDescriptors() { @Override protected String getSubstitution(FormEntrySession session, FormSubmissionController controllerActions, Map parameters) { - AppointmentsElement element = new AppointmentsElement(session.getContext()); + AppointmentsElement element = new AppointmentsElement(session.getContext(), parameters); session.getSubmissionController().addAction(element); return element.generateHtml(session.getContext()); } diff --git a/api/src/main/java/org/openmrs/module/htmlformentry/widget/AppointmentsWidget.java b/api/src/main/java/org/openmrs/module/htmlformentry/widget/AppointmentsWidget.java index bb6ed89c1..021d41281 100644 --- a/api/src/main/java/org/openmrs/module/htmlformentry/widget/AppointmentsWidget.java +++ b/api/src/main/java/org/openmrs/module/htmlformentry/widget/AppointmentsWidget.java @@ -4,9 +4,12 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; +import org.joda.time.DateTime; import org.openmrs.api.context.Context; import org.openmrs.module.appointments.model.Appointment; import org.openmrs.module.htmlformentry.FormEntryContext; @@ -20,17 +23,34 @@ public class AppointmentsWidget implements Widget { private List checkboxWidgets = new ArrayList(); - public AppointmentsWidget(List appointments, FormEntryContext context) { + private String clazz = null; + + public AppointmentsWidget(List appointments, FormEntryContext context, String clazz) { + + this.clazz = clazz; + this.appointments = appointments; String fieldName = context.registerWidget(this); - // TODO: document why we need the special registration for form consistency + // note that we are relying on the register widget to generate a single unique field name, + // and then we are appending _1, _2, _3, etc to that field name to create unique field names for each checkbox + // this is to ensure that this widet consistently increments the field name sequential value once, + // for consistency among page reloads; otherws, if the number of matches appointments changed between, for example, + // when the form was opened and the form was saved, widget names might be inconsistent, wreaking havoc on the form int i = 1; for (Appointment appointment : appointments) { + + // compare dates so that we can highlight any that match the encounter darw + boolean appointmentDateMatchesEncounterDate = appointment.getStartDateTime() != null + && new DateTime(appointment.getStartDateTime()).withTimeAtStartOfDay() + .equals(new DateTime(context.getBestApproximationOfEncounterDate()).withTimeAtStartOfDay()); + CheckboxWidget checkboxWidget = new CheckboxWidget(); - checkboxWidget.setLabel( - dateTimeFormat().format(appointment.getStartDateTime()) + " - " + renderProviderNames(appointment) + " - " - + (appointment.getLocation() != null ? appointment.getLocation().getName() : "")); + checkboxWidget.setLabel((appointmentDateMatchesEncounterDate ? "" : "") + + HtmlFormEntryUtil.getDateTimeFormat().format(appointment.getStartDateTime()) + " - " + + renderProviderNames(appointment) + " - " + + (appointment.getLocation() != null ? appointment.getLocation().getName() : "") + + (appointmentDateMatchesEncounterDate ? "" : "")); checkboxWidget.setValue(appointment.getUuid()); if (appointment.getFulfillingEncounters() != null && context.getExistingEncounter() != null && appointment.getFulfillingEncounters().contains(context.getExistingEncounter())) { @@ -44,18 +64,19 @@ public AppointmentsWidget(List appointments, FormEntryContext conte @Override public void setInitialValue(Object initialValue) { - // TODO? + // the constructor takes care of setting the initial value for each checkbox } - + @Override public String generateHtml(FormEntryContext context) { if (appointments == null || appointments.isEmpty()) { - return "No appointments found"; // TODO translate, style + return Context.getMessageSourceService().getMessage("appointmentsui.noAppointmentsFound"); } return checkboxWidgets.stream().map(checkboxWidget -> { return checkboxWidget.generateHtml(context); - }).collect(Collectors.joining()); + }).map((html) -> "" + html + "") + .collect(Collectors.joining()); } @Override @@ -70,25 +91,12 @@ public Object getValue(FormEntryContext context, HttpServletRequest request) { return selectedAppointmentUuids; } - // TODO move to util method? - private SimpleDateFormat dateTimeFormat() { - String df = Context.getAdministrationService().getGlobalProperty(HtmlFormEntryConstants.GP_FORMATTER_DATETIME, - "yyyy-MM-dd, HH:mm:ss"); - if (StringUtils.hasText(df)) { - return new SimpleDateFormat(df, Context.getLocale()); - } else { - return Context.getDateTimeFormat(); - } - } - private String renderProviderNames(Appointment appointment) { if (appointment.getProviders() != null) { return appointment.getProviders().stream().map(provider -> { - return provider.getProvider().getPerson() != null ? HtmlFormEntryUtil.getFullNameWithFamilyNameFirst( - provider.getProvider().getPerson().getPersonName()) : provider.getProvider().getName(); + return HtmlFormEntryUtil.getProviderName(provider.getProvider()); }).collect(Collectors.joining("; ")); } return ""; - } } diff --git a/api/src/main/java/org/openmrs/module/htmlformentry/widget/ProviderWidget.java b/api/src/main/java/org/openmrs/module/htmlformentry/widget/ProviderWidget.java index f24c2a58a..f7573220d 100644 --- a/api/src/main/java/org/openmrs/module/htmlformentry/widget/ProviderWidget.java +++ b/api/src/main/java/org/openmrs/module/htmlformentry/widget/ProviderWidget.java @@ -104,10 +104,7 @@ public String generateHtml(FormEntryContext context) { sb.append("\n"); } } diff --git a/api/src/main/resources/messages.properties b/api/src/main/resources/messages.properties index 318be2c0b..3fdc04432 100644 --- a/api/src/main/resources/messages.properties +++ b/api/src/main/resources/messages.properties @@ -179,3 +179,5 @@ htmlformentry.conditionui.condition.label=Condition htmlformentry.conditionui.active.label=Active htmlformentry.conditionui.inactive.label=Inactive htmlformentry.conditionui.additionalDetail.label=Additional Detail + +htmlformentry.appointments.noAppointmentsFound=No appointments found