diff --git a/api-tests/pom.xml b/api-tests/pom.xml index 7e231352a..786767c64 100644 --- a/api-tests/pom.xml +++ b/api-tests/pom.xml @@ -61,6 +61,10 @@ org.openmrs.module providermanagement-api + + org.bahmni.module + appointments-api + org.openmrs.test openmrs-test diff --git a/api-tests/src/test/java/org/openmrs/module/htmlformentry/AppointmentsTagTest.java b/api-tests/src/test/java/org/openmrs/module/htmlformentry/AppointmentsTagTest.java new file mode 100644 index 000000000..8f6f99c16 --- /dev/null +++ b/api-tests/src/test/java/org/openmrs/module/htmlformentry/AppointmentsTagTest.java @@ -0,0 +1,281 @@ +package org.openmrs.module.htmlformentry; + +import java.util.Date; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.openmrs.api.context.Context; +import org.openmrs.module.appointments.model.Appointment; +import org.openmrs.module.appointments.model.AppointmentStatus; +import org.openmrs.module.appointments.service.AppointmentsService; +import org.springframework.mock.web.MockHttpServletRequest; + +public class AppointmentsTagTest extends BaseHtmlFormEntryTest { + + @Before + public void loadTestData() throws Exception { + executeVersionedDataSet("org/openmrs/module/htmlformentry/data/RegressionTest-data-openmrs-2.3.xml"); + executeVersionedDataSet("org/openmrs/module/htmlformentry/data/appointmentCheckInTest.xml"); + } + + @Test + public void testAppointmentCheckInTag_shouldDisplayCheckboxesForScheduledAppointmentsForPatient() throws Exception { + new RegressionTestHelper() { + + @Override + public String getFormName() { + return "appointmentCheckInForm"; + } + + @Override + public String[] widgetLabels() { + return new String[] { "Date:", "Location:", "Provider:", "Appointments:" }; + } + + @Override + public void testBlankFormHtml(String html) { + TestUtil.assertFuzzyContains("value=\"05f2ad92-1cc8-4cec-bf54-9cac0200746d\"/>", html); + TestUtil.assertFuzzyContains("2024-08-16, 13:00:00 - of Cos, Hippocrates - Never Never Land", html); + TestUtil.assertFuzzyContains("value=\"75504r42-3ca8-11e3-bf2b-0800271c1111\"/>", html); + TestUtil.assertFuzzyContains("2024-08-15, 12:00:00 - of Cos, Hippocrates - Xanadu", html); + } + }.run(); + } + + @Test + public void testAppointmentCheckInTag_shouldCheckPatientInForSingleAppointment() throws Exception { + final Date date = new Date(); + new RegressionTestHelper() { + + @Override + public String getFormName() { + return "appointmentCheckInForm"; + } + + @Override + public String[] widgetLabels() { + return new String[] { "Date:", "Location:", "Provider:", "Appointments:" }; + } + + @Override + public void setupRequest(MockHttpServletRequest request, Map widgets) { + 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 + public void testResults(SubmissionResults results) { + results.assertNoErrors(); + results.assertEncounterCreated(); + Appointment appointment1 = Context.getService(AppointmentsService.class) + .getAppointmentByUuid("05f2ad92-1cc8-4cec-bf54-9cac0200746d"); + Assert.assertEquals(AppointmentStatus.CheckedIn, appointment1.getStatus()); + Assert.assertEquals(results.getEncounterCreated(), + appointment1.getFulfillingEncounters().stream().findFirst().get()); + + // just confirm the other appointment has not been checked in or linked to the encounter + Appointment appointment2 = Context.getService(AppointmentsService.class) + .getAppointmentByUuid("75504r42-3ca8-11e3-bf2b-0800271c1111"); + Assert.assertEquals(AppointmentStatus.Scheduled, appointment2.getStatus()); + Assert.assertEquals(0, appointment2.getFulfillingEncounters().size()); + } + }.run(); + } + + @Test + public void testAppointmentCheckInTag_shouldCheckPatientInForMultipleAppointments() throws Exception { + final Date date = new Date(); + new RegressionTestHelper() { + + @Override + public String getFormName() { + return "appointmentCheckInForm"; + } + + @Override + public String[] widgetLabels() { + return new String[] { "Date:", "Location:", "Provider:", "Appointments:" }; + } + + @Override + public void setupRequest(MockHttpServletRequest request, Map widgets) { + 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"); + } + + @Override + public void testResults(SubmissionResults results) { + results.assertNoErrors(); + results.assertEncounterCreated(); + + Appointment appointment1 = Context.getService(AppointmentsService.class) + .getAppointmentByUuid("05f2ad92-1cc8-4cec-bf54-9cac0200746d"); + Assert.assertEquals(AppointmentStatus.CheckedIn, appointment1.getStatus()); + Assert.assertEquals(results.getEncounterCreated(), + appointment1.getFulfillingEncounters().stream().findFirst().get()); + + Appointment appointment2 = Context.getService(AppointmentsService.class) + .getAppointmentByUuid("75504r42-3ca8-11e3-bf2b-0800271c1111"); + Assert.assertEquals(AppointmentStatus.CheckedIn, appointment2.getStatus()); + Assert.assertEquals(results.getEncounterCreated(), + appointment2.getFulfillingEncounters().stream().findFirst().get()); + } + }.run(); + } + + @Test + public void testAppointmentCheckInTag_editShouldDisassociateEncounterFromAppointmentAndAssociateNewEncounter() + throws Exception { + final Date date = new Date(); + new RegressionTestHelper() { + + @Override + public String getFormName() { + return "appointmentCheckInForm"; + } + + @Override + public String[] widgetLabels() { + return new String[] { "Date:", "Location:", "Provider:", "Appointments:" }; + } + + @Override + public void setupRequest(MockHttpServletRequest request, Map widgets) { + 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 + public boolean doEditEncounter() { + return true; + } + + @Override + public String[] widgetLabelsForEdit() { + return new String[] { "Appointments:" }; + } + + public void setupEditRequest(MockHttpServletRequest request, Map widgets) { + request.setParameter(widgets.get("Appointments:"), ""); + request.setParameter(widgets.get("Appointments:").replace("_1", "_2"), + "75504r42-3ca8-11e3-bf2b-0800271c1111"); + } + + @Override + public void testResults(SubmissionResults results) { + results.assertNoErrors(); + results.assertEncounterCreated(); + + // sanity check + Appointment appointment1 = Context.getService(AppointmentsService.class) + .getAppointmentByUuid("05f2ad92-1cc8-4cec-bf54-9cac0200746d"); + Assert.assertEquals(AppointmentStatus.CheckedIn, appointment1.getStatus()); + Assert.assertEquals(results.getEncounterCreated(), + appointment1.getFulfillingEncounters().stream().findFirst().get()); + + Appointment appointment2 = Context.getService(AppointmentsService.class) + .getAppointmentByUuid("75504r42-3ca8-11e3-bf2b-0800271c1111"); + Assert.assertEquals(AppointmentStatus.Scheduled, appointment2.getStatus()); + Assert.assertEquals(0, appointment2.getFulfillingEncounters().size()); + } + + // confirm that the associated appointment is now checked by default + @Override + public void testEditFormHtml(String html) { + TestUtil.assertFuzzyContains("value=\"05f2ad92-1cc8-4cec-bf54-9cac0200746d\" checked=\"true\"/>", html); + } + + @Override + public void testEditedResults(SubmissionResults results) { + results.assertNoErrors(); + results.assertEncounterCreated(); + + 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 + Assert.assertEquals(0, appointment1.getFulfillingEncounters().size()); // but encounter should be removed + + Appointment appointment2 = Context.getService(AppointmentsService.class) + .getAppointmentByUuid("75504r42-3ca8-11e3-bf2b-0800271c1111"); + Assert.assertEquals(AppointmentStatus.CheckedIn, appointment2.getStatus()); + Assert.assertEquals(results.getEncounterCreated(), + appointment2.getFulfillingEncounters().stream().findFirst().get()); + } + }.run(); + } + + @Test + public void testAppointmentCheckInTag_editingFormWithoutChangingStatusShouldNotCauseError() throws Exception { + final Date date = new Date(); + new RegressionTestHelper() { + + @Override + public String getFormName() { + return "appointmentCheckInForm"; + } + + @Override + public String[] widgetLabels() { + return new String[] { "Date:", "Location:", "Provider:", "Appointments:" }; + } + + @Override + public void setupRequest(MockHttpServletRequest request, Map widgets) { + 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 + public boolean doEditEncounter() { + return true; + } + + @Override + public String[] widgetLabelsForEdit() { + return new String[] { "Appointments:" }; + } + + public void setupEditRequest(MockHttpServletRequest request, Map widgets) { + // upon edit keep the same checked + request.setParameter(widgets.get("Appointments:"), "05f2ad92-1cc8-4cec-bf54-9cac0200746d"); + } + + @Override + public void testResults(SubmissionResults results) { + results.assertNoErrors(); + results.assertEncounterCreated(); + + // sanity check + Appointment appointment1 = Context.getService(AppointmentsService.class) + .getAppointmentByUuid("05f2ad92-1cc8-4cec-bf54-9cac0200746d"); + Assert.assertEquals(AppointmentStatus.CheckedIn, appointment1.getStatus()); + Assert.assertEquals(results.getEncounterCreated(), + appointment1.getFulfillingEncounters().stream().findFirst().get()); + } + + @Override + public void testEditedResults(SubmissionResults results) { + results.assertNoErrors(); + results.assertEncounterCreated(); + + Appointment appointment1 = Context.getService(AppointmentsService.class) + .getAppointmentByUuid("05f2ad92-1cc8-4cec-bf54-9cac0200746d"); + Assert.assertEquals(AppointmentStatus.CheckedIn, appointment1.getStatus()); + Assert.assertEquals(results.getEncounterCreated(), + appointment1.getFulfillingEncounters().stream().findFirst().get()); + } + }.run(); + } +} diff --git a/api-tests/src/test/resources/test-hibernate.cfg.xml b/api-tests/src/test/resources/test-hibernate.cfg.xml index a237592da..e99851903 100644 --- a/api-tests/src/test/resources/test-hibernate.cfg.xml +++ b/api-tests/src/test/resources/test-hibernate.cfg.xml @@ -5,6 +5,15 @@ + + + + + + + + + diff --git a/api/pom.xml b/api/pom.xml index bb0f99977..b79f82cbd 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -45,6 +45,11 @@ org.openmrs.module providermanagement-api + + org.bahmni.module + appointments-api + + 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 4690f8885..1ad95633a 100644 --- a/api/src/main/java/org/openmrs/module/htmlformentry/FormEntryContext.java +++ b/api/src/main/java/org/openmrs/module/htmlformentry/FormEntryContext.java @@ -146,26 +146,40 @@ 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 */ - public String registerWidget(Widget widget) { + public String registerWidget(Widget widget, String fieldName) { if (fieldNames.containsKey(widget)) throw new IllegalArgumentException("This widget is already registered"); - int thisVal = 0; - synchronized (sequenceNextVal) { - thisVal = sequenceNextVal; - sequenceNextVal = sequenceNextVal + 1; + + if (StringUtils.isBlank(fieldName)) { + int thisVal = 0; + synchronized (sequenceNextVal) { + thisVal = sequenceNextVal; + sequenceNextVal = sequenceNextVal + 1; + } + fieldName = "w" + thisVal; } - String fieldName = "w" + thisVal; + fieldNames.put(widget, fieldName); if (log.isTraceEnabled()) log.trace("Registered widget " + widget.getClass() + " as " + fieldName); return fieldName; } + /** + * Registers a widget within the Context, generating a unique field id + */ + public String registerWidget(Widget widget) { + return registerWidget(widget, null); + } + /** * Registers an error widget within the Context * 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 baadf0542..374eb96a2 100644 --- a/api/src/main/java/org/openmrs/module/htmlformentry/FormEntrySession.java +++ b/api/src/main/java/org/openmrs/module/htmlformentry/FormEntrySession.java @@ -29,6 +29,7 @@ import org.openmrs.api.ObsService; import org.openmrs.api.context.Context; import org.openmrs.module.htmlformentry.FormEntryContext.Mode; +import org.openmrs.module.htmlformentry.appointment.AppointmentsAbstractor; import org.openmrs.module.htmlformentry.property.ExitFromCareProperty; import org.openmrs.module.htmlformentry.velocity.VelocityContextContentProvider; import org.openmrs.module.htmlformentry.widget.AutocompleteWidget; @@ -636,6 +637,17 @@ public void applyActions() throws BadFormDesignException { } } + // handle appointments (needs to happen after encounter is saved?) + if (submissionActions.getAppointmentsToMarkCheckedInAndAssociateWithEncounter() != null) { + new AppointmentsAbstractor().markAppointmentsAsCheckedInAndAssociateWithEncounter( + submissionActions.getAppointmentsToMarkCheckedInAndAssociateWithEncounter(), encounter); + } + + if (submissionActions.getAppointmentsToDisassociateFromEncounter() != null) { + new AppointmentsAbstractor().disassociateAppointmentsFromEncounter( + submissionActions.getAppointmentsToDisassociateFromEncounter(), encounter); + } + //deal with relationships if (submissionActions.getRelationshipsToCreate() != null) { for (Relationship r : submissionActions.getRelationshipsToCreate()) { diff --git a/api/src/main/java/org/openmrs/module/htmlformentry/FormSubmissionActions.java b/api/src/main/java/org/openmrs/module/htmlformentry/FormSubmissionActions.java index d74cdcf2c..c42b96a59 100644 --- a/api/src/main/java/org/openmrs/module/htmlformentry/FormSubmissionActions.java +++ b/api/src/main/java/org/openmrs/module/htmlformentry/FormSubmissionActions.java @@ -79,6 +79,10 @@ public class FormSubmissionActions { private List identifiersToVoid = new Vector(); + private List appointmentsToMarkCheckedInAndAssociateWithEncounter = new Vector(); + + private List appointmentsToDisassociateFromEncounter = new Vector(); + private ExitFromCareProperty exitFromCareProperty; private List customFormSubmissionActions; @@ -946,4 +950,21 @@ public List getCustomFormSubmissionActions() { public void setCustomFormSubmissionActions(List customFormSubmissionActions) { this.customFormSubmissionActions = customFormSubmissionActions; } + + public List getAppointmentsToMarkCheckedInAndAssociateWithEncounter() { + return appointmentsToMarkCheckedInAndAssociateWithEncounter; + } + + public void setAppointmentsToMarkCheckedInAndAssociateWithEncounter( + List appointmentsToMarkCheckedInAndAssociateWithEncounter) { + this.appointmentsToMarkCheckedInAndAssociateWithEncounter = appointmentsToMarkCheckedInAndAssociateWithEncounter; + } + + public List getAppointmentsToDisassociateFromEncounter() { + return appointmentsToDisassociateFromEncounter; + } + + public void setAppointmentsToDisassociateFromEncounter(List appointmentsToDisassociateFromEncounter) { + this.appointmentsToDisassociateFromEncounter = appointmentsToDisassociateFromEncounter; + } } 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/ObsGroupComponent.java b/api/src/main/java/org/openmrs/module/htmlformentry/ObsGroupComponent.java index 2c7acfaa3..695360915 100644 --- a/api/src/main/java/org/openmrs/module/htmlformentry/ObsGroupComponent.java +++ b/api/src/main/java/org/openmrs/module/htmlformentry/ObsGroupComponent.java @@ -92,7 +92,7 @@ public Drug getAnswerDrug() { public void setAnswerDrug(Drug answerDrug) { this.answerDrug = answerDrug; } - + public static int supportingRank(List obsGroupComponents, Set obsSet) { int rank = 0; diff --git a/api/src/main/java/org/openmrs/module/htmlformentry/appointment/AppointmentsAbstractor.java b/api/src/main/java/org/openmrs/module/htmlformentry/appointment/AppointmentsAbstractor.java new file mode 100644 index 000000000..aae89e893 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/htmlformentry/appointment/AppointmentsAbstractor.java @@ -0,0 +1,46 @@ +package org.openmrs.module.htmlformentry.appointment; + +import java.util.Collections; +import java.util.List; + +import org.openmrs.Encounter; +import org.openmrs.api.context.Context; +import org.openmrs.module.appointments.model.Appointment; +import org.openmrs.module.appointments.model.AppointmentStatus; +import org.openmrs.module.appointments.service.AppointmentsService; + +/** + * Calls to the AppointmentService are abstracted here, rather than directly accessed in the Form + * Entry Session to avoid runtime class loading issues in instances where the Appointments module is + * not present + */ +public class AppointmentsAbstractor { + + public void markAppointmentsAsCheckedInAndAssociateWithEncounter(List appointments, Encounter encounter) { + for (Object appointmentObject : appointments) { + Appointment appointment = (Appointment) appointmentObject; + if (appointment.getStatus() == AppointmentStatus.Scheduled) { + Context.getService(AppointmentsService.class).changeStatus(appointment, + AppointmentStatus.CheckedIn.toString(), encounter.getEncounterDatetime()); + } + if (appointment.getFulfillingEncounters() != null) { + appointment.getFulfillingEncounters().add(encounter); + } else { + appointment.setFulfillingEncounters(Collections.singleton(encounter)); + } + // see: https://bahmni.atlassian.net/browse/BAH-3855 for why we need to call the Supplier version of validateAndSave + Context.getService(AppointmentsService.class).validateAndSave(() -> appointment); + } + } + + public void disassociateAppointmentsFromEncounter(List appointments, Encounter encounter) { + for (Object appointmentObject : appointments) { + Appointment appointment = (Appointment) appointmentObject; + if (appointment.getFulfillingEncounters() != null) { + appointment.getFulfillingEncounters().remove(encounter); + // see: https://bahmni.atlassian.net/browse/BAH-3855 for why we need to call the Supplier version of validateAndSave + Context.getService(AppointmentsService.class).validateAndSave(() -> appointment); + } + } + } +} 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 new file mode 100644 index 000000000..1defa9c0f --- /dev/null +++ b/api/src/main/java/org/openmrs/module/htmlformentry/element/AppointmentsElement.java @@ -0,0 +1,105 @@ +package org.openmrs.module.htmlformentry.element; + +import javax.servlet.http.HttpServletRequest; + +import java.util.ArrayList; +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; +import org.openmrs.api.context.Context; +import org.openmrs.module.appointments.model.Appointment; +import org.openmrs.module.appointments.model.AppointmentSearchRequest; +import org.openmrs.module.appointments.model.AppointmentStatus; +import org.openmrs.module.appointments.service.AppointmentsService; +import org.openmrs.module.htmlformentry.FormEntryContext; +import org.openmrs.module.htmlformentry.FormEntrySession; +import org.openmrs.module.htmlformentry.FormSubmissionError; +import org.openmrs.module.htmlformentry.action.FormSubmissionControllerAction; +import org.openmrs.module.htmlformentry.widget.AppointmentsWidget; + +public class AppointmentsElement implements HtmlGeneratorElement, FormSubmissionControllerAction { + + private AppointmentsWidget appointmentsWidget; + + private List appointments = new ArrayList<>(); + + 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()); // hack, see: https://bahmni.atlassian.net/browse/BAH-3867; this will start to fail in a thousand years + appointments = Context.getService(AppointmentsService.class).search(request); + + appointments.sort(Comparator.comparing(Appointment::getStartDateTime).reversed()); + + // in VIEW mode, only show appointments linked to encounter; + if (context.getMode() == FormEntryContext.Mode.VIEW) { + appointments.removeIf(appointment -> appointment.getFulfillingEncounters() == null + || !appointment.getFulfillingEncounters().contains(context.getExistingEncounter())); + } + // in ENTER and EDIT mode show those linked to encounter, as well as all scheduled appts + else { + appointments.removeIf(appointment -> (appointment.getFulfillingEncounters() == null + || !appointment.getFulfillingEncounters().contains(context.getExistingEncounter())) + && appointment.getStatus() != AppointmentStatus.Scheduled); + } + } + } + + @Override + public String generateHtml(FormEntryContext context) { + appointmentsWidget = new AppointmentsWidget(appointments, context, clazz); + return appointmentsWidget.generateHtml(context); + } + + @Override + public Collection validateSubmission(FormEntryContext context, HttpServletRequest submission) { + return null; + } + + @Override + public void handleSubmission(FormEntrySession session, HttpServletRequest submission) { + List selectedAppointmentUuids = (List) appointmentsWidget.getValue(session.getContext(), submission); + List appointmentsToMarkCheckedIn = new ArrayList<>(); + List appointmentsToDisassociateFromEncounter = new ArrayList<>(); + + // find appointments that need to be marked as checked in + for (String uuid : selectedAppointmentUuids) { + appointments.stream().filter(appointment -> appointment.getUuid().equals(uuid)).findFirst() + .ifPresent(appointment -> { + appointmentsToMarkCheckedIn.add(appointment); + }); + } + if (appointmentsToMarkCheckedIn.size() > 0) { + session.getSubmissionActions() + .setAppointmentsToMarkCheckedInAndAssociateWithEncounter(appointmentsToMarkCheckedIn); + } + + // find appointments that need to be disassociated from the encounter + appointments.stream() + .filter(appointment -> (!selectedAppointmentUuids.contains(appointment.getUuid()) + && appointment.getFulfillingEncounters() != null + && appointment.getFulfillingEncounters().contains(session.getEncounter()))) + .forEach(appointment -> { + appointmentsToDisassociateFromEncounter.add(appointment); + }); + if (appointmentsToDisassociateFromEncounter.size() > 0) { + session.getSubmissionActions() + .setAppointmentsToDisassociateFromEncounter(appointmentsToDisassociateFromEncounter); + } + } +} 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 new file mode 100644 index 000000000..141089a8b --- /dev/null +++ b/api/src/main/java/org/openmrs/module/htmlformentry/handler/AppointmentsTagHandler.java @@ -0,0 +1,27 @@ +package org.openmrs.module.htmlformentry.handler; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.openmrs.module.htmlformentry.FormEntrySession; +import org.openmrs.module.htmlformentry.FormSubmissionController; +import org.openmrs.module.htmlformentry.element.AppointmentsElement; + +public class AppointmentsTagHandler extends SubstitutionTagHandler { + + @Override + protected List createAttributeDescriptors() { + List attributeDescriptors = new ArrayList(); + return Collections.unmodifiableList(attributeDescriptors); + } + + @Override + protected String getSubstitution(FormEntrySession session, FormSubmissionController controllerActions, + Map parameters) { + 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 new file mode 100644 index 000000000..a3cfe78cb --- /dev/null +++ b/api/src/main/java/org/openmrs/module/htmlformentry/widget/AppointmentsWidget.java @@ -0,0 +1,96 @@ +package org.openmrs.module.htmlformentry.widget; + +import javax.servlet.http.HttpServletRequest; + +import java.util.ArrayList; +import java.util.List; +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; +import org.openmrs.module.htmlformentry.HtmlFormEntryUtil; + +public class AppointmentsWidget implements Widget { + + private List appointments; + + private List checkboxWidgets = new ArrayList(); + + private String clazz = null; + + public AppointmentsWidget(List appointments, FormEntryContext context, String clazz) { + + this.clazz = clazz; + + this.appointments = appointments; + String fieldName = context.registerWidget(this); + + // note that we are relying on the register widget to generate a single unique field name, + // and then we are appending the uuid to that field name to create unique field names for each checkbox + // this is to ensure that this widget consistently increments the field name sequential value only once, + // otherwise, if the number of matches appointments changed between when the form was opened and the form was saved, + // widget names would be inconsistent, wreaking havoc on the form + 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((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())) { + checkboxWidget.setInitialValue(appointment.getUuid()); + } + context.registerWidget(checkboxWidget, fieldName + "_" + appointment.getId()); + checkboxWidgets.add(checkboxWidget); + } + + } + + @Override + public void setInitialValue(Object initialValue) { + // the constructor takes care of setting the initial value for each checkbox + } + + @Override + public String generateHtml(FormEntryContext context) { + if (appointments == null || appointments.isEmpty()) { + return Context.getMessageSourceService().getMessage("htmlformentry.appointments.noAppointmentsFound"); + } + + return checkboxWidgets.stream().map(checkboxWidget -> { + return checkboxWidget.generateHtml(context); + }).map((html) -> "" + html + "") + .collect(Collectors.joining()); + } + + @Override + public Object getValue(FormEntryContext context, HttpServletRequest request) { + List selectedAppointmentUuids = new ArrayList(); + for (CheckboxWidget checkboxWidget : checkboxWidgets) { + String value = (String) checkboxWidget.getValue(context, request); + if (value != null) { + selectedAppointmentUuids.add(value); + } + } + return selectedAppointmentUuids; + } + + private String renderProviderNames(Appointment appointment) { + if (appointment.getProviders() != null) { + return appointment.getProviders().stream().map(provider -> { + 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 diff --git a/api/src/main/resources/moduleApplicationContext.xml b/api/src/main/resources/moduleApplicationContext.xml index e19f18a58..12061c426 100644 --- a/api/src/main/resources/moduleApplicationContext.xml +++ b/api/src/main/resources/moduleApplicationContext.xml @@ -113,6 +113,9 @@ + + + diff --git a/api/src/test/resources/org/openmrs/module/htmlformentry/data/appointmentCheckInTest.xml b/api/src/test/resources/org/openmrs/module/htmlformentry/data/appointmentCheckInTest.xml new file mode 100644 index 000000000..e4641a9bf --- /dev/null +++ b/api/src/test/resources/org/openmrs/module/htmlformentry/data/appointmentCheckInTest.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/api/src/test/resources/org/openmrs/module/htmlformentry/include/appointmentCheckInForm.xml b/api/src/test/resources/org/openmrs/module/htmlformentry/include/appointmentCheckInForm.xml new file mode 100644 index 000000000..b4321892e --- /dev/null +++ b/api/src/test/resources/org/openmrs/module/htmlformentry/include/appointmentCheckInForm.xml @@ -0,0 +1,8 @@ + + Date: + Location: + Provider: + + Appointments: + + diff --git a/omod/src/main/resources/config.xml b/omod/src/main/resources/config.xml index 7074847cd..b4b6eac98 100644 --- a/omod/src/main/resources/config.xml +++ b/omod/src/main/resources/config.xml @@ -22,6 +22,7 @@ org.openmrs.module.legacyui org.openmrs.module.metadatamapping org.openmrs.module.providermanagement + org.bahmni.module.appointments diff --git a/pom.xml b/pom.xml index c7b9c65f0..2b456d2a0 100644 --- a/pom.xml +++ b/pom.xml @@ -125,6 +125,12 @@ 2.11.0 provided + + org.bahmni.module + appointments-api + 2.0.0-SNAPSHOT + provided + org.codehaus.jackson jackson-mapper-asl