Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
75f68bd
Signed-off-by: Dr M H B Ariyaratne <buddhika.ari@gmail.com>
buddhika75 Apr 2, 2026
af097d6
Merge branch '19655-inward-reports-rename-charge-type-summary-add-bht…
buddhika75 Apr 2, 2026
f64912f
Closes #19655
buddhika75 Apr 2, 2026
e1a9e9f
Signed-off-by: Dr M H B Ariyaratne <buddhika.ari@gmail.com>
buddhika75 Apr 2, 2026
8aeccd7
Signed-off-by: Dr M H B Ariyaratne <buddhika.ari@gmail.com>
buddhika75 Apr 2, 2026
11cda0d
Implement three-tier discharge system: clinical, room, and hospital d…
buddhika75 Apr 2, 2026
0eb60f9
Address CodeRabbit review comments on #19655 breakdown report
buddhika75 Apr 2, 2026
90225ad
Fix compilation error: use PatientEncounterFacade for encounter updat…
buddhika75 Apr 2, 2026
1cb3a3f
added paginations in consumption report
PasinduW99 Apr 2, 2026
06c8756
set the correct prepared details of the PO bill
PasinduW99 Apr 2, 2026
a31efab
set the correct prepared details of the PO bill
PasinduW99 Apr 2, 2026
490b77c
set the correct prepared details of the PO bill
PasinduW99 Apr 2, 2026
e220bfa
Merge pull request #19665 from hmislk/#19599-incorrect-datetime-displ…
PasinduW99 Apr 2, 2026
962b84d
Merge pull request #19656 from hmislk/19655-inward-reports-rename-cha…
buddhika75 Apr 2, 2026
fd9a04d
Address CodeRabbit review comments on three-tier discharge system
buddhika75 Apr 2, 2026
39b8169
Merge pull request #19658 from hmislk/19657-three-tier-discharge-system
buddhika75 Apr 2, 2026
efc2013
Fix discharge duplicate encounters grouping by wrong column
buddhika75 Apr 2, 2026
a0c7e19
Merge pull request #19676 from hmislk/19675-fix-discharge-duplicate-e…
buddhika75 Apr 3, 2026
e60de5e
Add InpatientClinicalDischarge to privilege tree and document 3-step …
buddhika75 Apr 3, 2026
f7393bc
Merge pull request #19678 from hmislk/19677-add-inpatient-clinical-di…
buddhika75 Apr 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
### When Working on Inward / Inpatient Module
- [Inward Navigation & Reference](developer_docs/navigation/inward_navigation.md) - Pages, controllers, workflow, open issues

### When Adding a New Privilege
- [Privilege System Guide](developer_docs/security/privilege-system.md) - **All 3 steps required**: enum value + `getCategory()` case + `UserPrivilageController` tree node. Adding only the enum is NOT sufficient — the privilege will be invisible in the admin UI. This was missed for `InpatientClinicalDischarge` (PR #19658, issue #19677).

### When Committing Code
- [Commit Conventions](developer_docs/git/commit-conventions.md) - Message format

Expand Down
16 changes: 14 additions & 2 deletions developer_docs/security/privilege-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,23 @@ HMIS relies on enum-driven privileges to gate sensitive UI flows. Follow this gu
4. **Coordinate with administrators** to assign the new privilege to appropriate roles in production.

### Adding a New Privilege

> 🚨 **All three steps below are mandatory.** Adding only the enum value is NOT sufficient — the privilege will silently never appear in the admin UI, so administrators can never grant it. This was missed for `InpatientClinicalDischarge` in PR #19658, reported in issue #19677.

Follow this checklist whenever a new privilege is required:

**Step 1 — Declare it in `Privileges.java`**
- Inspect `src/main/java/com/divudi/core/data/Privileges.java` and **reuse an existing privilege** if one with matching behaviour already exists. For backward compatibility, *never* rename or edit legacy enum values even if they contain spelling or convention issues.
- If a new entry is necessary, add it to the most relevant section of the enum, keeping the alphabetical or functional grouping that already exists.
- Update the admin privilege maintenance page `src/main/webapp/admin/users/user_privileges.xhtml` so the new privilege can be assigned through the UI.
- Extend the `UserPrivilageController` implementation—specifically the `createPrivilegeHolderTreeNodes()` method—to place the new privilege in the appropriate branch of the tree structure so it renders correctly in the privileges UI.
- Add a `case YourPrivilege:` entry in the `getCategory()` method in the same file so the privilege maps to the correct module.

**Step 2 — Register it in `UserPrivilageController.java`**
- Extend the `UserPrivilageController` implementation—specifically the method that builds the privilege tree—to place the new privilege as a `DefaultTreeNode` in the appropriate branch. Without this step the privilege **will not appear** in the admin UI at `/admin/users/user_privileges.xhtml` and can never be assigned.
- Example: `new DefaultTreeNode(new PrivilegeHolder(Privileges.YourPrivilege, "Display Label"), parentNode);`

**Step 3 — Guard the UI**
- Reference the privilege from XHTML using `rendered="#{webUserController.hasPrivilege('YourPrivilege')}"`.
- Update the admin privilege maintenance page `src/main/webapp/admin/users/user_privileges.xhtml` if static privilege rows are used (tree-based pages handle this automatically via Step 2).

## Testing Checklist
- Log in with an account that lacks the privilege to confirm the UI element stays hidden or disabled.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/*
* Open Hospital Management Information System
*
* Dr M H B Ariyaratne
* Acting Consultant (Health Informatics)
*/
package com.divudi.bean.clinical;

import com.divudi.bean.common.SessionController;
import com.divudi.core.util.JsfUtil;
import com.divudi.core.data.SymanticType;
import com.divudi.core.entity.clinical.ClinicalEntity;
import com.divudi.core.facade.ClinicalEntityFacade;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.ejb.EJB;
import javax.enterprise.context.SessionScoped;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.FacesConverter;
import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

/**
* Manages configurable discharge condition values (e.g. Stable, DAMA, Referred)
* used in the clinical discharge workflow.
*
* @author Dr. M. H. B. Ariyaratne
*/
@Named
@SessionScoped
public class DischargeConditionController implements Serializable {

private static final long serialVersionUID = 1L;

@Inject
SessionController sessionController;

@EJB
private ClinicalEntityFacade ejbFacade;

private ClinicalEntity current;
private List<ClinicalEntity> items = null;
private String selectText = "";

public DischargeConditionController() {
}

public void prepareAdd() {
current = new ClinicalEntity();
current.setSymanticType(SymanticType.Discharge_Condition);
}

public void saveSelected() {
current.setSymanticType(SymanticType.Discharge_Condition);
if (getCurrent().getId() != null && getCurrent().getId() > 0) {
getFacade().edit(current);
JsfUtil.addSuccessMessage("Updated");
} else {
current.setCreatedAt(new Date());
current.setCreater(getSessionController().getLoggedUser());
getFacade().create(current);
JsfUtil.addSuccessMessage("Saved");
}
recreateModel();
getItems();
}

public void delete() {
if (current != null) {
current.setRetired(true);
current.setRetiredAt(new Date());
current.setRetirer(getSessionController().getLoggedUser());
getFacade().edit(current);
JsfUtil.addSuccessMessage("Deleted Successfully");
} else {
JsfUtil.addErrorMessage("Nothing to Delete");
}
recreateModel();
getItems();
current = null;
getCurrent();
}

public List<ClinicalEntity> getItems() {
if (items == null) {
Map m = new HashMap();
m.put("t", SymanticType.Discharge_Condition);
String sql = "select c from ClinicalEntity c where c.retired=false and c.symanticType=:t order by c.name";
items = getFacade().findByJpql(sql, m);
}
return items;
}

public List<ClinicalEntity> completeDischargeConditions(String qry) {
if (qry == null || qry.trim().isEmpty()) {
return new ArrayList<>();
}
List<ClinicalEntity> c;
Map m = new HashMap();
m.put("t", SymanticType.Discharge_Condition);
m.put("n", "%" + qry.toUpperCase() + "%");
String sql = "select c from ClinicalEntity c where c.retired=false and upper(c.name) like :n and c.symanticType=:t order by c.name";
c = getFacade().findByJpql(sql, m, 10);
if (c == null) {
c = new ArrayList<>();
}
return c;
}

public void downloadAsExcel() {
getItems();
FacesContext context = FacesContext.getCurrentInstance();
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("Discharge Conditions");

Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("No");
headerRow.createCell(1).setCellValue("Name");
headerRow.createCell(2).setCellValue("Code");
headerRow.createCell(3).setCellValue("Description");

int rowNum = 1;
for (ClinicalEntity item : items) {
Row row = sheet.createRow(rowNum);
row.createCell(0).setCellValue(rowNum);
row.createCell(1).setCellValue(item.getName());
row.createCell(2).setCellValue(item.getCode());
row.createCell(3).setCellValue(item.getDescreption());
rowNum++;
}

HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=\"discharge_conditions.xlsx\"");

workbook.write(response.getOutputStream());
context.responseComplete();
} catch (Exception e) {
JsfUtil.addErrorMessage("Error generating Excel file: " + e.getMessage());
}
}

private void recreateModel() {
items = null;
}

private ClinicalEntityFacade getFacade() {
return ejbFacade;
}

public ClinicalEntity getCurrent() {
if (current == null) {
current = new ClinicalEntity();
}
return current;
}

public void setCurrent(ClinicalEntity current) {
this.current = current;
}

public String getSelectText() {
return selectText;
}

public void setSelectText(String selectText) {
this.selectText = selectText;
}

public SessionController getSessionController() {
return sessionController;
}

public void setSessionController(SessionController sessionController) {
this.sessionController = sessionController;
}

public ClinicalEntityFacade getEjbFacade() {
return ejbFacade;
}

public void setEjbFacade(ClinicalEntityFacade ejbFacade) {
this.ejbFacade = ejbFacade;
}

@FacesConverter("dischargeConditionConverter")
public static class DischargeConditionConverter implements Converter {

@Override
public Object getAsObject(FacesContext facesContext, UIComponent component, String value) {
if (value == null || value.length() == 0) {
return null;
}
try {
DischargeConditionController controller = (DischargeConditionController) facesContext.getApplication()
.getELResolver().getValue(facesContext.getELContext(), null, "dischargeConditionController");
return controller.getEjbFacade().find(Long.valueOf(value));
} catch (NumberFormatException e) {
return null;
}
}

@Override
public String getAsString(FacesContext facesContext, UIComponent component, Object object) {
if (object == null) {
return null;
}
if (object instanceof ClinicalEntity) {
ClinicalEntity o = (ClinicalEntity) object;
return String.valueOf(o.getId());
} else {
throw new IllegalArgumentException("object " + object + " is of type "
+ object.getClass().getName() + "; expected type: ClinicalEntity");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4886,13 +4886,20 @@ public void setMissingFields(Set<String> missingFields) {
public void dischargeOldDuplicateEncounters() {
try {
String t = patientEncounterFacade.getTableName();
String pr = patientRoomFacade.getTableName();
// Group by the actual room (roomFacilityCharge_id in patientroom), not by the
// PatientRoom assignment ID. Multiple PatientRoom records can point to the same
// physical room, so grouping by currentPatientRoom_id only catches duplicates
// within the same assignment, not across different admissions to the same room.
String sql = "UPDATE " + t + " pe "
+ "JOIN " + pr + " prm ON pe.currentPatientRoom_id = prm.id "
+ "JOIN ( "
+ " SELECT MAX(id) AS keep_id, currentPatientRoom_id "
+ " FROM " + t + " "
+ " WHERE discharged = 0 AND paymentFinalized = 0 AND currentPatientRoom_id IS NOT NULL "
+ " GROUP BY currentPatientRoom_id "
+ ") latest ON pe.currentPatientRoom_id = latest.currentPatientRoom_id "
+ " SELECT MAX(pe2.id) AS keep_id, prm2.roomFacilityCharge_id "
+ " FROM " + t + " pe2 "
+ " JOIN " + pr + " prm2 ON pe2.currentPatientRoom_id = prm2.id "
+ " WHERE pe2.discharged = 0 AND pe2.paymentFinalized = 0 AND pe2.currentPatientRoom_id IS NOT NULL "
+ " GROUP BY prm2.roomFacilityCharge_id "
+ ") latest ON prm.roomFacilityCharge_id = latest.roomFacilityCharge_id "
+ "SET pe.discharged = 1 "
+ "WHERE pe.discharged = 0 AND pe.paymentFinalized = 0 "
+ "AND pe.id != latest.keep_id "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ private TreeNode<PrivilegeHolder> createPrivilegeHolderTreeNodes() {

TreeNode inwardClinicalNode = new DefaultTreeNode(new PrivilegeHolder(null, "Clinical"), inwardNode);
new DefaultTreeNode(new PrivilegeHolder(Privileges.InpatientClinicalAssessment, "Clinical Notes / Assessments"), inwardClinicalNode);
new DefaultTreeNode(new PrivilegeHolder(Privileges.InpatientClinicalDischarge, "Clinical Discharge"), inwardClinicalNode);

TreeNode additionalPrivilegesNode = new DefaultTreeNode(new PrivilegeHolder(null, "Additional Privileges"), inwardNode);
new DefaultTreeNode(new PrivilegeHolder(Privileges.InwardAdditionalPrivilages, "Additional Privilege Menu"), additionalPrivilegesNode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import com.divudi.core.facade.PaymentFacade;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
Expand Down Expand Up @@ -67,8 +66,8 @@ public class BhtPaymentSummaryReportController implements Serializable {
// -------------------------------------------------------------------------
private List<BhtPaymentSummaryDTO> reportRows;

/** All PaymentMethod values — drives the dynamic columns in XHTML. */
private final List<PaymentMethod> allPaymentMethods = Arrays.asList(PaymentMethod.values());
/** Payment methods that have at least one non-zero deposit in the current result set. */
private List<PaymentMethod> allPaymentMethods = new ArrayList<>();

/** Column totals, keyed by PaymentMethod ordinal. */
private Map<PaymentMethod, Double> columnTotals = new HashMap<>();
Expand All @@ -87,20 +86,29 @@ public void generateReport() {

List<PatientEncounter> encounters = fetchEncounters();
if (encounters == null || encounters.isEmpty()) {
allPaymentMethods = new ArrayList<>();
return;
}

for (PatientEncounter enc : encounters) {
BhtPaymentSummaryDTO row = buildRow(enc);
reportRows.add(row);

// accumulate column totals
for (PaymentMethod pm : allPaymentMethods) {
// accumulate column totals across all known methods
for (PaymentMethod pm : PaymentMethod.values()) {
columnTotals.merge(pm, row.getDepositForMethod(pm), Double::sum);
}
grandTotalDeposits += row.getTotalDeposits();
grandTotalCreditSettlement += row.getCreditSettlementTotal();
}

// only show columns where at least one BHT had a non-zero deposit
allPaymentMethods = new ArrayList<>();
for (PaymentMethod pm : PaymentMethod.values()) {
if (columnTotals.getOrDefault(pm, 0.0) != 0.0) {
allPaymentMethods.add(pm);
}
}
}

// -------------------------------------------------------------------------
Expand Down Expand Up @@ -269,6 +277,7 @@ public void makeNull() {
columnTotals = new HashMap<>();
grandTotalDeposits = 0;
grandTotalCreditSettlement = 0;
allPaymentMethods = new ArrayList<>();
}

// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1620,6 +1620,10 @@ public void discharge() {
return;
}

if (!getPatientEncounter().isClinicallyDischarged()) {
JsfUtil.addErrorMessage("Warning: Clinical discharge has not been confirmed for this patient.");
}

getPatientEncounter().setDateOfDischarge(date);
getDischargeController().setCurrent((Admission) getPatientEncounter());
getDischargeController().discharge();
Expand Down
Loading