diff --git a/CLAUDE.md b/CLAUDE.md index a5445a5a5ab..b7e722a36e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/developer_docs/security/privilege-system.md b/developer_docs/security/privilege-system.md index 2d772ba7174..29cec504c00 100644 --- a/developer_docs/security/privilege-system.md +++ b/developer_docs/security/privilege-system.md @@ -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. diff --git a/src/main/java/com/divudi/bean/clinical/DischargeConditionController.java b/src/main/java/com/divudi/bean/clinical/DischargeConditionController.java new file mode 100644 index 00000000000..b57fad799ab --- /dev/null +++ b/src/main/java/com/divudi/bean/clinical/DischargeConditionController.java @@ -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 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 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 completeDischargeConditions(String qry) { + if (qry == null || qry.trim().isEmpty()) { + return new ArrayList<>(); + } + List 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"); + } + } + } +} diff --git a/src/main/java/com/divudi/bean/common/DataAdministrationController.java b/src/main/java/com/divudi/bean/common/DataAdministrationController.java index 68a082211d3..e9982cea4b3 100644 --- a/src/main/java/com/divudi/bean/common/DataAdministrationController.java +++ b/src/main/java/com/divudi/bean/common/DataAdministrationController.java @@ -4886,13 +4886,20 @@ public void setMissingFields(Set 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 " diff --git a/src/main/java/com/divudi/bean/common/UserPrivilageController.java b/src/main/java/com/divudi/bean/common/UserPrivilageController.java index c070eefb096..1ac8589bf78 100644 --- a/src/main/java/com/divudi/bean/common/UserPrivilageController.java +++ b/src/main/java/com/divudi/bean/common/UserPrivilageController.java @@ -215,6 +215,7 @@ private TreeNode 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); diff --git a/src/main/java/com/divudi/bean/inward/BhtPaymentSummaryReportController.java b/src/main/java/com/divudi/bean/inward/BhtPaymentSummaryReportController.java index 302cfe7af3b..31e2b55cc0a 100644 --- a/src/main/java/com/divudi/bean/inward/BhtPaymentSummaryReportController.java +++ b/src/main/java/com/divudi/bean/inward/BhtPaymentSummaryReportController.java @@ -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; @@ -67,8 +66,8 @@ public class BhtPaymentSummaryReportController implements Serializable { // ------------------------------------------------------------------------- private List reportRows; - /** All PaymentMethod values — drives the dynamic columns in XHTML. */ - private final List allPaymentMethods = Arrays.asList(PaymentMethod.values()); + /** Payment methods that have at least one non-zero deposit in the current result set. */ + private List allPaymentMethods = new ArrayList<>(); /** Column totals, keyed by PaymentMethod ordinal. */ private Map columnTotals = new HashMap<>(); @@ -87,6 +86,7 @@ public void generateReport() { List encounters = fetchEncounters(); if (encounters == null || encounters.isEmpty()) { + allPaymentMethods = new ArrayList<>(); return; } @@ -94,13 +94,21 @@ public void generateReport() { 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); + } + } } // ------------------------------------------------------------------------- @@ -269,6 +277,7 @@ public void makeNull() { columnTotals = new HashMap<>(); grandTotalDeposits = 0; grandTotalCreditSettlement = 0; + allPaymentMethods = new ArrayList<>(); } // ------------------------------------------------------------------------- diff --git a/src/main/java/com/divudi/bean/inward/BhtSummeryController.java b/src/main/java/com/divudi/bean/inward/BhtSummeryController.java index 7d173a7cf24..2a92e99cf83 100644 --- a/src/main/java/com/divudi/bean/inward/BhtSummeryController.java +++ b/src/main/java/com/divudi/bean/inward/BhtSummeryController.java @@ -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(); diff --git a/src/main/java/com/divudi/bean/inward/InpatientClinicalDataController.java b/src/main/java/com/divudi/bean/inward/InpatientClinicalDataController.java index 03b38edc751..5fe19f2af9a 100644 --- a/src/main/java/com/divudi/bean/inward/InpatientClinicalDataController.java +++ b/src/main/java/com/divudi/bean/inward/InpatientClinicalDataController.java @@ -163,6 +163,9 @@ public class InpatientClinicalDataController implements Serializable { private List clinicalAssessments; private boolean viewOnly; + // Clinical discharge + private PatientEncounter clinicalDischargeRecord; + @Inject private FavouriteController favouriteController; @@ -2228,6 +2231,90 @@ public String navigateToDischargeMedicinesFromAdmission(PatientEncounter admissi return "/inward/inward_assessment_discharge_medicines?faces-redirect=true"; } + public String navigateToClinicalDischargeFromAdmission(PatientEncounter admission) { + this.parentAdmission = admission; + clinicalDischargeRecord = findOrCreateClinicalDischargeRecord(admission); + this.current = clinicalDischargeRecord; + fillCurrentPatientLists(admission.getPatient()); + fillCurrentEncounterLists(clinicalDischargeRecord); + return "/inward/inward_clinical_discharge?faces-redirect=true"; + } + + private PatientEncounter findOrCreateClinicalDischargeRecord(PatientEncounter admission) { + Map m = new HashMap<>(); + m.put("parent", admission); + m.put("type", PatientEncounterType.ClinicalDischarge); + m.put("ret", false); + String sql = "select e from PatientEncounter e " + + "where e.parentEncounter=:parent " + + "and e.patientEncounterType=:type " + + "and e.retired=:ret " + + "order by e.id desc"; + List existing = ejbFacade.findByJpql(sql, m, 1); + if (existing != null && !existing.isEmpty()) { + return existing.get(0); + } + PatientEncounter record = new PatientEncounter(); + record.setParentEncounter(admission); + record.setPatient(admission.getPatient()); + record.setPatientEncounterType(PatientEncounterType.ClinicalDischarge); + record.setEncounterDateTime(new Date()); + record.setInstitution(sessionController.getInstitution()); + record.setDepartment(sessionController.getDepartment()); + return record; + } + + public void saveClinicalDischarge() { + if (clinicalDischargeRecord == null) { + JsfUtil.addErrorMessage("No clinical discharge record found."); + return; + } + clinicalDischargeRecord.setDepartment(sessionController.getDepartment()); + if (clinicalDischargeRecord.getId() != null) { + getFacade().edit(clinicalDischargeRecord); + JsfUtil.addSuccessMessage("Clinical discharge details saved."); + } else { + clinicalDischargeRecord.setCreatedAt(new Date()); + clinicalDischargeRecord.setCreater(sessionController.getLoggedUser()); + getFacade().create(clinicalDischargeRecord); + JsfUtil.addSuccessMessage("Clinical discharge record created."); + } + fillCurrentEncounterLists(clinicalDischargeRecord); + } + + public void confirmClinicalDischarge() { + if (clinicalDischargeRecord == null) { + JsfUtil.addErrorMessage("No clinical discharge record found."); + return; + } + saveClinicalDischarge(); + parentAdmission.setClinicallyDischarged(Boolean.TRUE); + parentAdmission.setClinicalDischargeDateTime(new Date()); + parentAdmission.setClinicalDischargedBy(sessionController.getLoggedUser()); + getFacade().edit(parentAdmission); + JsfUtil.addSuccessMessage("Clinical discharge confirmed."); + } + + public void cancelClinicalDischarge() { + if (parentAdmission == null) { + JsfUtil.addErrorMessage("No admission found."); + return; + } + parentAdmission.setClinicallyDischarged(Boolean.FALSE); + parentAdmission.setClinicalDischargeDateTime(null); + parentAdmission.setClinicalDischargedBy(null); + getFacade().edit(parentAdmission); + JsfUtil.addSuccessMessage("Clinical discharge cancelled."); + } + + public PatientEncounter getClinicalDischargeRecord() { + return clinicalDischargeRecord; + } + + public void setClinicalDischargeRecord(PatientEncounter clinicalDischargeRecord) { + this.clinicalDischargeRecord = clinicalDischargeRecord; + } + public PatientEncounter getParentAdmission() { return parentAdmission; } diff --git a/src/main/java/com/divudi/bean/inward/InwardChargeTypeBreakdownController.java b/src/main/java/com/divudi/bean/inward/InwardChargeTypeBreakdownController.java new file mode 100644 index 00000000000..77271888ca9 --- /dev/null +++ b/src/main/java/com/divudi/bean/inward/InwardChargeTypeBreakdownController.java @@ -0,0 +1,741 @@ +package com.divudi.bean.inward; + +import com.divudi.bean.common.ConfigOptionApplicationController; +import com.divudi.core.data.BillType; +import com.divudi.core.data.BillTypeAtomic; +import com.divudi.core.data.inward.AdmissionStatus; +import com.divudi.core.data.inward.CalculationMethod; +import com.divudi.core.data.inward.InwardChargeType; +import com.divudi.core.entity.Department; +import com.divudi.core.entity.Institution; +import com.divudi.core.entity.PatientEncounter; +import com.divudi.core.entity.BillFee; +import com.divudi.core.entity.BillItem; +import com.divudi.core.entity.inward.AdmissionType; +import com.divudi.core.entity.inward.PatientRoom; +import com.divudi.core.facade.BillFeeFacade; +import com.divudi.core.facade.BillItemFacade; +import com.divudi.core.facade.PatientEncounterFacade; +import com.divudi.core.facade.PatientRoomFacade; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.ejb.EJB; +import javax.enterprise.context.SessionScoped; +import javax.inject.Inject; +import javax.inject.Named; +import javax.persistence.TemporalType; + +/** + * Controller for the Inward Charge Type Breakdown by BHT report (Issue #19655). + * + * Produces a pivot table: one row per BHT, one column per distinct item/staff + * found in the period for the selected InwardChargeType. The data source and + * column structure depend on the charge type's CalculationMethod. + */ +@Named +@SessionScoped +public class InwardChargeTypeBreakdownController implements Serializable { + + // ------------------------------------------------------------------------- + // EJBs / CDI + // ------------------------------------------------------------------------- + @EJB + private BillItemFacade billItemFacade; + @EJB + private PatientRoomFacade patientRoomFacade; + @EJB + private BillFeeFacade billFeeFacade; + @EJB + private PatientEncounterFacade patientEncounterFacade; + @Inject + private ConfigOptionApplicationController configOptionApplicationController; + + // ------------------------------------------------------------------------- + // Filter fields + // ------------------------------------------------------------------------- + private Date fromDate = startOfCurrentMonth(); + private Date toDate = new Date(); + + /** "admissionDate" or "dischargeDate" */ + private String dateBasis = "dischargeDate"; + + private AdmissionStatus admissionStatus = AdmissionStatus.DISCHARGED_AND_FINAL_BILL_COMPLETED; + private AdmissionType admissionType; + private Institution institution; + private Institution site; + private Department department; + + /** Mandatory — report cannot be generated without this */ + private InwardChargeType selectedChargeType; + + // ------------------------------------------------------------------------- + // Report output + // ------------------------------------------------------------------------- + private List columnKeys; + private List columnLabels; + private List rows; + private List columnTotals; // List, NOT double[] — EL cannot iterate primitive arrays + private double grandTotal; + private String errorMessage; + + // ------------------------------------------------------------------------- + // Inner DTO + // ------------------------------------------------------------------------- + + public static class BhtBreakdownRow implements Serializable { + + private String bhtNo; + private String patientName; + private Date dateOfAdmission; + private Date dateOfDischarge; + /** columnKey → net amount for that column on this BHT */ + private Map cellValues = new LinkedHashMap<>(); + private double rowTotal; + + public String getBhtNo() { return bhtNo; } + public void setBhtNo(String bhtNo) { this.bhtNo = bhtNo; } + + public String getPatientName() { return patientName; } + public void setPatientName(String patientName) { this.patientName = patientName; } + + public Date getDateOfAdmission() { return dateOfAdmission; } + public void setDateOfAdmission(Date dateOfAdmission) { this.dateOfAdmission = dateOfAdmission; } + + public Date getDateOfDischarge() { return dateOfDischarge; } + public void setDateOfDischarge(Date dateOfDischarge) { this.dateOfDischarge = dateOfDischarge; } + + public Map getCellValues() { return cellValues; } + public void setCellValues(Map cellValues) { this.cellValues = cellValues; } + + public double getRowTotal() { return rowTotal; } + public void setRowTotal(double rowTotal) { this.rowTotal = rowTotal; } + } + + // ------------------------------------------------------------------------- + // Main generate method + // ------------------------------------------------------------------------- + + public void generateReport() { + errorMessage = null; + columnKeys = null; + columnLabels = null; + rows = null; + columnTotals = null; + grandTotal = 0.0; + + if (selectedChargeType == null) { + errorMessage = "Please select an Inward Charge Type before generating the report."; + return; + } + + if ("dischargeDate".equals(dateBasis) + && admissionStatus == AdmissionStatus.ADMITTED_BUT_NOT_DISCHARGED) { + errorMessage = "Cannot filter by discharge date for patients not yet discharged."; + return; + } + + if (fromDate != null && toDate != null && fromDate.after(toDate)) { + errorMessage = "From date must not be after To date."; + return; + } + + CalculationMethod method = selectedChargeType.getCalculationMethod(); + switch (method) { + case BILL_ITEM: + buildFromBillItems(); + break; + case PHARMACY_BILL: + buildFromPharmacyBillItems(); + break; + case STORE_BILL: + buildFromStoreBillItems(); + break; + case PATIENT_ROOM: + buildFromPatientRoom(); + break; + case BILL_FEE: + buildFromBillFees(); + break; + case ADMISSION_FEE: + buildFromAdmissionFee(); + break; + default: + buildFromBillItems(); + } + } + + // ------------------------------------------------------------------------- + // Encounter filter helper (same logic as InwardChargeTypeDetailController) + // ------------------------------------------------------------------------- + + private void appendEncounterFilters(StringBuilder jpql, Map params, + String encAlias) { + jpql.append(" and ").append(encAlias).append(".retired = false"); + + if (fromDate != null && toDate != null) { + if ("admissionDate".equals(dateBasis)) { + jpql.append(" and ").append(encAlias).append(".dateOfAdmission between :fromDate and :toDate"); + } else { + jpql.append(" and ").append(encAlias).append(".dateOfDischarge between :fromDate and :toDate"); + } + params.put("fromDate", fromDate); + params.put("toDate", toDate); + } + + if (admissionStatus != null && admissionStatus != AdmissionStatus.ANY_STATUS) { + switch (admissionStatus) { + case ADMITTED_BUT_NOT_DISCHARGED: + jpql.append(" and ").append(encAlias).append(".discharged = :dis"); + params.put("dis", false); + break; + case DISCHARGED_AND_FINAL_BILL_COMPLETED: + jpql.append(" and ").append(encAlias).append(".discharged = :dis"); + jpql.append(" and ").append(encAlias).append(".paymentFinalized = :pf"); + params.put("dis", true); + params.put("pf", true); + break; + case DISCHARGED_BUT_FINAL_BILL_NOT_COMPLETED: + jpql.append(" and ").append(encAlias).append(".discharged = :dis"); + jpql.append(" and ").append(encAlias).append(".paymentFinalized = :pf"); + params.put("dis", true); + params.put("pf", false); + break; + default: + break; + } + } + + if (admissionType != null) { + jpql.append(" and ").append(encAlias).append(".admissionType = :admType"); + params.put("admType", admissionType); + } + + if (institution != null) { + jpql.append(" and ").append(encAlias).append(".institution = :ins"); + params.put("ins", institution); + } + + if (site != null) { + jpql.append(" and ").append(encAlias).append(".department.site = :site"); + params.put("site", site); + } + + if (department != null) { + jpql.append(" and ").append(encAlias).append(".department = :dept"); + params.put("dept", department); + } + } + + // ------------------------------------------------------------------------- + // Shared pivot builder for BillItem-based cases + // ------------------------------------------------------------------------- + + private void buildPivotFromBillItems(List rawList) { + // 1. Collect distinct column keys (item.id → item.name); skip null items + Map keyToLabel = new LinkedHashMap<>(); + for (BillItem bi : rawList) { + if (bi.getItem() == null) { + continue; + } + String key = String.valueOf(bi.getItem().getId()); + keyToLabel.put(key, bi.getItem().getName()); + } + + // 2. Sort entries alphabetically by label (item name) + List> sortedEntries = new ArrayList<>(keyToLabel.entrySet()); + sortedEntries.sort(Comparator.comparing(e -> e.getValue() == null ? "" : e.getValue())); + + columnKeys = new ArrayList<>(); + columnLabels = new ArrayList<>(); + for (Map.Entry entry : sortedEntries) { + columnKeys.add(entry.getKey()); + columnLabels.add(entry.getValue()); + } + + // 3. Group by BHT + Map bhtMap = new LinkedHashMap<>(); + for (BillItem bi : rawList) { + if (bi.getItem() == null || bi.getBill() == null) { + continue; + } + PatientEncounter enc = bi.getBill().getPatientEncounter(); + if (enc == null || enc.getBhtNo() == null) { + continue; + } + String bhtNo = enc.getBhtNo(); + BhtBreakdownRow row = bhtMap.computeIfAbsent(bhtNo, k -> createRow(enc)); + String key = String.valueOf(bi.getItem().getId()); + double net = bi.getNetValue(); + row.getCellValues().merge(key, net, Double::sum); + } + + // 4. Sort rows by bhtNo and calculate row totals + rows = new ArrayList<>(bhtMap.values()); + rows.sort(Comparator.comparing(BhtBreakdownRow::getBhtNo, + Comparator.nullsLast(Comparator.naturalOrder()))); + for (BhtBreakdownRow row : rows) { + double total = row.getCellValues().values().stream() + .mapToDouble(Double::doubleValue).sum(); + row.setRowTotal(total); + } + + // 5. Calculate column totals and grand total + columnTotals = new ArrayList<>(); + grandTotal = 0.0; + for (String key : columnKeys) { + double colSum = 0; + for (BhtBreakdownRow row : rows) { + Double val = row.getCellValues().get(key); + if (val != null) { + colSum += val; + } + } + columnTotals.add(colSum); + grandTotal += colSum; + } + } + + // ------------------------------------------------------------------------- + // BILL_ITEM + // ------------------------------------------------------------------------- + + private void buildFromBillItems() { + StringBuilder jpql = new StringBuilder( + "select bi from BillItem bi join bi.bill b join b.patientEncounter enc" + + " where bi.retired = false" + + " and b.retired = false" + + " and b.cancelled = false" + + " and b.billType in :btps" + + " and bi.inwardChargeType = :ct"); + + Map params = new HashMap<>(); + List btps = new ArrayList<>(); + btps.add(BillType.InwardBill); + btps.add(BillType.InwardOutSideBill); + params.put("btps", btps); + params.put("ct", selectedChargeType); + appendEncounterFilters(jpql, params, "enc"); + + List raw = billItemFacade.findByJpql(jpql.toString(), params, TemporalType.TIMESTAMP); + if (raw == null) { + raw = new ArrayList<>(); + } + buildPivotFromBillItems(raw); + } + + // ------------------------------------------------------------------------- + // PHARMACY_BILL + // ------------------------------------------------------------------------- + + private void buildFromPharmacyBillItems() { + List btas = new ArrayList<>(); + btas.add(BillTypeAtomic.PHARMACY_DIRECT_ISSUE); + btas.add(BillTypeAtomic.PHARMACY_DIRECT_ISSUE_CANCELLED); + btas.add(BillTypeAtomic.DIRECT_ISSUE_INWARD_MEDICINE); + btas.add(BillTypeAtomic.DIRECT_ISSUE_INWARD_MEDICINE_RETURN); + btas.add(BillTypeAtomic.DIRECT_ISSUE_INWARD_MEDICINE_CANCELLATION); + btas.add(BillTypeAtomic.ISSUE_MEDICINE_ON_REQUEST_INWARD); + btas.add(BillTypeAtomic.ISSUE_MEDICINE_ON_REQUEST_INWARD_RETURN); + btas.add(BillTypeAtomic.ISSUE_MEDICINE_ON_REQUEST_INWARD_CANCELLATION); + + StringBuilder jpql = new StringBuilder( + "select bi from BillItem bi join bi.bill b join b.patientEncounter enc" + + " where bi.retired = false" + + " and b.retired = false" + + " and b.cancelled = false" + + " and b.billTypeAtomic in :btas" + + " and bi.inwardChargeType = :ct"); + + Map params = new HashMap<>(); + params.put("btas", btas); + params.put("ct", selectedChargeType); + appendEncounterFilters(jpql, params, "enc"); + + List raw = billItemFacade.findByJpql(jpql.toString(), params, TemporalType.TIMESTAMP); + if (raw == null) { + raw = new ArrayList<>(); + } + buildPivotFromBillItems(raw); + } + + // ------------------------------------------------------------------------- + // STORE_BILL + // ------------------------------------------------------------------------- + + private void buildFromStoreBillItems() { + StringBuilder jpql = new StringBuilder( + "select bi from BillItem bi join bi.bill b join b.patientEncounter enc" + + " where bi.retired = false" + + " and b.retired = false" + + " and b.cancelled = false" + + " and b.billType = :btp" + + " and bi.inwardChargeType = :ct"); + + Map params = new HashMap<>(); + params.put("btp", BillType.StoreBhtPre); + params.put("ct", selectedChargeType); + appendEncounterFilters(jpql, params, "enc"); + + List raw = billItemFacade.findByJpql(jpql.toString(), params, TemporalType.TIMESTAMP); + if (raw == null) { + raw = new ArrayList<>(); + } + buildPivotFromBillItems(raw); + } + + // ------------------------------------------------------------------------- + // PATIENT_ROOM — two fixed columns + // ------------------------------------------------------------------------- + + private void buildFromPatientRoom() { + columnKeys = Arrays.asList("TIME_BASED", "SERVICE_ITEMS"); + columnLabels = Arrays.asList("Time-based Charge", "Service Items"); + + // Sub-query 1: PatientRoom time-based calculated charges + StringBuilder jpql1 = new StringBuilder( + "select pr from PatientRoom pr join pr.patientEncounter enc" + + " where pr.retired = false"); + Map params1 = new HashMap<>(); + appendEncounterFilters(jpql1, params1, "enc"); + + List patientRooms = patientRoomFacade.findByJpql( + jpql1.toString(), params1, TemporalType.TIMESTAMP); + + Map bhtMap = new LinkedHashMap<>(); + if (patientRooms != null) { + for (PatientRoom pr : patientRooms) { + PatientEncounter enc = pr.getPatientEncounter(); + if (enc == null || enc.getBhtNo() == null) { + continue; + } + String bhtNo = enc.getBhtNo(); + BhtBreakdownRow row = bhtMap.computeIfAbsent(bhtNo, k -> createRow(enc)); + double charge = extractPatientRoomCharge(pr); + if (charge != 0.0) { + row.getCellValues().merge("TIME_BASED", charge, Double::sum); + } + } + } + + // Sub-query 2: Service BillItems under the same charge type (InwardBill only), summed per BHT + StringBuilder jpql2 = new StringBuilder( + "select bi from BillItem bi join bi.bill b join b.patientEncounter enc" + + " where bi.retired = false" + + " and b.retired = false" + + " and b.cancelled = false" + + " and b.billType = :btp" + + " and bi.inwardChargeType = :ct"); + Map params2 = new HashMap<>(); + params2.put("btp", BillType.InwardBill); + params2.put("ct", selectedChargeType); + appendEncounterFilters(jpql2, params2, "enc"); + + List serviceItems = billItemFacade.findByJpql( + jpql2.toString(), params2, TemporalType.TIMESTAMP); + if (serviceItems != null) { + for (BillItem bi : serviceItems) { + if (bi.getBill() == null) { + continue; + } + PatientEncounter enc = bi.getBill().getPatientEncounter(); + if (enc == null || enc.getBhtNo() == null) { + continue; + } + String bhtNo = enc.getBhtNo(); + BhtBreakdownRow row = bhtMap.computeIfAbsent(bhtNo, k -> createRow(enc)); + row.getCellValues().merge("SERVICE_ITEMS", bi.getNetValue(), Double::sum); + } + } + + // Sort and finalise + rows = new ArrayList<>(bhtMap.values()); + rows.sort(Comparator.comparing(BhtBreakdownRow::getBhtNo, + Comparator.nullsLast(Comparator.naturalOrder()))); + for (BhtBreakdownRow row : rows) { + double total = row.getCellValues().values().stream() + .mapToDouble(Double::doubleValue).sum(); + row.setRowTotal(total); + } + + columnTotals = new ArrayList<>(); + grandTotal = 0.0; + for (String key : columnKeys) { + double colSum = 0; + for (BhtBreakdownRow row : rows) { + Double val = row.getCellValues().get(key); + if (val != null) { + colSum += val; + } + } + columnTotals.add(colSum); + grandTotal += colSum; + } + } + + private double extractPatientRoomCharge(PatientRoom pr) { + switch (selectedChargeType) { + case RoomCharges: return pr.getCalculatedRoomCharge(); + case MOCharges: return pr.getCalculatedMoCharge(); + case NursingCharges: return pr.getCalculatedNursingCharge(); + case LinenCharges: return pr.getCalculatedLinenCharge(); + case AdministrationCharge: return pr.getCalculatedAdministrationCharge(); + case MedicalCareICU: return pr.getCalculatedMedicalCareCharge(); + case MaintainCharges: return pr.getCalculatedMaintainCharge(); + default: return 0.0; + } + } + + // ------------------------------------------------------------------------- + // BILL_FEE — pivot on staff name + // ------------------------------------------------------------------------- + + private void buildFromBillFees() { + StringBuilder jpql = new StringBuilder( + "select bf from BillFee bf join bf.bill b join b.patientEncounter enc" + + " where bf.retired = false" + + " and b.retired = false" + + " and b.cancelled = false" + + " and b.billType = :btp"); + + Map params = new HashMap<>(); + params.put("btp", BillType.InwardProfessional); + appendEncounterFilters(jpql, params, "enc"); + + List raw = billFeeFacade.findByJpql(jpql.toString(), params, TemporalType.TIMESTAMP); + if (raw == null) { + raw = new ArrayList<>(); + } + + // Collect distinct staff names as column keys (sorted alphabetically) + Map keyToLabel = new LinkedHashMap<>(); + for (BillFee bf : raw) { + String key = resolveStaffName(bf); + keyToLabel.put(key, key); + } + List sortedKeys = new ArrayList<>(keyToLabel.keySet()); + sortedKeys.sort(Comparator.nullsLast(Comparator.naturalOrder())); + columnKeys = sortedKeys; + columnLabels = new ArrayList<>(sortedKeys); + + // Group by BHT + Map bhtMap = new LinkedHashMap<>(); + for (BillFee bf : raw) { + if (bf.getBill() == null) { + continue; + } + PatientEncounter enc = bf.getBill().getPatientEncounter(); + if (enc == null || enc.getBhtNo() == null) { + continue; + } + String bhtNo = enc.getBhtNo(); + BhtBreakdownRow row = bhtMap.computeIfAbsent(bhtNo, k -> createRow(enc)); + String key = resolveStaffName(bf); + row.getCellValues().merge(key, bf.getFeeValue(), Double::sum); + } + + rows = new ArrayList<>(bhtMap.values()); + rows.sort(Comparator.comparing(BhtBreakdownRow::getBhtNo, + Comparator.nullsLast(Comparator.naturalOrder()))); + for (BhtBreakdownRow row : rows) { + double total = row.getCellValues().values().stream() + .mapToDouble(Double::doubleValue).sum(); + row.setRowTotal(total); + } + + columnTotals = new ArrayList<>(); + grandTotal = 0.0; + for (String key : columnKeys) { + double colSum = 0; + for (BhtBreakdownRow row : rows) { + Double val = row.getCellValues().get(key); + if (val != null) { + colSum += val; + } + } + columnTotals.add(colSum); + grandTotal += colSum; + } + } + + private String resolveStaffName(BillFee bf) { + if (bf.getStaff() != null && bf.getStaff().getPerson() != null + && bf.getStaff().getPerson().getNameWithTitle() != null) { + return bf.getStaff().getPerson().getNameWithTitle(); + } + return "Unknown"; + } + + // ------------------------------------------------------------------------- + // ADMISSION_FEE — single fixed column + // ------------------------------------------------------------------------- + + private void buildFromAdmissionFee() { + columnKeys = Arrays.asList("ADMISSION_FEE"); + columnLabels = Arrays.asList("Admission Fee"); + + StringBuilder jpql = new StringBuilder( + "select enc from PatientEncounter enc where enc.retired = false"); + Map params = new HashMap<>(); + appendEncounterFilters(jpql, params, "enc"); + + List encounters = patientEncounterFacade.findByJpql( + jpql.toString(), params, TemporalType.TIMESTAMP); + + Map bhtMap = new LinkedHashMap<>(); + if (encounters != null) { + for (PatientEncounter enc : encounters) { + if (enc.getBhtNo() == null) { + continue; + } + double fee = (enc.getAdmissionType() != null) + ? enc.getAdmissionType().getAdmissionFee() : 0.0; + if (fee == 0.0) { + continue; + } + BhtBreakdownRow row = bhtMap.computeIfAbsent(enc.getBhtNo(), k -> createRow(enc)); + row.getCellValues().put("ADMISSION_FEE", fee); + } + } + + rows = new ArrayList<>(bhtMap.values()); + rows.sort(Comparator.comparing(BhtBreakdownRow::getBhtNo, + Comparator.nullsLast(Comparator.naturalOrder()))); + for (BhtBreakdownRow row : rows) { + Double val = row.getCellValues().get("ADMISSION_FEE"); + row.setRowTotal(val != null ? val : 0.0); + } + + double colSum = 0; + for (BhtBreakdownRow row : rows) { + Double val = row.getCellValues().get("ADMISSION_FEE"); + if (val != null) { + colSum += val; + } + } + grandTotal = colSum; + columnTotals = Arrays.asList(colSum); + } + + // ------------------------------------------------------------------------- + // Helper: create a new BhtBreakdownRow from a PatientEncounter + // ------------------------------------------------------------------------- + + private BhtBreakdownRow createRow(PatientEncounter enc) { + BhtBreakdownRow row = new BhtBreakdownRow(); + row.setBhtNo(enc.getBhtNo()); + if (enc.getPatient() != null && enc.getPatient().getPerson() != null) { + row.setPatientName(enc.getPatient().getPerson().getNameWithTitle()); + } + row.setDateOfAdmission(enc.getDateOfAdmission()); + row.setDateOfDischarge(enc.getDateOfDischarge()); + return row; + } + + // ------------------------------------------------------------------------- + // Charge type helpers (delegates to ConfigOptionApplicationController) + // ------------------------------------------------------------------------- + + public List getAllChargeTypes() { + return Arrays.asList(InwardChargeType.values()); + } + + public List completeInwardChargeType(String query) { + String lower = query == null ? "" : query.toLowerCase(); + List results = new ArrayList<>(); + for (InwardChargeType ct : InwardChargeType.values()) { + String label = configOptionApplicationController.getInwardChargeTypeLabel(ct); + if (label == null) { + label = ""; + } + if (label.toLowerCase().contains(lower) || ct.name().toLowerCase().contains(lower)) { + results.add(ct); + } + } + return results; + } + + public String getChargeTypeLabel(InwardChargeType type) { + if (type == null) { + return ""; + } + return configOptionApplicationController.getInwardChargeTypeLabel(type); + } + + // ------------------------------------------------------------------------- + // Lifecycle + // ------------------------------------------------------------------------- + + public void makeNull() { + fromDate = startOfCurrentMonth(); + toDate = new Date(); + dateBasis = "dischargeDate"; + admissionStatus = AdmissionStatus.DISCHARGED_AND_FINAL_BILL_COMPLETED; + admissionType = null; + institution = null; + site = null; + department = null; + selectedChargeType = null; + columnKeys = null; + columnLabels = null; + rows = null; + columnTotals = null; + grandTotal = 0.0; + errorMessage = null; + } + + private static Date startOfCurrentMonth() { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.DAY_OF_MONTH, 1); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal.getTime(); + } + + // ------------------------------------------------------------------------- + // Getters / setters + // ------------------------------------------------------------------------- + + public Date getFromDate() { return fromDate; } + public void setFromDate(Date fromDate) { this.fromDate = fromDate; } + + public Date getToDate() { return toDate; } + public void setToDate(Date toDate) { this.toDate = toDate; } + + public String getDateBasis() { return dateBasis; } + public void setDateBasis(String dateBasis) { this.dateBasis = dateBasis; } + + public AdmissionStatus getAdmissionStatus() { return admissionStatus; } + public void setAdmissionStatus(AdmissionStatus admissionStatus) { this.admissionStatus = admissionStatus; } + + public AdmissionType getAdmissionType() { return admissionType; } + public void setAdmissionType(AdmissionType admissionType) { this.admissionType = admissionType; } + + public Institution getInstitution() { return institution; } + public void setInstitution(Institution institution) { this.institution = institution; } + + public Institution getSite() { return site; } + public void setSite(Institution site) { this.site = site; } + + public Department getDepartment() { return department; } + public void setDepartment(Department department) { this.department = department; } + + public InwardChargeType getSelectedChargeType() { return selectedChargeType; } + public void setSelectedChargeType(InwardChargeType selectedChargeType) { this.selectedChargeType = selectedChargeType; } + + public List getColumnKeys() { return columnKeys; } + public List getColumnLabels() { return columnLabels; } + public List getRows() { return rows; } + public List getColumnTotals() { return columnTotals; } + public double getGrandTotal() { return grandTotal; } + public String getErrorMessage() { return errorMessage; } +} diff --git a/src/main/java/com/divudi/bean/inward/RoomChangeController.java b/src/main/java/com/divudi/bean/inward/RoomChangeController.java index c71fdb518f3..26f0984f11c 100644 --- a/src/main/java/com/divudi/bean/inward/RoomChangeController.java +++ b/src/main/java/com/divudi/bean/inward/RoomChangeController.java @@ -20,6 +20,7 @@ import com.divudi.core.entity.inward.PatientRoom; import com.divudi.core.entity.inward.RoomFacilityCharge; import com.divudi.core.facade.AdmissionFacade; +import com.divudi.core.facade.PatientEncounterFacade; import com.divudi.core.facade.PatientFacade; import com.divudi.core.facade.PatientRoomFacade; import com.divudi.core.facade.PersonFacade; @@ -69,6 +70,8 @@ public class RoomChangeController implements Serializable { @EJB private AdmissionFacade ejbFacade; @EJB + private PatientEncounterFacade patientEncounterFacade; + @EJB private PersonFacade personFacade; @EJB private PatientFacade patientFacade; @@ -303,6 +306,14 @@ public void discharge(PatientRoom pR) { pR.setDischargedBy(getSessionController().getLoggedUser()); getPatientRoomFacade().edit(pR); notificationController.createNotification(pR, "Discharge"); + + // Sync room discharge to the parent encounter when this is the last (current) room + if (pR.getNextRoom() == null && pR.getPatientEncounter() != null) { + com.divudi.core.entity.PatientEncounter encounter = pR.getPatientEncounter(); + encounter.setRoomDischargeDateTime(pR.getDischargedAt()); + encounter.setRoomDischargedBy(getSessionController().getLoggedUser()); + patientEncounterFacade.edit(encounter); + } } public void dischargeWithCurrentTime(PatientRoom pR) { @@ -330,6 +341,14 @@ public void dischargeWithCurrentTime(PatientRoom pR) { pR.setDischargedBy(getSessionController().getLoggedUser()); getPatientRoomFacade().edit(pR); notificationController.createNotification(pR, "Discharge"); + + // Sync room discharge to the parent encounter when this is the last (current) room + if (pR.getNextRoom() == null && pR.getPatientEncounter() != null) { + com.divudi.core.entity.PatientEncounter encounter = pR.getPatientEncounter(); + encounter.setRoomDischargeDateTime(pR.getDischargedAt()); + encounter.setRoomDischargedBy(getSessionController().getLoggedUser()); + patientEncounterFacade.edit(encounter); + } } public void dischargeCancel(PatientRoom pR) { diff --git a/src/main/java/com/divudi/core/data/Privileges.java b/src/main/java/com/divudi/core/data/Privileges.java index 05eef470be4..d9af956b547 100644 --- a/src/main/java/com/divudi/core/data/Privileges.java +++ b/src/main/java/com/divudi/core/data/Privileges.java @@ -119,6 +119,7 @@ public enum Privileges { InwardBillSettleWithoutCheck("Inward Bill Settle Without Check"), TheaterIssueBHT("Theater Issue BHT"), InpatientClinicalAssessment("Inpatient Clinical Assessment"), + InpatientClinicalDischarge("Inpatient Clinical Discharge"), // // @@ -1013,6 +1014,7 @@ public String getCategory() { case InwardAppointmentUpdate: case InwardAppointmentCancel: case InpatientClinicalAssessment: + case InpatientClinicalDischarge: return "Inward"; default: diff --git a/src/main/java/com/divudi/core/data/SymanticType.java b/src/main/java/com/divudi/core/data/SymanticType.java index 0afaae812b8..9099bd634d6 100644 --- a/src/main/java/com/divudi/core/data/SymanticType.java +++ b/src/main/java/com/divudi/core/data/SymanticType.java @@ -159,5 +159,6 @@ public enum SymanticType { Employment, Blood_Group, Civil_status, - Relationships + Relationships, + Discharge_Condition } diff --git a/src/main/java/com/divudi/core/data/inward/CalculationMethod.java b/src/main/java/com/divudi/core/data/inward/CalculationMethod.java new file mode 100644 index 00000000000..731e93f5acc --- /dev/null +++ b/src/main/java/com/divudi/core/data/inward/CalculationMethod.java @@ -0,0 +1,21 @@ +package com.divudi.core.data.inward; + +/** + * Identifies how each InwardChargeType value is calculated and which data + * sources contribute to it. Used by InwardChargeTypeBreakdownController to + * branch report logic per charge type. + */ +public enum CalculationMethod { + /** BillItem from InwardBill + InwardOutSideBill — default for most values */ + BILL_ITEM, + /** BillItem from pharmacy issue bills identified by BillTypeAtomic */ + PHARMACY_BILL, + /** BillItem from store issue bills (StoreBhtPre) */ + STORE_BILL, + /** PatientRoom calculated fields (time-based) plus optional service BillItems — 2 fixed columns */ + PATIENT_ROOM, + /** BillFee.feeValue from InwardProfessional bills, pivot on staff name */ + BILL_FEE, + /** Flat fee from admissionType.admissionFee — single fixed column */ + ADMISSION_FEE +} diff --git a/src/main/java/com/divudi/core/data/inward/InwardChargeType.java b/src/main/java/com/divudi/core/data/inward/InwardChargeType.java index b5c8681b7d5..e7029a1d8ad 100644 --- a/src/main/java/com/divudi/core/data/inward/InwardChargeType.java +++ b/src/main/java/com/divudi/core/data/inward/InwardChargeType.java @@ -2,37 +2,37 @@ public enum InwardChargeType { - AdmissionFee("Admission Fee"), + AdmissionFee("Admission Fee", CalculationMethod.ADMISSION_FEE), AmbulanceCharges("Ambulance Charges"), - AdministrationCharge("Administration Fee"),//GOES WITH ROOM CHARGE + AdministrationCharge("Administration Fee", CalculationMethod.PATIENT_ROOM),//GOES WITH ROOM CHARGE BloodTransfusioncharges("Blood Transfusion Charges"), CT("CT Scan"), DressingCharges("Dressing Charges"), Equipment("Equipment Charges"), ECG_EEG("ECG/EEG"),//"ECG/EEG/ECHO/EXECG" ETUCharges("ETU Charges"), - GeneralIssuing("General Issuing"), + GeneralIssuing("General Issuing", CalculationMethod.STORE_BILL), HomeVisiting("Home Visiting"), Immunization("Immunization"), IntensiveCareManagement("Intensive Care Management"), LarbourRoomCharges("Larbour Room Charges"), - LinenCharges("Linen Charges"),//GOES WITH PATIENT ROOM + LinenCharges("Linen Charges", CalculationMethod.PATIENT_ROOM),//GOES WITH PATIENT ROOM Laboratory("Laboratory Charges"), MealCharges("Meal Charges"), - MedicalCareICU("Medical Care"),//Goes With Room + MedicalCareICU("Medical Care", CalculationMethod.PATIENT_ROOM),//Goes With Room MedicalServices("Medical Services"), - Medicine("Medicine"),//For BHT ISSUE + Medicine("Medicine", CalculationMethod.PHARMACY_BILL),//For BHT ISSUE MedicinesAndSurgicalSupplies("Medicines and Surgical Supplies"),//For Surgery Bill Medicines - MOCharges("MO Charges"),//GOES WITH PATIENT ROOM - MaintainCharges("Maintain Charges"),//GOES WITH PATIENT ROOM - DoctorAndNurses("Assisting Charge"),//Set Doctor && Nurse Fees - NursingCharges("Nursing Care"),//GOES WITH PATIENT ROOM + MOCharges("MO Charges", CalculationMethod.PATIENT_ROOM),//GOES WITH PATIENT ROOM + MaintainCharges("Maintain Charges", CalculationMethod.PATIENT_ROOM),//GOES WITH PATIENT ROOM + DoctorAndNurses("Assisting Charge", CalculationMethod.BILL_FEE),//Set Doctor && Nurse Fees + NursingCharges("Nursing Care", CalculationMethod.PATIENT_ROOM),//GOES WITH PATIENT ROOM OxygenCharges("Oxygen Charges"), OtherCharges("Other Charges"), OperationTheatreCharges("Operation Theatre Charges"), - ProfessionalCharge("Professional Charge"),//Only for Consultant Fees + ProfessionalCharge("Professional Charge", CalculationMethod.BILL_FEE),//Only for Consultant Fees ReimbursementCharges("Reimbursement Charges"), - RoomCharges("Room Charges"),//GOES WITH PATIENT ROOM + RoomCharges("Room Charges", CalculationMethod.PATIENT_ROOM),//GOES WITH PATIENT ROOM physiotherapy("Physiotherapy Charges"), Scanning("Scanning Charges"), TreatmentCharges("Treatment Charges"), @@ -194,11 +194,21 @@ public enum InwardChargeType { private final String nameAsString; private String name; + private CalculationMethod calculationMethod; InwardChargeType(String nameAsString) { this.nameAsString = nameAsString; } + InwardChargeType(String nameAsString, CalculationMethod calculationMethod) { + this.nameAsString = nameAsString; + this.calculationMethod = calculationMethod; + } + + public CalculationMethod getCalculationMethod() { + return calculationMethod != null ? calculationMethod : CalculationMethod.BILL_ITEM; + } + @Override public String toString() { return nameAsString; diff --git a/src/main/java/com/divudi/core/data/inward/PatientEncounterType.java b/src/main/java/com/divudi/core/data/inward/PatientEncounterType.java index 1c422a597a5..7ce5a21a1ab 100644 --- a/src/main/java/com/divudi/core/data/inward/PatientEncounterType.java +++ b/src/main/java/com/divudi/core/data/inward/PatientEncounterType.java @@ -15,5 +15,6 @@ public enum PatientEncounterType { Procedure, ETU, ClinicalAssessment, + ClinicalDischarge, } diff --git a/src/main/java/com/divudi/core/entity/PatientEncounter.java b/src/main/java/com/divudi/core/entity/PatientEncounter.java index 76ec8470c62..153c5284558 100644 --- a/src/main/java/com/divudi/core/entity/PatientEncounter.java +++ b/src/main/java/com/divudi/core/entity/PatientEncounter.java @@ -102,6 +102,19 @@ public class PatientEncounter implements Serializable, RetirableEntity { Date timeOfDischarge; @Temporal(javax.persistence.TemporalType.TIMESTAMP) Date dateOfDischarge; + + // Clinical discharge — set on the parent admission when doctor confirms clinical clearance + Boolean clinicallyDischarged = false; + @Temporal(javax.persistence.TemporalType.TIMESTAMP) + private Date clinicalDischargeDateTime; + @ManyToOne + private WebUser clinicalDischargedBy; + + // Room discharge mirror — set on the parent admission when the last room is vacated + @Temporal(javax.persistence.TemporalType.TIMESTAMP) + private Date roomDischargeDateTime; + @ManyToOne + private WebUser roomDischargedBy; double creditLimit; double creditUsedAmount; private double creditPaidAmount; @@ -307,6 +320,16 @@ public void setClaimable(boolean claimable) { String comments; @Lob private String planOfAction; + + // Clinical discharge content fields — used on the child ClinicalDischarge record + @ManyToOne + private ClinicalEntity dischargeCondition; + @Lob + private String followUpPlan; + @Lob + private String activityInstructions; + @Lob + private String dietInstructions; @Transient List diagnosis; @ManyToOne @@ -846,6 +869,82 @@ public void setPrintingDischargeTime(Date printingDischargeTime) { this.printingDischargeTime = printingDischargeTime; } + public Boolean getClinicallyDischarged() { + return clinicallyDischarged; + } + + public boolean isClinicallyDischarged() { + return Boolean.TRUE.equals(clinicallyDischarged); + } + + public void setClinicallyDischarged(Boolean clinicallyDischarged) { + this.clinicallyDischarged = clinicallyDischarged; + } + + public Date getClinicalDischargeDateTime() { + return clinicalDischargeDateTime; + } + + public void setClinicalDischargeDateTime(Date clinicalDischargeDateTime) { + this.clinicalDischargeDateTime = clinicalDischargeDateTime; + } + + public WebUser getClinicalDischargedBy() { + return clinicalDischargedBy; + } + + public void setClinicalDischargedBy(WebUser clinicalDischargedBy) { + this.clinicalDischargedBy = clinicalDischargedBy; + } + + public Date getRoomDischargeDateTime() { + return roomDischargeDateTime; + } + + public void setRoomDischargeDateTime(Date roomDischargeDateTime) { + this.roomDischargeDateTime = roomDischargeDateTime; + } + + public WebUser getRoomDischargedBy() { + return roomDischargedBy; + } + + public void setRoomDischargedBy(WebUser roomDischargedBy) { + this.roomDischargedBy = roomDischargedBy; + } + + public ClinicalEntity getDischargeCondition() { + return dischargeCondition; + } + + public void setDischargeCondition(ClinicalEntity dischargeCondition) { + this.dischargeCondition = dischargeCondition; + } + + public String getFollowUpPlan() { + return followUpPlan; + } + + public void setFollowUpPlan(String followUpPlan) { + this.followUpPlan = followUpPlan; + } + + public String getActivityInstructions() { + return activityInstructions; + } + + public void setActivityInstructions(String activityInstructions) { + this.activityInstructions = activityInstructions; + } + + public String getDietInstructions() { + return dietInstructions; + } + + public void setDietInstructions(String dietInstructions) { + this.dietInstructions = dietInstructions; + } + public Bill getFinalBill() { return finalBill; } diff --git a/src/main/webapp/emr/admin/discharge_conditions.xhtml b/src/main/webapp/emr/admin/discharge_conditions.xhtml new file mode 100644 index 00000000000..707067e5318 --- /dev/null +++ b/src/main/webapp/emr/admin/discharge_conditions.xhtml @@ -0,0 +1,99 @@ + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
diff --git a/src/main/webapp/inward/admission_profile.xhtml b/src/main/webapp/inward/admission_profile.xhtml index bab279c2810..b9030bb8885 100644 --- a/src/main/webapp/inward/admission_profile.xhtml +++ b/src/main/webapp/inward/admission_profile.xhtml @@ -422,6 +422,15 @@ icon="fa fa-prescription-bottle-alt" class="w-100"> + + - + + + + + + + + + + + +
+
+ + + + + +
+
+ + + +
+
+ + + +
+
+
+ + + + + + +
+
+ +
+
+
+ +
+
+ + + + + + + +
+
+
+ + +
+
+ + + + +
+
+
+
+ + + + + + + +
+ +
+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + +
+
+ + + + + + + +
+
+
+ + +
+
+
+
+ + + + + + + +
+
+
+ +
+
+
+
+ + + + + + + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+ + + + + + + +
+ +
+
+
+ +
+
+
+
+ diff --git a/src/main/webapp/inward/inward_report_inward_charge_type_breakdown.xhtml b/src/main/webapp/inward/inward_report_inward_charge_type_breakdown.xhtml new file mode 100644 index 00000000000..b54a01d62e3 --- /dev/null +++ b/src/main/webapp/inward/inward_report_inward_charge_type_breakdown.xhtml @@ -0,0 +1,317 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BHT NoPatient NameAdmittedDischarged + + Total
+ + + + + + + + + + + + + + + + + + + +
Totals + + + + + + + +
+
+ +
+ +
+
+
+
+ diff --git a/src/main/webapp/inward/inward_report_inward_charge_type_detail.xhtml b/src/main/webapp/inward/inward_report_inward_charge_type_detail.xhtml index 7420fb5ae26..fe7c2b55e77 100644 --- a/src/main/webapp/inward/inward_report_inward_charge_type_detail.xhtml +++ b/src/main/webapp/inward/inward_report_inward_charge_type_detail.xhtml @@ -12,7 +12,7 @@ - + diff --git a/src/main/webapp/inward/inward_reports.xhtml b/src/main/webapp/inward/inward_reports.xhtml index 19b2fc8be1d..4a2fdbb80b9 100644 --- a/src/main/webapp/inward/inward_reports.xhtml +++ b/src/main/webapp/inward/inward_reports.xhtml @@ -51,11 +51,16 @@
- + diff --git a/src/main/webapp/reports/inventoryReports/consumption.xhtml b/src/main/webapp/reports/inventoryReports/consumption.xhtml index b90e5a8d610..6ded55dd856 100644 --- a/src/main/webapp/reports/inventoryReports/consumption.xhtml +++ b/src/main/webapp/reports/inventoryReports/consumption.xhtml @@ -333,9 +333,13 @@ + rowIndexVar="n" rowKey="#{entry.key}" + paginator="true" + paginatorPosition="bottom" + rows="10" + paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}" + currentPageReportTemplate="Showing {startRecord} to {endRecord} of {totalRecords}" + rowsPerPageTemplate="5,10,15,25,50,100,500,1000"> @@ -829,9 +833,12 @@ var="entry" rowIndexVar="n" rowKey="#{entry.key}" + paginator="true" + paginatorPosition="bottom" rows="10" paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}" - currentPageReportTemplate="Showing {startRecord} to {endRecord} of {totalRecords}"> + currentPageReportTemplate="Showing {startRecord} to {endRecord} of {totalRecords}" + rowsPerPageTemplate="5,10,15,25,50,100,500,1000"> - -
@@ -176,29 +174,33 @@
- -
- - - - - - - -
- - - -
- - - - - +
+
+ + +
+
+
+ + + + +
+
+
+
+ + +
+
+
+ + + + +
@@ -219,15 +221,18 @@

-
-
- - - -    - +
+
+
+
+
+ + + + +
diff --git a/src/main/webapp/resources/pharmacy/po_custom_2.xhtml b/src/main/webapp/resources/pharmacy/po_custom_2.xhtml index 2e467686e48..20e6d65c3ad 100644 --- a/src/main/webapp/resources/pharmacy/po_custom_2.xhtml +++ b/src/main/webapp/resources/pharmacy/po_custom_2.xhtml @@ -109,8 +109,6 @@
- -
@@ -177,28 +175,32 @@ -
- - - - - - - -
- -
- - +
+
+ + +
+
+
+ + + + +
-
- - - - - - +
+
+ + +
+
+
+ + + + +
@@ -218,11 +220,16 @@

-
-
- +
+
+ + +
+
+
+ - +
diff --git a/src/main/webapp/resources/pharmacy/po_custom_4_letter.xhtml b/src/main/webapp/resources/pharmacy/po_custom_4_letter.xhtml index 0c7f3ff7591..01c52379d32 100644 --- a/src/main/webapp/resources/pharmacy/po_custom_4_letter.xhtml +++ b/src/main/webapp/resources/pharmacy/po_custom_4_letter.xhtml @@ -164,12 +164,12 @@
- +
- +