diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/api/section/FacilityHeaderSection.java b/api/src/main/java/org/openmrs/module/patientdocuments/api/section/FacilityHeaderSection.java index 20babbf..fb6a784 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/api/section/FacilityHeaderSection.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/api/section/FacilityHeaderSection.java @@ -37,6 +37,12 @@ public boolean isEnabled() { return true; } + /** Always on (see isEnabled), so config UIs must not offer a toggle. */ + @Override + public boolean isToggleable() { + return false; + } + @Override protected FacilityInfo gatherData(Visit visit) { if (visit.getLocation() == null) { diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/api/section/FooterSection.java b/api/src/main/java/org/openmrs/module/patientdocuments/api/section/FooterSection.java index 4620f87..8837ef2 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/api/section/FooterSection.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/api/section/FooterSection.java @@ -44,6 +44,12 @@ public boolean isEnabled() { return true; } + /** Always on (see isEnabled), so config UIs must not offer a toggle. */ + @Override + public boolean isToggleable() { + return false; + } + @Override protected FooterInfo gatherData(Visit visit) { User currentUser = Context.getAuthenticatedUser(); diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/api/section/PatientInfoSection.java b/api/src/main/java/org/openmrs/module/patientdocuments/api/section/PatientInfoSection.java index 5086117..a519bef 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/api/section/PatientInfoSection.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/api/section/PatientInfoSection.java @@ -41,6 +41,12 @@ public boolean isEnabled() { return true; } + /** Always on (see isEnabled), so config UIs must not offer a toggle. */ + @Override + public boolean isToggleable() { + return false; + } + @Override protected PatientVisitInfo gatherData(Visit visit) { Patient patient = visit.getPatient(); diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/api/section/VisitSummarySection.java b/api/src/main/java/org/openmrs/module/patientdocuments/api/section/VisitSummarySection.java index 5a0932c..8fb1992 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/api/section/VisitSummarySection.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/api/section/VisitSummarySection.java @@ -10,6 +10,8 @@ package org.openmrs.module.patientdocuments.api.section; import org.openmrs.Visit; +import org.openmrs.api.context.Context; +import org.openmrs.module.patientdocuments.common.PatientDocumentsConstants; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -19,6 +21,8 @@ * renderXml() on each enabled section sorted by getOrder(). * Implementing directly (without TypedSection) is valid for sections that don't * fit the gather-then-render flow. + * Config UIs list the same beans via getLabel(), isEnabled(), getOrder() and + * isToggleable() — sections need no extra registration to appear there. */ public interface VisitSummarySection { @@ -44,6 +48,34 @@ default int getOrder() { return Integer.MAX_VALUE; } + /** + * Human-readable name for this section, localized for the current user locale. + * Resolves the same heading message key the PDF rendering uses + * (patientdocuments.visitSummary.section.<key>.heading), falling back to a + * humanized form of getSectionKey() when no message is defined — so downstream + * sections get a usable label without shipping message bundles. + */ + default String getLabel() { + String key = PatientDocumentsConstants.MODULE_ARTIFACT_ID + "." + PatientDocumentsConstants.VISIT_SUMMARY_ID + + ".section." + getSectionKey() + ".heading"; + String fallback = humanizeSectionKey(getSectionKey()); + try { + return Context.getMessageSourceService().getMessage(key, null, fallback, Context.getLocale()); + } + catch (Exception e) { + return fallback; + } + } + + /** + * Whether administrators may switch this section off via configuration. + * Sections that must appear on every PDF (facility identity, patient identity, + * audit footer) return false so config UIs can disable their toggle. + */ + default boolean isToggleable() { + return true; + } + /** * Build this section's XML elements into the document. * The Visit gives access to both the visit and its patient via visit.getPatient(). @@ -51,4 +83,24 @@ default int getOrder() { * TypedSection renders a section-error element on failure for clinical safety visibility. */ void renderXml(Document doc, Element root, Visit visit); + + /** + * Splits a camelCase section key into capitalized words, e.g. "labResults" becomes + * "Lab Results". Used as the getLabel() fallback when no heading message exists. + */ + static String humanizeSectionKey(String sectionKey) { + if (sectionKey == null || sectionKey.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + sb.append(Character.toUpperCase(sectionKey.charAt(0))); + for (int i = 1; i < sectionKey.length(); i++) { + char c = sectionKey.charAt(i); + if (Character.isUpperCase(c)) { + sb.append(' '); + } + sb.append(c); + } + return sb.toString(); + } } diff --git a/api/src/main/resources/messages.properties b/api/src/main/resources/messages.properties index 9665a8d..bf5bead 100644 --- a/api/src/main/resources/messages.properties +++ b/api/src/main/resources/messages.properties @@ -90,7 +90,11 @@ ${project.parent.artifactId}.visitSummary.section.labResults.flag.abnormal=Abnor ${project.parent.artifactId}.visitSummary.section.labResults.flag.critically_abnormal=Critically Abnormal ${project.parent.artifactId}.visitSummary.section.labResults.unknown=Unknown +# Visit summary - Facility Header section +${project.parent.artifactId}.visitSummary.section.facilityHeader.heading=Facility Header + # Visit summary - Footer section +${project.parent.artifactId}.visitSummary.section.footer.heading=Footer ${project.parent.artifactId}.visitSummary.section.footer.lbl.printedBy=Printed by: ${project.parent.artifactId}.visitSummary.section.footer.lbl.systemId=System ID: diff --git a/api/src/main/resources/messages_ar.properties b/api/src/main/resources/messages_ar.properties index 48e80b6..f2d70fc 100644 --- a/api/src/main/resources/messages_ar.properties +++ b/api/src/main/resources/messages_ar.properties @@ -95,7 +95,11 @@ ${project.parent.artifactId}.visitSummary.section.labResults.flag.abnormal=Abnor ${project.parent.artifactId}.visitSummary.section.labResults.flag.critically_abnormal=Critically Abnormal ${project.parent.artifactId}.visitSummary.section.labResults.unknown=Unknown +# Visit summary - Facility Header section +${project.parent.artifactId}.visitSummary.section.facilityHeader.heading=Facility Header + # Visit summary - Footer section (English placeholders pending native-speaker translation) +${project.parent.artifactId}.visitSummary.section.footer.heading=Footer ${project.parent.artifactId}.visitSummary.section.footer.lbl.printedBy=Printed by: ${project.parent.artifactId}.visitSummary.section.footer.lbl.systemId=System ID: diff --git a/api/src/main/resources/messages_fr.properties b/api/src/main/resources/messages_fr.properties index a700d84..ffabaaa 100644 --- a/api/src/main/resources/messages_fr.properties +++ b/api/src/main/resources/messages_fr.properties @@ -88,7 +88,11 @@ ${project.parent.artifactId}.visitSummary.section.labResults.flag.abnormal=Abnor ${project.parent.artifactId}.visitSummary.section.labResults.flag.critically_abnormal=Critically Abnormal ${project.parent.artifactId}.visitSummary.section.labResults.unknown=Unknown +# Visit summary - Facility Header section +${project.parent.artifactId}.visitSummary.section.facilityHeader.heading=Facility Header + # Visit summary - Footer section (English placeholders pending native-speaker translation) +${project.parent.artifactId}.visitSummary.section.footer.heading=Footer ${project.parent.artifactId}.visitSummary.section.footer.lbl.printedBy=Printed by: ${project.parent.artifactId}.visitSummary.section.footer.lbl.systemId=System ID: diff --git a/api/src/test/java/org/openmrs/module/patientdocuments/api/section/VisitSummarySectionDefaultsTest.java b/api/src/test/java/org/openmrs/module/patientdocuments/api/section/VisitSummarySectionDefaultsTest.java new file mode 100644 index 0000000..73e3125 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/patientdocuments/api/section/VisitSummarySectionDefaultsTest.java @@ -0,0 +1,76 @@ +/** + * 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.patientdocuments.api.section; + +import java.util.Locale; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.Visit; +import org.openmrs.api.context.Context; +import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Tests for the VisitSummarySection default methods (getLabel, isToggleable), + * exercised through an anonymous implementation the way a downstream module + * would provide one — no changes in this module, no message bundle. + */ +public class VisitSummarySectionDefaultsTest extends BaseModuleContextSensitiveTest { + + private VisitSummarySection sectionWithKey(String sectionKey) { + return new VisitSummarySection() { + + @Override + public String getSectionKey() { + return sectionKey; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public void renderXml(Document doc, Element root, Visit visit) { + // no-op: these tests exercise getLabel()/isToggleable() only, never rendering + } + }; + } + + @BeforeEach + public void setUp() { + Context.setLocale(Locale.ENGLISH); + } + + @Test + public void getLabel_shouldResolveHeadingMessageForKnownSectionKey() { + Assertions.assertEquals("Vital Signs", sectionWithKey("vitals").getLabel()); + } + + @Test + public void getLabel_shouldHumanizeSectionKeyWhenNoHeadingMessageExists() { + Assertions.assertEquals("Referral Notes", sectionWithKey("referralNotes").getLabel()); + } + + @Test + public void isToggleable_shouldDefaultToTrue() { + Assertions.assertTrue(sectionWithKey("referralNotes").isToggleable()); + } + + @Test + public void humanizeSectionKey_shouldHandleSingleWordAndEmptyKeys() { + Assertions.assertEquals("Vitals", VisitSummarySection.humanizeSectionKey("vitals")); + Assertions.assertEquals("", VisitSummarySection.humanizeSectionKey("")); + Assertions.assertEquals("", VisitSummarySection.humanizeSectionKey(null)); + } +} diff --git a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/VisitSummarySectionsController.java b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/VisitSummarySectionsController.java new file mode 100644 index 0000000..d4f8bd5 --- /dev/null +++ b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/VisitSummarySectionsController.java @@ -0,0 +1,88 @@ +/** + * 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.patientdocuments.web.rest.controller; + +import static org.openmrs.module.patientdocuments.common.PatientDocumentsConstants.MODULE_ARTIFACT_ID; +import static org.openmrs.module.patientdocuments.common.PatientDocumentsConstants.VISIT_SUMMARY_ID; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.openmrs.api.APIAuthenticationException; +import org.openmrs.api.context.Context; +import org.openmrs.api.context.ContextAuthenticationException; +import org.openmrs.module.patientdocuments.api.section.VisitSummarySection; +import org.openmrs.module.webservices.rest.SimpleObject; +import org.openmrs.module.webservices.rest.web.RestConstants; +import org.openmrs.module.webservices.rest.web.v1_0.controller.BaseRestController; +import org.openmrs.util.PrivilegeConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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; + +/** + * REST controller that lists the registered visit summary sections with their + * effective configuration, for admin/config UIs. + *
+ * Accessible at: {@code GET /rest/v1/patientdocuments/visitSummary/sections} + *
+ * Returns the standard REST collection shape — a {@code results} array of
+ * {@code {sectionKey, label, enabled, order, toggleable}} objects sorted by
+ * effective order, the same order the PDF renderer applies.
+ */
+@Controller
+@RequestMapping(value = "/rest/" + RestConstants.VERSION_1 + "/" + MODULE_ARTIFACT_ID + "/" + VISIT_SUMMARY_ID
+ + "/sections")
+public class VisitSummarySectionsController extends BaseRestController {
+
+ private static final Logger logger = LoggerFactory.getLogger(VisitSummarySectionsController.class);
+
+ @Autowired(required = false)
+ private List