From d30a7a8b448c8624841344abb806b7f35d2f0ac9 Mon Sep 17 00:00:00 2001 From: Dr M H B Ariyaratne Date: Fri, 12 Jun 2026 21:33:04 +0530 Subject: [PATCH 1/8] feat(pharmacy): auto-load prescribed medicines into retail sale bill from clinical queue When an OPD prescription is written in Prescriptions Given on the EMR visit page (emr/opd_visit.xhtml) and the user clicks Pharmacy Bill on the clinical queue (clinical/clinical_queue.xhtml), the prescribed medicines now load automatically onto the retail sale bill - matching the inward flow where admission prescriptions are converted to pharmacy dispensing notes. Root cause: PracticeBookingController.issuePharmacyBill() loaded the encounter medicines and iterated them with an empty loop body - the conversion to bill items was never implemented. Only the patient, referring doctor and prescription text (as a comment) reached the retail sale page. Changes: - PharmacySaleController.addBillItemsFromEncounterMedicines(): converts each VisitMedicine prescription to a bill item. Resolves dispensable item and quantity via PrescriptionToItemService (qty defaults to 1 when the prescription is incomplete so the pharmacist adjusts it on the page), auto-picks an earliest-expiry (FEFO) in-date stock batch from the logged-in user's department, resolving VMP/VTM prescriptions to candidate AMPs via PharmacyBean.resolveAmps(). - Items are added through the existing addBillItemSingleItem() so all retail-sale safeguards apply unchanged: expiry check, stock quantity check, duplicate batch check, UserStock locking and allergy check. - Dose, frequency, duration and comment are carried onto the bill item's prescription so bill descriptions and drug label printing work as in the inward discharge issue flow. - Medicines that cannot be auto-added (no stock, conversion failure, duplicate batch) are reported in a visible warning, never silently dropped. - PracticeBookingController.issuePharmacyBill(): replaced the empty placeholder loop with a call to the new conversion method. Mirrors PharmacySaleBhtController.prepareDischargeIssueFromPrescriptions (issue #21334) for the OPD retail sale path. Co-Authored-By: Claude Fable 5 --- .../clinical/PracticeBookingController.java | 8 +- .../bean/pharmacy/PharmacySaleController.java | 131 ++++++++++++++++++ 2 files changed, 136 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/divudi/bean/clinical/PracticeBookingController.java b/src/main/java/com/divudi/bean/clinical/PracticeBookingController.java index 71626098afb..988e0556061 100644 --- a/src/main/java/com/divudi/bean/clinical/PracticeBookingController.java +++ b/src/main/java/com/divudi/bean/clinical/PracticeBookingController.java @@ -380,9 +380,11 @@ public String issuePharmacyBill() { logger.log(Level.FINE, "No prescription text to set"); } - getPatientEncounterController().fillEncounterMedicines(opdVisit); - for(ClinicalFindingValue cli :patientEncounterController.getEncounterMedicines()){ - } + // Auto-load the prescribed medicines onto the retail sale bill, mirroring + // the inward prescription-to-dispensing conversion on the admission profile. + List encounterMedicines + = getPatientEncounterController().fillEncounterMedicines(opdVisit); + getPharmacySaleController().addBillItemsFromEncounterMedicines(encounterMedicines); return "/pharmacy/pharmacy_bill_retail_sale?faces-redirect=true"; } diff --git a/src/main/java/com/divudi/bean/pharmacy/PharmacySaleController.java b/src/main/java/com/divudi/bean/pharmacy/PharmacySaleController.java index a717694424f..8f9128f816a 100644 --- a/src/main/java/com/divudi/bean/pharmacy/PharmacySaleController.java +++ b/src/main/java/com/divudi/bean/pharmacy/PharmacySaleController.java @@ -43,6 +43,7 @@ import com.divudi.ejb.CashTransactionBean; import com.divudi.ejb.PharmacyBean; import com.divudi.ejb.PharmacyService; +import com.divudi.ejb.PrescriptionToItemService; import com.divudi.core.util.CommonFunctions; import com.divudi.service.StaffService; import com.divudi.core.entity.Bill; @@ -196,6 +197,8 @@ public class PharmacySaleController implements Serializable, ControllerWithPatie @EJB private PharmacyService pharmacyService; @EJB + private PrescriptionToItemService prescriptionToItemService; + @EJB private BillService billService; @EJB private AuditService auditService; @@ -1834,6 +1837,134 @@ public double addBillItemSingleItem() { return addedQty; } + /** + * Pre-loads the retail sale bill with the medicines prescribed in an OPD + * encounter (Pharmacy Bill button on the clinical queue). Each visit + * medicine's prescription is resolved to a dispensable item and quantity via + * {@link PrescriptionToItemService}, and the earliest-expiry (FEFO) in-date + * stock batch in the logged-in department is auto-picked. Items are added + * through {@link #addBillItemSingleItem()} so the usual checks (expiry, + * available quantity, duplicate batch, user stock locking, allergies) all + * apply, and the user can still edit or remove lines before settling. + * + *

Mirrors {@link PharmacySaleBhtController#prepareDischargeIssueFromPrescriptions}. + * Medicines with no stock (or whose conversion fails) are skipped with a + * visible warning, never silently dropped.

+ * + * @param encounterMedicines visit medicines of the OPD encounter + */ + public void addBillItemsFromEncounterMedicines(List encounterMedicines) { + if (encounterMedicines == null || encounterMedicines.isEmpty()) { + return; + } + Department dispensingDepartment = getSessionController().getLoggedUser() != null + ? getSessionController().getLoggedUser().getDepartment() : null; + if (dispensingDepartment == null) { + JsfUtil.addErrorMessage("No logged-in department to dispense the prescribed medicines from. Please add them manually."); + return; + } + List skipped = new ArrayList<>(); + for (ClinicalFindingValue encounterMedicine : encounterMedicines) { + if (encounterMedicine == null + || encounterMedicine.getPrescription() == null + || encounterMedicine.getPrescription().getItem() == null) { + continue; + } + Prescription sourcePrescription = encounterMedicine.getPrescription(); + Item dispensableItem = sourcePrescription.getItem(); + Double calculatedQty = null; + try { + PrescriptionToItemService.PrescriptionToItemResult result + = prescriptionToItemService.calculateItemAndQuantity(sourcePrescription); + if (result != null && result.isSuccess()) { + if (result.getItem() != null) { + dispensableItem = result.getItem(); + } + if (result.getQuantity() != null) { + calculatedQty = result.getQuantity(); + } + } + } catch (Exception e) { + // Incomplete or unconvertible prescription: fall through to the + // qty=1 default so the pharmacist sets the real quantity on the page. + } + double requiredQty = (calculatedQty != null && calculatedQty > 0) ? Math.ceil(calculatedQty) : 1.0; + + Stock fefoStock = findFefoStockForPrescribedItem(dispensableItem, dispensingDepartment, requiredQty); + if (fefoStock == null || fefoStock.getStock() <= 0) { + skipped.add(dispensableItem.getName() + " - no stock"); + continue; + } + + BillItem newBillItem = new BillItem(); + newBillItem.setItem(fefoStock.getItemBatch().getItem()); + // Carry the prescription details onto the bill item so dose/frequency/ + // duration show on the bill and drug labels print correctly. + Prescription billItemPrescription = new Prescription(); + billItemPrescription.setItem(fefoStock.getItemBatch().getItem()); + billItemPrescription.setDose(sourcePrescription.getDose()); + billItemPrescription.setDoseUnit(sourcePrescription.getDoseUnit()); + billItemPrescription.setFrequencyUnit(sourcePrescription.getFrequencyUnit()); + billItemPrescription.setDuration(sourcePrescription.getDuration()); + billItemPrescription.setDurationUnit(sourcePrescription.getDurationUnit()); + billItemPrescription.setComment(sourcePrescription.getComment()); + billItemPrescription.setPatient(getPatient()); + newBillItem.setPrescription(billItemPrescription); + + setBillItem(newBillItem); + setStock(fefoStock); + setQty(Math.min(requiredQty, fefoStock.getStock())); + + double addedQty = addBillItemSingleItem(); + if (addedQty <= 0) { + String reason = (errorMessage != null && !errorMessage.trim().isEmpty()) + ? errorMessage : "could not be added"; + skipped.add(dispensableItem.getName() + " - " + reason); + clearBillItem(); + } + } + calculateBillItemsAndBillTotalsOfPreBill(); + if (!skipped.isEmpty()) { + JsfUtil.addErrorMessage("Not auto-added from the prescription: " + String.join("; ", skipped) + + ". Please add manually if needed."); + } + } + + /** + * FEFO stock lookup for a prescribed item: in-date, positive-stock batches in + * the dispensing department, earliest expiry first. Prefers the first batch + * that covers the required quantity; otherwise returns the earliest-expiry + * batch (the caller caps the quantity to its stock). For VMP/VTM prescriptions + * the candidate AMPs are resolved first, as in the BHT discharge issue flow. + */ + private Stock findFefoStockForPrescribedItem(Item dispensableItem, Department dispensingDepartment, double requiredQty) { + List amps = pharmacyBean.resolveAmps(dispensableItem); + if (amps == null || amps.isEmpty()) { + return null; + } + Map m = new HashMap<>(); + m.put("amps", amps); + m.put("d", dispensingDepartment); + m.put("z", 0.0); + m.put("doe", new Date()); + String jpql = "select s from Stock s " + + " where s.itemBatch.item in :amps " + + " and s.department=:d " + + " and s.stock > :z " + + " and s.itemBatch.dateOfExpire > :doe " + + " order by s.itemBatch.dateOfExpire"; + List availableStocks = getStockFacade().findByJpql(jpql, m, TemporalType.TIMESTAMP); + if (availableStocks == null || availableStocks.isEmpty()) { + return null; + } + for (Stock s : availableStocks) { + if (s.getStock() >= requiredQty) { + return s; + } + } + return availableStocks.get(0); + } + public void addBillItemMultipleBatches() { editingQty = null; errorMessage = null; From 5bab3077f85038f3c63235a73eef90295a5643db Mon Sep 17 00:00:00 2001 From: Dr M H B Ariyaratne Date: Fri, 12 Jun 2026 21:33:55 +0530 Subject: [PATCH 2/8] refactor(clinical): modernise clinical queue page layout and accessibility Rework clinical/clinical_queue.xhtml to follow the project UI and accessibility-first guidelines: - Replace the h:panelGrid filter layout with a responsive Bootstrap row/col grid; labels bound to inputs via p:outputLabel for=... - Add stable ids to every actionable control (btnProcess, btnVisit, btnMarkPresent, btnMarkAbsent, btnEditVisit, btnViewVisit, btnOpdBill, btnPharmacyBill) and descriptive title attributes that include the patient name and queue number, so Playwright/automation can target individual rows. - Cap both speciality and doctor autocompletes with maxResults="20" and add placeholders. - Date/doctor filter changes now re-render the whole queue form (render=":queueForm") instead of a single resolved component id. - DataTables: add widgetVar and rowKey, pagination (25 rows, bottom, hidden when not needed), emptyMessage texts, and a row highlight for absent patients in the To Complete tab. - To Complete tab gains Age, Phone and a Waiting/Absent status badge column; both tab titles show live session counts. - Consistent icon set and button styling (success/info/danger/warning variants) across row actions; growl shows message details. JSF-only change - no Java modifications. Co-Authored-By: Claude Fable 5 --- src/main/webapp/clinical/clinical_queue.xhtml | 332 ++++++++++++------ 1 file changed, 219 insertions(+), 113 deletions(-) diff --git a/src/main/webapp/clinical/clinical_queue.xhtml b/src/main/webapp/clinical/clinical_queue.xhtml index 89ddb45c203..1924c68738a 100644 --- a/src/main/webapp/clinical/clinical_queue.xhtml +++ b/src/main/webapp/clinical/clinical_queue.xhtml @@ -12,135 +12,241 @@ - - - - - - - - - - + + + + + + + + + +
+
+ + + render=":queueForm"/> - - - + +
+ + - + maxResults="20" + placeholder="Search speciality" + styleClass="w-100" inputStyleClass="w-100"> + - +
- - + + - + maxResults="20" + placeholder="Search doctor" + styleClass="w-100" inputStyleClass="w-100"> + - - - - - - - - - - - - - - - #{bs.serialNo} - #{bs.bill.patient.person.nameWithTitle} - - - - - - - - - - - - - - - - - - - - - - - #{bsc.serialNo} - #{bsc.bill.patient.person.nameWithTitle} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + Absent + + + Waiting + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
- - From 97e6e06bcd1f7226a7a912fbf3bfd841a541ddce Mon Sep 17 00:00:00 2001 From: Dr M H B Ariyaratne Date: Fri, 12 Jun 2026 21:42:38 +0530 Subject: [PATCH 3/8] Signed-off-by: Dr M H B Ariyaratne --- src/main/resources/META-INF/persistence.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml index 1d0c6dfe595..b464de3b66a 100644 --- a/src/main/resources/META-INF/persistence.xml +++ b/src/main/resources/META-INF/persistence.xml @@ -2,7 +2,7 @@ org.eclipse.persistence.jpa.PersistenceProvider - ${JDBC_DATASOURCE} + jdbc/coop false @@ -51,7 +51,7 @@ - ${JDBC_AUDIT_DATASOURCE} + jdbc/rhAuditDS false From 67864d215a04bef3714ebcb4044d526a9f7139f6 Mon Sep 17 00:00:00 2001 From: Dr M H B Ariyaratne Date: Fri, 12 Jun 2026 21:54:38 +0530 Subject: [PATCH 4/8] feat(clinical): add diagnosis search and FavouriteDiagnosis support to favourite medicines API - Add GET /clinical/favourite_medicines/entities/diagnoses to search ClinicalEntity records (Disease_or_Syndrome) for use as forItemName - Support type=FavouriteMedicine (default) / type=FavouriteDiagnosis on create, search, and by-id endpoints - Resolve forItemName to a diagnosis for FavouriteDiagnosis templates, with "did you mean" suggestions on mismatch - Register the new endpoint and capability in CapabilityStatementResource and the AI chat module listing Closes #21452 --- .../FavouriteMedicineCreateRequestDTO.java | 16 ++- .../divudi/service/AnthropicApiService.java | 13 +- .../clinical/FavouriteMedicineApiService.java | 133 ++++++++++++++++-- .../ws/clinical/FavouriteMedicineApi.java | 61 +++++++- .../common/CapabilityStatementResource.java | 4 +- 5 files changed, 206 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/divudi/core/data/dto/clinical/FavouriteMedicineCreateRequestDTO.java b/src/main/java/com/divudi/core/data/dto/clinical/FavouriteMedicineCreateRequestDTO.java index 21fe91fe5f6..e660c69d6a2 100644 --- a/src/main/java/com/divudi/core/data/dto/clinical/FavouriteMedicineCreateRequestDTO.java +++ b/src/main/java/com/divudi/core/data/dto/clinical/FavouriteMedicineCreateRequestDTO.java @@ -23,6 +23,9 @@ public class FavouriteMedicineCreateRequestDTO implements Serializable { private Double fromYears; // Minimum age in years private Double toYears; // Maximum age in years + // "FavouriteMedicine" (default) or "FavouriteDiagnosis" + private String type; + // Medicine details private String categoryName; // Medicine category (e.g., "suspension", "tablet") @@ -41,7 +44,10 @@ public class FavouriteMedicineCreateRequestDTO implements Serializable { private boolean indoor = false; // For indoor patients only private String sex; // "Male", "Female", or null for both private Double orderNo; // Display order (auto-generated if not provided) - private String forItemName; // The item this is a favourite for (optional) + // The item this is a favourite for. Optional for type=FavouriteMedicine (defaults to itemName itself). + // Required for type=FavouriteDiagnosis: the diagnosis name (resolved via + // GET /entities/diagnoses) that itemName is being suggested for. + private String forItemName; // Control flags private boolean createMissingEntities = false; // Auto-create missing entities @@ -85,6 +91,14 @@ public void setToYears(Double toYears) { this.toYears = toYears; } + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + public String getCategoryName() { return categoryName; } diff --git a/src/main/java/com/divudi/service/AnthropicApiService.java b/src/main/java/com/divudi/service/AnthropicApiService.java index 2caa36c9e2e..59518161167 100644 --- a/src/main/java/com/divudi/service/AnthropicApiService.java +++ b/src/main/java/com/divudi/service/AnthropicApiService.java @@ -3246,13 +3246,17 @@ public String buildSystemPrompt(String hmisApiBaseUrl, String userHmisApiKey, St }); appendModule(sb, "Clinical - Favourite Medicines", "/clinical/favourite_medicines", - "Manage clinician favourite medicine templates. " + "Manage clinician favourite medicine templates and favourite-diagnosis " + + "medicine suggestions (PrescriptionTemplate types FavouriteMedicine / FavouriteDiagnosis). " + + "POST/GET accept type=FavouriteMedicine (default) or type=FavouriteDiagnosis. " + + "For FavouriteDiagnosis, forItemName (resolved via /entities/diagnoses) is required " + + "and is set as the diagnosis (forItem); itemName/itemType is the suggested medicine. " + "/validate (bulk entity validation) is live. " + "/parse and /suggest are not yet implemented (return 501).", githubUrl(branch, "developer_docs/API_CLINICAL_FAVOURITE_MEDICINES.md"), new String[][]{ - {"GET", "/clinical/favourite_medicines", "List favourite medicine templates"}, - {"POST", "/clinical/favourite_medicines", "Create a new template"}, + {"GET", "/clinical/favourite_medicines", "List favourite medicine/diagnosis templates. Use type=FavouriteDiagnosis for diagnosis suggestions"}, + {"POST", "/clinical/favourite_medicines", "Create a new template. Set type=FavouriteDiagnosis + forItemName= for diagnosis suggestions"}, {"GET", "/clinical/favourite_medicines/{id}", "Get template by ID"}, {"PUT", "/clinical/favourite_medicines/{id}", "Update a template"}, {"DELETE", "/clinical/favourite_medicines/{id}", "Retire a template"}, @@ -3260,7 +3264,8 @@ public String buildSystemPrompt(String hmisApiBaseUrl, String userHmisApiKey, St {"POST", "/clinical/favourite_medicines/suggest", "Not implemented (501) — reserved for future auto-suggest"}, {"POST", "/clinical/favourite_medicines/validate", "Bulk-validate a set of medicine entities"}, {"GET", "/clinical/favourite_medicines/entities/vtms","List/search Virtual Therapeutic Moieties"}, - {"GET", "/clinical/favourite_medicines/entities/amps", "List/search Actual Medicinal Products"} + {"GET", "/clinical/favourite_medicines/entities/amps", "List/search Actual Medicinal Products"}, + {"GET", "/clinical/favourite_medicines/entities/diagnoses", "List/search diagnoses (ClinicalEntity, Disease_or_Syndrome) for use as forItemName"} }); // ── FHIR ────────────────────────────────────────────────────────────── diff --git a/src/main/java/com/divudi/service/clinical/FavouriteMedicineApiService.java b/src/main/java/com/divudi/service/clinical/FavouriteMedicineApiService.java index a453fc69615..15c3873d638 100644 --- a/src/main/java/com/divudi/service/clinical/FavouriteMedicineApiService.java +++ b/src/main/java/com/divudi/service/clinical/FavouriteMedicineApiService.java @@ -10,10 +10,12 @@ import com.divudi.bean.pharmacy.MeasurementUnitController; import com.divudi.bean.pharmacy.VmpController; import com.divudi.core.data.ItemType; +import com.divudi.core.data.SymanticType; import com.divudi.core.data.clinical.PrescriptionTemplateType; import com.divudi.core.entity.Category; import com.divudi.core.entity.Item; import com.divudi.core.entity.WebUser; +import com.divudi.core.entity.clinical.ClinicalEntity; import com.divudi.core.entity.clinical.PrescriptionTemplate; import com.divudi.core.entity.lab.Antibiotic; import com.divudi.core.entity.pharmacy.MeasurementUnit; @@ -24,6 +26,7 @@ import com.divudi.core.entity.pharmacy.Vmpp; import com.divudi.core.entity.pharmacy.Vtm; import com.divudi.core.facade.CategoryFacade; +import com.divudi.core.facade.ClinicalEntityFacade; import com.divudi.core.facade.ItemFacade; import com.divudi.core.facade.MeasurementUnitFacade; import com.divudi.core.facade.PrescriptionTemplateFacade; @@ -63,6 +66,9 @@ public class FavouriteMedicineApiService implements Serializable { @EJB private MeasurementUnitFacade measurementUnitFacade; + @EJB + private ClinicalEntityFacade clinicalEntityFacade; + // =================== CONTROLLER INJECTIONS =================== @Inject @@ -84,6 +90,9 @@ public class FavouriteMedicineApiService implements Serializable { */ public PrescriptionTemplate createFavouriteMedicine(WebUser user, Map requestData) { try { + // Determine whether this is a FavouriteMedicine or a FavouriteDiagnosis entry + PrescriptionTemplateType templateType = parseTemplateType((String) requestData.get("type")); + // Extract required fields String itemName = (String) requestData.get("itemName"); String itemType = (String) requestData.get("itemType"); @@ -136,10 +145,41 @@ public PrescriptionTemplate createFavouriteMedicine(WebUser user, Map similarDiagnoses = searchDiagnoses(forItemName.trim(), 5); + if (!similarDiagnoses.isEmpty()) { + errorMessage.append(". Did you mean: "); + for (int i = 0; i < similarDiagnoses.size(); i++) { + if (i > 0) { + errorMessage.append(", "); + } + errorMessage.append(similarDiagnoses.get(i).getName()); + } + errorMessage.append("?"); + } + + throw new IllegalArgumentException(errorMessage.toString()); + } + + template.setForItem(diagnosis); + } else { + template.setForItem(item); // Default to same item + } // Set age range (convert years to days) template.setFromDays(convertYearsToDays(fromYears)); @@ -152,7 +192,7 @@ public PrescriptionTemplate createFavouriteMedicine(WebUser user, Map searchFavouriteMedicines(WebUser user, Map parameters = new HashMap<>(); parameters.put("user", user); - parameters.put("type", PrescriptionTemplateType.FavouriteMedicine); + // Defaults to FavouriteMedicine for backward compatibility; pass type=FavouriteDiagnosis + // to search favourite-diagnosis entries instead + parameters.put("type", parseTemplateType((String) searchCriteria.get("type"))); // Add filters based on search criteria String query = (String) searchCriteria.get("query"); @@ -302,12 +344,14 @@ public PrescriptionTemplate getFavouriteMedicineById(WebUser user, Long id) { String jpql = "SELECT p FROM PrescriptionTemplate p " + "WHERE p.id = :id AND p.retired = false " + - "AND p.forWebUser = :user AND p.type = :type"; + "AND p.forWebUser = :user " + + "AND p.type IN (:favouriteMedicine, :favouriteDiagnosis)"; Map parameters = new HashMap<>(); parameters.put("id", id); parameters.put("user", user); - parameters.put("type", PrescriptionTemplateType.FavouriteMedicine); + parameters.put("favouriteMedicine", PrescriptionTemplateType.FavouriteMedicine); + parameters.put("favouriteDiagnosis", PrescriptionTemplateType.FavouriteDiagnosis); PrescriptionTemplate template = prescriptionTemplateFacade.findFirstByJpql(jpql, parameters); @@ -1050,26 +1094,87 @@ private void setOptionalFields(PrescriptionTemplate template, Map parameters = new HashMap<>(); parameters.put("user", user); - parameters.put("type", PrescriptionTemplateType.FavouriteMedicine); + parameters.put("type", type); Double maxOrder = prescriptionTemplateFacade.findDoubleByJpql(jpql, parameters); return (maxOrder != null ? maxOrder + 1.0 : 1.0); } + + /** + * Parse the "type" request field to a PrescriptionTemplateType. + * Defaults to FavouriteMedicine when not provided, for backward compatibility. + * Only FavouriteMedicine and FavouriteDiagnosis are accepted via the API. + */ + private PrescriptionTemplateType parseTemplateType(String type) { + if (type == null || type.trim().isEmpty()) { + return PrescriptionTemplateType.FavouriteMedicine; + } + + switch (type.trim().toLowerCase()) { + case "favouritemedicine": + return PrescriptionTemplateType.FavouriteMedicine; + case "favouritediagnosis": + return PrescriptionTemplateType.FavouriteDiagnosis; + default: + throw new IllegalArgumentException("Invalid type: " + type + ". Must be one of: FavouriteMedicine, FavouriteDiagnosis"); + } + } + + // =================== DIAGNOSIS (CLINICAL ENTITY) OPERATIONS =================== + + /** + * Search diagnoses (ClinicalEntity with SymanticType.Disease_or_Syndrome) by name + * Used to resolve forItemName when creating FavouriteDiagnosis entries + */ + public List searchDiagnoses(String query, Integer limit) { + if (query == null || query.trim().isEmpty()) { + return new ArrayList<>(); + } + + String jpql = "SELECT c FROM ClinicalEntity c WHERE c.retired = false " + + "AND c.symanticType = :symanticType " + + "AND UPPER(c.name) LIKE :query " + + "ORDER BY c.name"; + + Map parameters = new HashMap<>(); + parameters.put("symanticType", SymanticType.Disease_or_Syndrome); + parameters.put("query", "%" + query.trim().toUpperCase() + "%"); + + if (limit != null && limit > 0) { + return clinicalEntityFacade.findByJpql(jpql, parameters, limit); + } else { + return clinicalEntityFacade.findByJpql(jpql, parameters); + } + } + + /** + * Find a diagnosis (ClinicalEntity with SymanticType.Disease_or_Syndrome) by exact name + */ + public ClinicalEntity findClinicalEntityByName(String name) { + if (name == null || name.trim().isEmpty()) { + return null; + } + + String jpql = "SELECT c FROM ClinicalEntity c WHERE c.retired = false " + + "AND c.symanticType = :symanticType " + + "AND UPPER(c.name) = :name"; + + Map parameters = new HashMap<>(); + parameters.put("symanticType", SymanticType.Disease_or_Syndrome); + parameters.put("name", name.trim().toUpperCase()); + + return clinicalEntityFacade.findFirstByJpql(jpql, parameters); + } } \ No newline at end of file diff --git a/src/main/java/com/divudi/ws/clinical/FavouriteMedicineApi.java b/src/main/java/com/divudi/ws/clinical/FavouriteMedicineApi.java index 5384e666863..148fc9c8cd5 100644 --- a/src/main/java/com/divudi/ws/clinical/FavouriteMedicineApi.java +++ b/src/main/java/com/divudi/ws/clinical/FavouriteMedicineApi.java @@ -10,6 +10,7 @@ import com.divudi.core.entity.Category; import com.divudi.core.entity.Item; import com.divudi.core.entity.WebUser; +import com.divudi.core.entity.clinical.ClinicalEntity; import com.divudi.core.entity.clinical.PrescriptionTemplate; import com.divudi.core.entity.pharmacy.MeasurementUnit; import com.divudi.service.clinical.FavouriteMedicineApiService; @@ -503,6 +504,56 @@ public Response createAmp(String jsonRequest) { } } + /** + * Search diagnoses (ClinicalEntity with SymanticType.Disease_or_Syndrome) + * GET /api/clinical/favourite_medicines/entities/diagnoses?query=respiratory&limit=20 + * + * Use this to resolve the "forItemName" value when creating a + * FavouriteDiagnosis entry (POST /api/clinical/favourite_medicines with type=FavouriteDiagnosis) + */ + @GET + @Path("/entities/diagnoses") + @Produces(MediaType.APPLICATION_JSON) + public Response searchDiagnoses() { + try { + WebUser user = validateApiKey(); + if (user == null) { + return errorResponse("Not a valid key", 401); + } + + // Get query parameters + String query = uriInfo.getQueryParameters().getFirst("query"); + String limitStr = uriInfo.getQueryParameters().getFirst("limit"); + + if (query == null || query.trim().isEmpty()) { + return errorResponse("Query parameter is required", 400); + } + + Integer limit = null; + if (limitStr != null && !limitStr.trim().isEmpty()) { + try { + limit = Integer.parseInt(limitStr.trim()); + } catch (NumberFormatException e) { + return errorResponse("Invalid limit format", 400); + } + } + + // Search diagnoses + List diagnoses = favouriteMedicineService.searchDiagnoses(query.trim(), limit); + + // Convert to DTOs + List> responseData = new ArrayList<>(); + for (ClinicalEntity diagnosis : diagnoses) { + responseData.add(convertItemToMap(diagnosis)); + } + + return successResponse(responseData); + + } catch (Exception e) { + return errorResponse("An error occurred: " + e.getMessage(), 500); + } + } + /** * Search measurement units * GET /api/clinical/favourite_medicines/entities/units?query=ml&unitType=DoseUnit&limit=20 @@ -812,6 +863,7 @@ private Map convertTemplateToMap(PrescriptionTemplate template) } map.put("id", template.getId()); + map.put("type", template.getType() != null ? template.getType().toString() : null); // Item information if (template.getItem() != null) { @@ -863,9 +915,10 @@ private Map convertTemplateToMap(PrescriptionTemplate template) map.put("indoor", template.isIndoor()); map.put("sex", template.getSex() != null ? template.getSex().toString() : null); - // For item + // For item (the medicine itself for FavouriteMedicine; the diagnosis for FavouriteDiagnosis) if (template.getForItem() != null) { map.put("forItem", template.getForItem().getName()); + map.put("forItemId", template.getForItem().getId()); } // Audit information @@ -948,6 +1001,12 @@ private Map parseSearchParams() { searchCriteria.put("forItemName", forItemName.trim()); } + // Template type: FavouriteMedicine (default) or FavouriteDiagnosis + String type = uriInfo.getQueryParameters().getFirst("type"); + if (type != null && !type.trim().isEmpty()) { + searchCriteria.put("type", type.trim()); + } + // Pagination and ordering String limit = uriInfo.getQueryParameters().getFirst("limit"); if (limit != null && !limit.trim().isEmpty()) { diff --git a/src/main/java/com/divudi/ws/common/CapabilityStatementResource.java b/src/main/java/com/divudi/ws/common/CapabilityStatementResource.java index f99d913bc16..31e3c7a2932 100644 --- a/src/main/java/com/divudi/ws/common/CapabilityStatementResource.java +++ b/src/main/java/com/divudi/ws/common/CapabilityStatementResource.java @@ -80,7 +80,9 @@ private javax.json.JsonArray buildResources() { "API Key", "GET", "POST", "PUT", "DELETE")) .add(resource("Clinical Favourite Medicines", "/api/clinical/favourite_medicines", - "Clinical favourite medicine management", + "Clinical favourite medicine templates and favourite-diagnosis medicine suggestions " + + "(type=FavouriteMedicine default, or type=FavouriteDiagnosis). " + + "Includes /entities/diagnoses for searching diagnoses to use as forItemName.", "API Key", "GET", "POST", "PUT", "DELETE")) .add(resource("Membership", "/api/apiMembership", From 9f7b6ddaf390ffad9e3daddc88e3627ed4509241 Mon Sep 17 00:00:00 2001 From: Dr M H B Ariyaratne Date: Sat, 13 Jun 2026 06:24:11 +0530 Subject: [PATCH 5/8] feat(clinical): add weight range support, no-filter fallbacks for favourite diagnosis/medicine, and channel report cashier summaries - FavouriteMedicineApiService: add fromKg/toKg weight range fields to templates; make age range optional when weight range is provided; validate weight ranges - PatientEncounterController: add method 4 (no age/weight filter) fallback so favourites load even when patient DOB/weight aren't recorded; validate a medicine is selected before 'Add Favourite'; fall back to FavouriteDiagnosis template directly when no separate FavouriteMedicine config exists - PastPatientEncounterController: detect legacy prescriptions persisted with null or VisitDocument type via DocumentTemplateType.Prescription, default new prescriptions to VisitPrescription type - PracticeBookingController: separate prescriptionHtml (rich text) from plain comment via htmlToPlainText(); clear stale prescription when switching encounters - ChannelReportTempController: add fetchBillsTotal, fetchCashiers, fetchUserRows, fetchDateRangeRows with WebUser/agency support for cashier-based channel reporting - FavouriteMedicineApi: serialize fromKg/toKg in API responses - PharmacyBean: fallback VMP lookup via direct VMP.VTM_ID when VirtualProductIngredient join table is unpopulated - Privileges/UserPrivilageController: add PharmacyItemNameEdit privilege - pharmacy_bill_retail_sale.xhtml: add toggleable prescription preview panel rendering prescriptionHtml - lab_vmp/vtm, store_atm/vtm XHTML: gate Add/Edit/Save buttons behind PharmacyItemNameEdit privilege - opd_visit.xhtml: process acMedicine + dose/frequency/duration fields when adding a favourite, so the current selection is captured Co-Authored-By: Claude --- .../channel/ChannelReportTempController.java | 747 ++++++++++++++++++ .../PastPatientEncounterController.java | 35 +- .../clinical/PatientEncounterController.java | 106 ++- .../clinical/PracticeBookingController.java | 26 +- .../bean/common/UserPrivilageController.java | 1 + .../bean/pharmacy/PharmacySaleController.java | 10 + .../java/com/divudi/core/data/Privileges.java | 4 +- .../java/com/divudi/ejb/PharmacyBean.java | 22 +- .../clinical/FavouriteMedicineApiService.java | 59 +- .../ws/clinical/FavouriteMedicineApi.java | 4 + src/main/webapp/emr/opd_visit.xhtml | 2 +- src/main/webapp/pharmacy/admin/lab_vmp.xhtml | 6 +- src/main/webapp/pharmacy/admin/lab_vtm.xhtml | 6 +- .../webapp/pharmacy/admin/store_atm.xhtml | 6 +- .../webapp/pharmacy/admin/store_vtm.xhtml | 6 +- .../pharmacy/pharmacy_bill_retail_sale.xhtml | 21 + 16 files changed, 1021 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/divudi/bean/channel/ChannelReportTempController.java b/src/main/java/com/divudi/bean/channel/ChannelReportTempController.java index 31aea2272ec..b9fb4fab87a 100644 --- a/src/main/java/com/divudi/bean/channel/ChannelReportTempController.java +++ b/src/main/java/com/divudi/bean/channel/ChannelReportTempController.java @@ -24,6 +24,7 @@ import com.divudi.core.entity.ServiceSessionLeave; import com.divudi.core.entity.Speciality; import com.divudi.core.entity.Staff; +import com.divudi.core.entity.WebUser; import com.divudi.core.entity.channel.AgentReferenceBook; import com.divudi.core.facade.AgentHistoryFacade; import com.divudi.core.facade.AgentReferenceBookFacade; @@ -35,6 +36,7 @@ import com.divudi.core.facade.ServiceSessionLeaveFacade; import com.divudi.core.facade.SpecialityFacade; import com.divudi.core.facade.StaffFacade; +import com.divudi.core.facade.WebUserFacade; import com.divudi.core.util.JsfUtil; import com.divudi.core.util.CommonFunctions; import javax.inject.Named; @@ -81,6 +83,8 @@ public class ChannelReportTempController implements Serializable { StaffFacade staffFacade; @EJB SpecialityFacade specialityFacade; + @EJB + WebUserFacade webUserFacade; // @EJB ChannelBean channelBean; @@ -113,6 +117,9 @@ public class ChannelReportTempController implements Serializable { boolean sessoinDate; boolean paid; boolean scan; + boolean agency; + ChannelTotal channelTotal; + List channelSummeryDateRangeOrUserRows = new ArrayList<>(); /** @@ -226,6 +233,487 @@ public double fetchBillsTotal(BillType[] billTypes, BillType bt, Class[] bills, } + public double fetchBillsTotal(BillType[] billTypes, BillType bt, Class[] bills, Class[] nbills, Bill b, Date fd, Date td, Institution billedInstitution, Institution creditCompany, boolean withOutDocFee, boolean count, Staff staff, Speciality sp, WebUser webUser) { + + String sql; + Map m = new HashMap(); + if (count) { + sql = " select count(b) "; + } else if (withOutDocFee) { + sql = " select sum(b.netTotal-b.staffFee) "; + } else { + sql = " select sum(b.netTotal) "; + } + + sql += " from Bill b " + + " where b.retired=false "; + + if (b.getClass().equals(BilledBill.class)) { + sql += " and b.singleBillSession.sessionDate between :fromDate and :toDate "; + } + if (b.getClass().equals(CancelledBill.class)) { + sql += " and b.createdAt between :fromDate and :toDate "; + } + if (b.getClass().equals(RefundBill.class)) { + sql += " and b.createdAt between :fromDate and :toDate "; + } + + if (billTypes != null) { + sql += " and b.billType in :bt "; + List bts = Arrays.asList(billTypes); + m.put("bt", bts); + } + if (bt != null) { + sql += " and b.billType=:bt "; + m.put("bt", bt); + } + if (bills != null) { + sql += " and type(b) in :class "; + List cs = Arrays.asList(bills); + m.put("class", cs); + } + if (nbills != null) { + sql += " and type(b) not in :nclass "; + List ncs = Arrays.asList(nbills); + m.put("nclass", ncs); + } + if (b != null) { + sql += " and type(b)=:class "; + m.put("class", b.getClass()); + } + if (billedInstitution != null) { + sql += " and b.institution=:ins "; + m.put("ins", billedInstitution); + } + if (creditCompany != null) { + sql += " and b.creditCompany=:cc "; + m.put("cc", creditCompany); + } + if (staff != null) { + sql += " and b.staff=:s "; + m.put("s", staff); + } + if (webUser != null) { + sql += " and b.creater=:wu "; + m.put("wu", webUser); + } + if (sp != null) { + sql += " and b.staff.speciality=:sp "; + m.put("sp", sp); + } + if (getReportKeyWord().getBillType() != null) { + sql += " and b.singleBillSession.serviceSession.originatingSession.forBillType=:fbt "; + m.put("fbt", getReportKeyWord().getBillType()); + } + + m.put("fromDate", fd); + m.put("toDate", td); + if (count) { + return getBillFacade().findLongByJpql(sql, m, TemporalType.TIMESTAMP); + } else { + return getBillFacade().findDoubleByJpql(sql, m, TemporalType.TIMESTAMP); + } + + } + + public double fetchBillsTotalSessoin(BillType[] billTypes, BillType bt, Class[] bills, Class[] nbills, Bill b, Date fd, Date td, Institution billedInstitution, Institution creditCompany, boolean withOutDocFee, boolean count, Staff staff, Speciality sp, WebUser webUser) { + + String sql; + Map m = new HashMap(); + if (count) { + sql = " select count(b) "; + } else if (withOutDocFee) { + sql = " select sum(b.netTotal-b.staffFee) "; + } else { + sql = " select sum(b.netTotal) "; + } + + sql += " from Bill b " + + " where b.retired=false" + + " and b.createdAt between :fromDate and :toDate " + + " and b.singleBillSession.sessionDate between :fd and :td "; + + if (billTypes != null) { + sql += " and b.billType in :bt "; + List bts = Arrays.asList(billTypes); + m.put("bt", bts); + } + if (bt != null) { + sql += " and b.billType=:bt "; + m.put("bt", bt); + } + if (bills != null) { + sql += " and type(b) in :class "; + List cs = Arrays.asList(bills); + m.put("class", cs); + } + if (nbills != null) { + sql += " and type(b) not in :nclass "; + List ncs = Arrays.asList(nbills); + m.put("nclass", ncs); + } + if (b != null) { + sql += " and type(b)=:class "; + m.put("class", b.getClass()); + } + if (billedInstitution != null) { + sql += " and b.institution=:ins "; + m.put("ins", billedInstitution); + } + if (creditCompany != null) { + sql += " and b.creditCompany=:cc "; + m.put("cc", creditCompany); + } + if (staff != null) { + sql += " and b.staff=:s "; + m.put("s", staff); + } + if (webUser != null) { + sql += " and b.creater=:wu "; + m.put("wu", webUser); + } + if (sp != null) { + sql += " and b.staff.speciality=:sp "; + m.put("sp", sp); + } + if (getReportKeyWord().getBillType() != null) { + sql += " and b.singleBillSession.serviceSession.originatingSession.forBillType=:fbt "; + m.put("fbt", getReportKeyWord().getBillType()); + } + + m.put("fromDate", fd); + m.put("toDate", td); + m.put("fd", getFromDate()); + m.put("td", getToDate()); + if (count) { + return getBillFacade().findLongByJpql(sql, m, TemporalType.TIMESTAMP); + } else { + return getBillFacade().findDoubleByJpql(sql, m, TemporalType.TIMESTAMP); + } + + } + + public List fetchCashiers(BillType[] bts) { + List cashiers; + String sql; + Map m = new HashMap(); + List btys = Arrays.asList(bts); + sql = "select us from " + + " Bill b " + + " join b.creater us " + + " where b.retired=false " + + " and b.institution=:ins " + + " and b.billType in :btp " + + " and b.createdAt between :fromDate and :toDate " + + " group by us " + + " having sum(b.netTotal)!=0 "; + m.put("toDate", getToDate()); + m.put("fromDate", getFromDate()); + m.put("btp", btys); + m.put("ins", sessionController.getInstitution()); + cashiers = getWebUserFacade().findByJpql(sql, m, TemporalType.TIMESTAMP); + if (cashiers == null) { + cashiers = new ArrayList<>(); + } + + return cashiers; + } + + public List fetchCashiersSession(BillType[] bts) { + List cashiers; + String sql; + Map m = new HashMap(); + List btys = Arrays.asList(bts); + sql = "select us from " + + " Bill b " + + " join b.creater us " + + " where b.retired=false " + + " and b.institution=:ins " + + " and b.billType in :btp " + + " and b.singleBillSession.sessionDate between :fromDate and :toDate " + + " group by us " + + " having sum(b.netTotal)!=0 "; + m.put("toDate", getToDate()); + m.put("fromDate", getFromDate()); + m.put("btp", btys); + m.put("ins", sessionController.getInstitution()); + cashiers = getWebUserFacade().findByJpql(sql, m, TemporalType.TIMESTAMP); + if (cashiers == null) { + cashiers = new ArrayList<>(); + } + + return cashiers; + } + + public List fetchUserRows(Date fDate, Date tDate, BillType[] bts) { + List userRows = new ArrayList<>(); + double tbc = 0.0; + double tcc = 0.0; + double trc = 0.0; + double tht = 0.0; + double tst = 0.0; + for (WebUser webUser : fetchCashiers(bts)) { + ChannelSummeryUserRow row = new ChannelSummeryUserRow(); + row.setUser(webUser); + row.setBillCount(fetchBillsTotal(bts, null, null, null, new BilledBill(), fDate, tDate, null, null, false, true, null, null, webUser)); + row.setCanceledCount(fetchBillsTotal(bts, null, null, null, new CancelledBill(), fDate, tDate, null, null, false, true, null, null, webUser)); + row.setRefundCount(fetchBillsTotal(bts, null, null, null, new RefundBill(), fDate, tDate, null, null, false, true, null, null, webUser)); + double netTotal = fetchBillsTotal(bts, null, null, null, new BilledBill(), fDate, tDate, null, null, false, false, null, null, webUser) + + (fetchBillsTotal(bts, null, null, null, new CancelledBill(), fDate, tDate, null, null, false, false, null, null, webUser) + + fetchBillsTotal(bts, null, null, null, new RefundBill(), fDate, tDate, null, null, false, false, null, null, webUser)); + double hosTotal = fetchBillsTotal(bts, null, null, null, new BilledBill(), fDate, tDate, null, null, true, false, null, null, webUser) + + (fetchBillsTotal(bts, null, null, null, new CancelledBill(), fDate, tDate, null, null, true, false, null, null, webUser) + + fetchBillsTotal(bts, null, null, null, new RefundBill(), fDate, tDate, null, null, true, false, null, null, webUser)); + row.setTotalHosFee(hosTotal); + row.setTotalDocFee(netTotal - hosTotal); + row.setBold(false); + if (row.getBillCount() != 0.0 || row.getCanceledCount() != 0.0 || row.getRefundCount() != 0.0) { + userRows.add(row); + } + + tbc += row.getBillCount(); + tcc += row.getCanceledCount(); + trc += row.getRefundCount(); + tht += row.getTotalHosFee(); + tst += row.getTotalDocFee(); + } + + ChannelSummeryUserRow row = new ChannelSummeryUserRow(); + row.setBillCount(tbc); + row.setCanceledCount(tcc); + row.setRefundCount(trc); + row.setTotalHosFee(tht); + row.setTotalDocFee(tst); + row.setBold(true); + userRows.add(row); + + channelTotal.setTotalBillCount(channelTotal.getTotalBillCount() + tbc); + channelTotal.setTotalCanceledCount(channelTotal.getTotalCanceledCount() + tcc); + channelTotal.setTotalRefundCount(channelTotal.getTotalRefundCount() + trc); + channelTotal.setTotalDocFee(channelTotal.getTotalDocFee() + tst); + channelTotal.setTotalHosFee(channelTotal.getTotalHosFee() + tht); + + return userRows; + } + + public List fetchDateRangeRows(Date fDate, Date tDate, WebUser webUser, BillType[] bts) { + List dateRangeRows = new ArrayList<>(); + Date nowDate = fDate; + double tbc = 0.0; + double tcc = 0.0; + double trc = 0.0; + double tht = 0.0; + double tst = 0.0; + while (nowDate.before(tDate)) { + ChannelSummeryDateRangeRow row = new ChannelSummeryDateRangeRow(); + String formatedDate; + Date fd; + Date td; + fd = CommonFunctions.getStartOfDay(nowDate); + td = CommonFunctions.getEndOfDay(nowDate); + + DateFormat df = new SimpleDateFormat("yyyy MMMM dd"); + formatedDate = df.format(fd); + row.setDate(formatedDate); + row.setBillCount(fetchBillsTotal(bts, null, null, null, new BilledBill(), fd, td, null, null, false, true, null, null, webUser)); + row.setCanceledCount(fetchBillsTotal(bts, null, null, null, new CancelledBill(), fd, td, null, null, false, true, null, null, webUser)); + row.setRefundCount(fetchBillsTotal(bts, null, null, null, new RefundBill(), fd, td, null, null, false, true, null, null, webUser)); + double netTotal = fetchBillsTotal(bts, null, null, null, new BilledBill(), fd, td, null, null, false, false, null, null, webUser) + + (fetchBillsTotal(bts, null, null, null, new CancelledBill(), fd, td, null, null, false, false, null, null, webUser) + + fetchBillsTotal(bts, null, null, null, new RefundBill(), fd, td, null, null, false, false, null, null, webUser)); + double hosTotal = fetchBillsTotal(bts, null, null, null, new BilledBill(), fd, td, null, null, true, false, null, null, webUser) + + (fetchBillsTotal(bts, null, null, null, new CancelledBill(), fd, td, null, null, true, false, null, null, webUser) + + fetchBillsTotal(bts, null, null, null, new RefundBill(), fd, td, null, null, true, false, null, null, webUser)); + row.setTotalHosFee(hosTotal); + row.setTotalDocFee(netTotal - hosTotal); + row.setBold(false); + + if (row.getBillCount() != 0.0 || row.getCanceledCount() != 0.0 || row.getRefundCount() != 0.0) { + dateRangeRows.add(row); + } + + tbc += row.getBillCount(); + tcc += row.getCanceledCount(); + trc += row.getRefundCount(); + tht += row.getTotalHosFee(); + tst += row.getTotalDocFee(); + + Calendar cal = Calendar.getInstance(); + cal.setTime(nowDate); + cal.add(Calendar.DATE, 1); + nowDate = cal.getTime(); + } + ChannelSummeryDateRangeRow row = new ChannelSummeryDateRangeRow(); + row.setBillCount(tbc); + row.setCanceledCount(tcc); + row.setRefundCount(trc); + row.setTotalHosFee(tht); + row.setTotalDocFee(tst); + row.setBold(true); + dateRangeRows.add(row); + + channelTotal.setTotalBillCount(channelTotal.getTotalBillCount() + tbc); + channelTotal.setTotalCanceledCount(channelTotal.getTotalCanceledCount() + tcc); + channelTotal.setTotalRefundCount(channelTotal.getTotalRefundCount() + trc); + channelTotal.setTotalDocFee(channelTotal.getTotalDocFee() + tst); + channelTotal.setTotalHosFee(channelTotal.getTotalHosFee() + tht); + + return dateRangeRows; + } + + public List fetchDateRangeRowsSession(Date fDate, Date tDate, WebUser webUser, BillType[] bts) { + List dateRangeRows = new ArrayList<>(); + Date nowDate = fDate; + double tbc = 0.0; + double tcc = 0.0; + double trc = 0.0; + double tht = 0.0; + double tst = 0.0; + while (nowDate.before(tDate)) { + ChannelSummeryDateRangeRow row = new ChannelSummeryDateRangeRow(); + String formatedDate; + Date fd; + Date td; + fd = CommonFunctions.getStartOfDay(nowDate); + td = CommonFunctions.getEndOfDay(nowDate); + + DateFormat df = new SimpleDateFormat("yyyy MMMM dd"); + formatedDate = df.format(fd); + row.setDate(formatedDate); + row.setBillCount(fetchBillsTotalSessoin(bts, null, null, null, new BilledBill(), fd, td, null, null, false, true, null, null, webUser)); + row.setCanceledCount(fetchBillsTotalSessoin(bts, null, null, null, new CancelledBill(), fd, td, null, null, false, true, null, null, webUser)); + row.setRefundCount(fetchBillsTotalSessoin(bts, null, null, null, new RefundBill(), fd, td, null, null, false, true, null, null, webUser)); + double netTotal = fetchBillsTotalSessoin(bts, null, null, null, null, fd, td, null, null, false, false, null, null, webUser); + double hosTotal = fetchBillsTotalSessoin(bts, null, null, null, null, fd, td, null, null, true, false, null, null, webUser); + row.setTotalHosFee(hosTotal); + row.setTotalDocFee(netTotal - hosTotal); + row.setBold(false); + + if (row.getBillCount() != 0.0 || row.getCanceledCount() != 0.0 || row.getRefundCount() != 0.0) { + dateRangeRows.add(row); + } + + tbc += row.getBillCount(); + tcc += row.getCanceledCount(); + trc += row.getRefundCount(); + tht += row.getTotalHosFee(); + tst += row.getTotalDocFee(); + + Calendar cal = Calendar.getInstance(); + cal.setTime(nowDate); + cal.add(Calendar.DATE, 1); + nowDate = cal.getTime(); + } + ChannelSummeryDateRangeRow row = new ChannelSummeryDateRangeRow(); + row.setBillCount(tbc); + row.setCanceledCount(tcc); + row.setRefundCount(trc); + row.setTotalHosFee(tht); + row.setTotalDocFee(tst); + row.setBold(true); + dateRangeRows.add(row); + + channelTotal.setTotalBillCount(channelTotal.getTotalBillCount() + tbc); + channelTotal.setTotalCanceledCount(channelTotal.getTotalCanceledCount() + tcc); + channelTotal.setTotalRefundCount(channelTotal.getTotalRefundCount() + trc); + channelTotal.setTotalDocFee(channelTotal.getTotalDocFee() + tst); + channelTotal.setTotalHosFee(channelTotal.getTotalHosFee() + tht); + + return dateRangeRows; + } + + public void createChannelCountByUserOrDate() { + channelSummeryDateRangeOrUserRows = new ArrayList<>(); + channelTotal = new ChannelTotal(); + BillType[] bts; + if (agency) { + bts = new BillType[]{BillType.ChannelCash, BillType.ChannelPaid, BillType.ChannelAgent,}; + } else { + bts = new BillType[]{BillType.ChannelCash, BillType.ChannelPaid,}; + } + + if (byDate) { + Date nowDate = getFromDate(); + while (nowDate.before(getToDate())) { + ChannelSummeryDateRangeOrUserRow row = new ChannelSummeryDateRangeOrUserRow(); + String formatedDate; + Date fd; + Date td; + fd = CommonFunctions.getStartOfDay(nowDate); + td = CommonFunctions.getEndOfDay(nowDate); + + DateFormat df = new SimpleDateFormat("yyyy MMMM dd"); + formatedDate = df.format(fd); + row.setDate(formatedDate); + row.setUserRows(fetchUserRows(fd, td, bts)); + if (row.getUserRows().size() > 1) { + channelSummeryDateRangeOrUserRows.add(row); + } + + Calendar cal = Calendar.getInstance(); + cal.setTime(nowDate); + cal.add(Calendar.DATE, 1); + nowDate = cal.getTime(); + } + + } else { + for (WebUser webUser : fetchCashiers(bts)) { + ChannelSummeryDateRangeOrUserRow row = new ChannelSummeryDateRangeOrUserRow(); + row.setUser(webUser); + row.setDateRangeRows(fetchDateRangeRows(getFromDate(), getToDate(), webUser, bts)); + if (row.getDateRangeRows().size() > 1) { + channelSummeryDateRangeOrUserRows.add(row); + } + } + } + + } + + public void createChannelCountByUserOrDate2() { + long lng = CommonFunctions.getDayCount(getFromDate(), getToDate()); + + if (Math.abs(lng) > 2) { + JsfUtil.addErrorMessage("Date Range is too Long"); + return; + } + channelSummeryDateRangeOrUserRows = new ArrayList<>(); + channelTotal = new ChannelTotal(); + BillType[] bts; + bts = new BillType[]{BillType.ChannelCash, BillType.ChannelPaid,}; + + for (WebUser webUser : fetchCashiersSession(bts)) { + ChannelSummeryDateRangeOrUserRow row = new ChannelSummeryDateRangeOrUserRow(); + row.setUser(webUser); + String sql; + Map m = new HashMap(); + + sql = "select b from Bill b " + + " where b.retired=false " + + " and b.singleBillSession.sessionDate between :fromDate and :toDate" + + " and b.billType in :bt " + + " and b.creater=:wu"; + + m.put("bt", Arrays.asList(bts)); + m.put("wu", webUser); + + m.put("fromDate", getFromDate()); + m.put("toDate", getToDate()); + List bills = getBillFacade().findByJpql(sql, m, TemporalType.TIMESTAMP); + Date fd = getFromDate(); + for (Bill b : bills) { + if (b.getCreatedAt().getTime() < fd.getTime()) { + fd = b.getCreatedAt(); + } + } + + row.setDateRangeRows(fetchDateRangeRowsSession(fd, CommonFunctions.getEndOfDay(new Date()), webUser, bts)); + if (row.getDateRangeRows().size() > 1) { + channelSummeryDateRangeOrUserRows.add(row); + } + } + + } + public List fetchBillsAgencys() { Date fd = CommonFunctions.getStartOfMonth(fromDate); @@ -1166,4 +1654,263 @@ public void setAgencies(List agencies) { this.agencies = agencies; } + public boolean isAgency() { + return agency; + } + + public void setAgency(boolean agency) { + this.agency = agency; + } + + public ChannelTotal getChannelTotal() { + return channelTotal; + } + + public void setChannelTotal(ChannelTotal channelTotal) { + this.channelTotal = channelTotal; + } + + public List getChannelSummeryDateRangeOrUserRows() { + return channelSummeryDateRangeOrUserRows; + } + + public void setChannelSummeryDateRangeOrUserRows(List channelSummeryDateRangeOrUserRows) { + this.channelSummeryDateRangeOrUserRows = channelSummeryDateRangeOrUserRows; + } + + public WebUserFacade getWebUserFacade() { + return webUserFacade; + } + + public void setWebUserFacade(WebUserFacade webUserFacade) { + this.webUserFacade = webUserFacade; + } + + public class ChannelTotal { + + double totalBillCount; + double totalCanceledCount; + double totalRefundCount; + double totalHosFee; + double totalDocFee; + + public double getTotalBillCount() { + return totalBillCount; + } + + public void setTotalBillCount(double totalBillCount) { + this.totalBillCount = totalBillCount; + } + + public double getTotalCanceledCount() { + return totalCanceledCount; + } + + public void setTotalCanceledCount(double totalCanceledCount) { + this.totalCanceledCount = totalCanceledCount; + } + + public double getTotalRefundCount() { + return totalRefundCount; + } + + public void setTotalRefundCount(double totalRefundCount) { + this.totalRefundCount = totalRefundCount; + } + + public double getTotalHosFee() { + return totalHosFee; + } + + public void setTotalHosFee(double totalHosFee) { + this.totalHosFee = totalHosFee; + } + + public double getTotalDocFee() { + return totalDocFee; + } + + public void setTotalDocFee(double totalDocFee) { + this.totalDocFee = totalDocFee; + } + + } + + public class ChannelSummeryDateRangeOrUserRow { + + String date; + WebUser user; + List userRows; + List dateRangeRows; + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + public WebUser getUser() { + return user; + } + + public void setUser(WebUser user) { + this.user = user; + } + + public List getUserRows() { + return userRows; + } + + public void setUserRows(List userRows) { + this.userRows = userRows; + } + + public List getDateRangeRows() { + return dateRangeRows; + } + + public void setDateRangeRows(List dateRangeRows) { + this.dateRangeRows = dateRangeRows; + } + + } + + public class ChannelSummeryUserRow { + + WebUser user; + double billCount; + double canceledCount; + double refundCount; + boolean bold; + double totalHosFee; + double totalDocFee; + + public double getBillCount() { + return billCount; + } + + public void setBillCount(double billCount) { + this.billCount = billCount; + } + + public double getCanceledCount() { + return canceledCount; + } + + public void setCanceledCount(double canceledCount) { + this.canceledCount = canceledCount; + } + + public double getRefundCount() { + return refundCount; + } + + public void setRefundCount(double refundCount) { + this.refundCount = refundCount; + } + + public boolean isBold() { + return bold; + } + + public void setBold(boolean bold) { + this.bold = bold; + } + + public double getTotalHosFee() { + return totalHosFee; + } + + public void setTotalHosFee(double totalHosFee) { + this.totalHosFee = totalHosFee; + } + + public double getTotalDocFee() { + return totalDocFee; + } + + public void setTotalDocFee(double totalDocFee) { + this.totalDocFee = totalDocFee; + } + + public WebUser getUser() { + return user; + } + + public void setUser(WebUser user) { + this.user = user; + } + + } + + public class ChannelSummeryDateRangeRow { + + String date; + double billCount; + double canceledCount; + double refundCount; + boolean bold; + double totalHosFee; + double totalDocFee; + + public double getBillCount() { + return billCount; + } + + public void setBillCount(double billCount) { + this.billCount = billCount; + } + + public double getCanceledCount() { + return canceledCount; + } + + public void setCanceledCount(double canceledCount) { + this.canceledCount = canceledCount; + } + + public double getRefundCount() { + return refundCount; + } + + public void setRefundCount(double refundCount) { + this.refundCount = refundCount; + } + + public boolean isBold() { + return bold; + } + + public void setBold(boolean bold) { + this.bold = bold; + } + + public double getTotalHosFee() { + return totalHosFee; + } + + public void setTotalHosFee(double totalHosFee) { + this.totalHosFee = totalHosFee; + } + + public double getTotalDocFee() { + return totalDocFee; + } + + public void setTotalDocFee(double totalDocFee) { + this.totalDocFee = totalDocFee; + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + } + } diff --git a/src/main/java/com/divudi/bean/clinical/PastPatientEncounterController.java b/src/main/java/com/divudi/bean/clinical/PastPatientEncounterController.java index 5681b1b2fc8..47687d42be9 100644 --- a/src/main/java/com/divudi/bean/clinical/PastPatientEncounterController.java +++ b/src/main/java/com/divudi/bean/clinical/PastPatientEncounterController.java @@ -16,6 +16,7 @@ import com.divudi.core.data.BillType; import com.divudi.core.data.SymanticType; import com.divudi.core.data.clinical.ClinicalFindingValueType; +import com.divudi.core.data.clinical.DocumentTemplateType; import com.divudi.core.data.clinical.PrescriptionTemplateType; import com.divudi.core.data.inward.PatientEncounterType; import com.divudi.core.data.lab.InvestigationResultForGraph; @@ -1401,6 +1402,7 @@ public void generateDocumentsFromDocumentTemplates(PatientEncounter encounter) { for (DocumentTemplate t : dts) { if (t.isDefaultTemplate()) { ClinicalFindingValue cfv = new ClinicalFindingValue(); + cfv.setClinicalFindingValueType(ClinicalFindingValueType.VisitPrescription); cfv.setEncounter(encounter); cfv.setDocumentTemplate(t); cfv.setStringValue(t.getName()); @@ -1518,6 +1520,9 @@ private void updateOrGeneratePrescription() { } if (encounterPrescreption != null) { encounterPrescreption.setLobValue(generateDocumentFromTemplate(encounterPrescreption.getDocumentTemplate(), current)); + if (encounterPrescreption.getClinicalFindingValueType() == null) { + encounterPrescreption.setClinicalFindingValueType(ClinicalFindingValueType.VisitPrescription); + } if (encounterPrescreption.getId() == null) { clinicalFindingValueFacade.create(encounterPrescreption); } else { @@ -1533,7 +1538,7 @@ private void updateOrGeneratePrescription() { } if (prescTemplate != null) { encounterPrescreption = new ClinicalFindingValue(); - encounterPrescreption.setClinicalFindingValueType(ClinicalFindingValueType.VisitDocument); + encounterPrescreption.setClinicalFindingValueType(ClinicalFindingValueType.VisitPrescription); encounterPrescreption.setDocumentTemplate(prescTemplate); encounterPrescreption.setEncounter(current); encounterPrescreption.setLobValue(generateDocumentFromTemplate(prescTemplate, current)); @@ -2699,9 +2704,31 @@ private List fillEncounterDocuments(PatientEncounter encou } private List fillEncounterPrescreptions(PatientEncounter encounter) { - List clinicalFindingValueTypes = new ArrayList<>(); - clinicalFindingValueTypes.add(ClinicalFindingValueType.VisitPrescription); - return loadCurrentEncounterFindingValues(encounter, clinicalFindingValueTypes); + List vs = new ArrayList<>(); + if (encounterFindingValues == null) { + encounterFindingValues = fillEncounterFindingValues(encounter); + } + if (encounterFindingValues == null) { + encounterFindingValues = new ArrayList<>(); + } + for (ClinicalFindingValue v : encounterFindingValues) { + if (v == null) { + continue; + } + if (v.getClinicalFindingValueType() == ClinicalFindingValueType.VisitPrescription) { + vs.add(v); + continue; + } + // Legacy prescriptions were persisted with a null or VisitDocument type; + // recognise them through their document template type instead. + boolean legacyType = v.getClinicalFindingValueType() == null + || v.getClinicalFindingValueType() == ClinicalFindingValueType.VisitDocument; + if (legacyType && v.getDocumentTemplate() != null + && v.getDocumentTemplate().getType() == DocumentTemplateType.Prescription) { + vs.add(v); + } + } + return vs; } private List fillPlanOfAction(PatientEncounter encounter) { diff --git a/src/main/java/com/divudi/bean/clinical/PatientEncounterController.java b/src/main/java/com/divudi/bean/clinical/PatientEncounterController.java index 02693219fe7..49b9a70313d 100644 --- a/src/main/java/com/divudi/bean/clinical/PatientEncounterController.java +++ b/src/main/java/com/divudi/bean/clinical/PatientEncounterController.java @@ -1344,6 +1344,16 @@ public void addFavouriteMedicines(Patient patient) { encounterMedicines = new ArrayList<>(); } + // The medicine selected via the "Medicine" autocomplete (acMedicine). Favourite + // lookups are scoped to this item so "Add Favourite" applies to the selected medicine. + Item selectedMedicine = getEncounterMedicine().getPrescription() != null + ? getEncounterMedicine().getPrescription().getItem() : null; + + if (selectedMedicine == null) { + JsfUtil.addErrorMessage("Please select a medicine first"); + return; + } + List favouriteMedicines = new ArrayList<>(); String lookupMethod = ""; @@ -1359,7 +1369,7 @@ public void addFavouriteMedicines(Patient patient) { // Method 2: By Patient Weight Group (if weight is available) if (patientWeight != null && patientWeight > 0) { favouriteMedicines = favouriteController.listFavouriteItems( - null, + selectedMedicine, PrescriptionTemplateType.FavouriteMedicine, patientWeight, null @@ -1372,7 +1382,7 @@ public void addFavouriteMedicines(Patient patient) { // Method 3: By Patient Age Group (fallback when weight is not available or no weight-based favourites found) if (favouriteMedicines.isEmpty() && patientAgeInDays != null && patientAgeInDays > 0) { favouriteMedicines = favouriteController.listFavouriteItems( - null, + selectedMedicine, PrescriptionTemplateType.FavouriteMedicine, null, patientAgeInDays @@ -1382,13 +1392,27 @@ public void addFavouriteMedicines(Patient patient) { } } + // Method 4: No age/weight restriction — fallback when patient DOB and weight are not + // recorded so the doctor still gets the favourite configuration. + if (favouriteMedicines.isEmpty()) { + favouriteMedicines = favouriteController.listFavouriteItems( + selectedMedicine, + PrescriptionTemplateType.FavouriteMedicine, + null, + null + ); + if (!favouriteMedicines.isEmpty()) { + lookupMethod = "medicine templates (no age/weight filter)"; + } + } + // Check if any favourites were found if (favouriteMedicines == null || favouriteMedicines.isEmpty()) { - String message = "No favourite medicines found"; + String message = "No favourite configuration found for " + selectedMedicine.getName(); if (patientWeight != null && patientWeight > 0) { - message += " for weight " + patientWeight + " kg"; + message += " at weight " + patientWeight + " kg"; } else if (patientAgeInDays != null && patientAgeInDays > 0) { - message += " for age " + (patientAgeInDays / 365) + " years"; + message += " at age " + (patientAgeInDays / 365) + " years"; } JsfUtil.addWarningMessage(message); return; @@ -1527,7 +1551,7 @@ public void addFavouriteDiagnosis() { } // Method 2: By Patient Age Group (fallback when weight is not available or no weight-based favourites found) - if (diagnosisMedicineList == null || diagnosisMedicineList.isEmpty() && patientAgeInDays != null && patientAgeInDays > 0) { + if ((diagnosisMedicineList == null || diagnosisMedicineList.isEmpty()) && patientAgeInDays != null && patientAgeInDays > 0) { System.out.println("DEBUG: Step 1 - Finding medicine list by age group: " + patientAgeInDays + " days"); diagnosisMedicineList = favouriteController.listFavouriteItems( selectedDiagnosis, @@ -1536,11 +1560,28 @@ public void addFavouriteDiagnosis() { patientAgeInDays ); System.out.println("DEBUG: Age-based lookup found " + (diagnosisMedicineList != null ? diagnosisMedicineList.size() : "null") + " medicine recommendations"); - if (diagnosisMedicineList!=null && !diagnosisMedicineList.isEmpty()) { + if (diagnosisMedicineList != null && !diagnosisMedicineList.isEmpty()) { lookupMethod = "age group (" + (patientAgeInDays / 365) + " years)"; } } + // Method 3: No age/weight restriction — used when patient weight and DOB are not + // recorded (ageInDays == null or 0). Returns all FavouriteDiagnosis templates for + // this diagnosis regardless of age range so the doctor still gets suggestions. + if (diagnosisMedicineList == null || diagnosisMedicineList.isEmpty()) { + System.out.println("DEBUG: Step 1 - No age/weight data available; fetching all templates for diagnosis"); + diagnosisMedicineList = favouriteController.listFavouriteItems( + selectedDiagnosis, + PrescriptionTemplateType.FavouriteDiagnosis, + null, + null + ); + System.out.println("DEBUG: Unrestricted lookup found " + (diagnosisMedicineList != null ? diagnosisMedicineList.size() : "null") + " medicine recommendations"); + if (diagnosisMedicineList != null && !diagnosisMedicineList.isEmpty()) { + lookupMethod = "diagnosis templates (no age/weight filter)"; + } + } + // Step 2: For each recommended medicine, get its detailed configuration from FavouriteMedicine if (diagnosisMedicineList != null && !diagnosisMedicineList.isEmpty()) { System.out.println("DEBUG: Step 2 - Getting detailed configurations for " + diagnosisMedicineList.size() + " medicines"); @@ -1578,6 +1619,17 @@ public void addFavouriteDiagnosis() { System.out.println("DEBUG: Age-based medicine config found " + (medicineConfigs != null ? medicineConfigs.size() : "null") + " results"); } + // Try no-filter if weight and age are both unavailable/zero + if (medicineConfigs == null || medicineConfigs.isEmpty()) { + medicineConfigs = favouriteController.listFavouriteItems( + diagnosisTemplate.getItem(), + PrescriptionTemplateType.FavouriteMedicine, + null, + null + ); + System.out.println("DEBUG: No-filter medicine config found " + (medicineConfigs != null ? medicineConfigs.size() : "null") + " results"); + } + // Use the first valid configuration found if (medicineConfigs != null && !medicineConfigs.isEmpty()) { PrescriptionTemplate medicineTemplate = medicineConfigs.get(0); @@ -1586,7 +1638,11 @@ public void addFavouriteDiagnosis() { " with dose=" + medicineTemplate.getDose() + ", frequency=" + (medicineTemplate.getFrequencyUnit() != null ? medicineTemplate.getFrequencyUnit().getName() : "null")); } else { - System.out.println("DEBUG: No detailed configuration found for " + diagnosisTemplate.getItem().getName() + " - skipping"); + // No separate FavouriteMedicine configuration exists for this medicine - + // fall back to the dose/frequency/duration already stored on the + // FavouriteDiagnosis template itself. + System.out.println("DEBUG: No separate FavouriteMedicine configuration found for " + diagnosisTemplate.getItem().getName() + " - using FavouriteDiagnosis template directly"); + favouriteMedicines.add(diagnosisTemplate); } } } @@ -2165,6 +2221,7 @@ public void generateDocumentsFromDocumentTemplates(PatientEncounter encounter) { for (DocumentTemplate t : dts) { if (t.isDefaultTemplate()) { ClinicalFindingValue cfv = new ClinicalFindingValue(); + cfv.setClinicalFindingValueType(ClinicalFindingValueType.VisitPrescription); cfv.setEncounter(encounter); cfv.setDocumentTemplate(t); cfv.setStringValue(t.getName()); @@ -2323,6 +2380,9 @@ private void updateOrGeneratePrescription() { } if (encounterPrescreption != null) { encounterPrescreption.setLobValue(generateDocumentFromTemplate(encounterPrescreption.getDocumentTemplate(), current)); + if (encounterPrescreption.getClinicalFindingValueType() == null) { + encounterPrescreption.setClinicalFindingValueType(ClinicalFindingValueType.VisitPrescription); + } if (encounterPrescreption.getId() == null) { clinicalFindingValueFacade.create(encounterPrescreption); } else { @@ -2339,7 +2399,7 @@ private void updateOrGeneratePrescription() { } if (prescTemplate != null) { encounterPrescreption = new ClinicalFindingValue(); - encounterPrescreption.setClinicalFindingValueType(ClinicalFindingValueType.VisitDocument); + encounterPrescreption.setClinicalFindingValueType(ClinicalFindingValueType.VisitPrescription); encounterPrescreption.setDocumentTemplate(prescTemplate); encounterPrescreption.setEncounter(current); encounterPrescreption.setLobValue(generateDocumentFromTemplate(prescTemplate, current)); @@ -3842,9 +3902,31 @@ private List fillEncounterDocuments(PatientEncounter encou } private List fillEncounterPrescreptions(PatientEncounter encounter) { - List clinicalFindingValueTypes = new ArrayList<>(); - clinicalFindingValueTypes.add(ClinicalFindingValueType.VisitPrescription); - return loadCurrentEncounterFindingValues(encounter, clinicalFindingValueTypes); + List vs = new ArrayList<>(); + if (encounterFindingValues == null) { + encounterFindingValues = fillEncounterFindingValues(encounter); + } + if (encounterFindingValues == null) { + encounterFindingValues = new ArrayList<>(); + } + for (ClinicalFindingValue v : encounterFindingValues) { + if (v == null) { + continue; + } + if (v.getClinicalFindingValueType() == ClinicalFindingValueType.VisitPrescription) { + vs.add(v); + continue; + } + // Legacy prescriptions were persisted with a null or VisitDocument type; + // recognise them through their document template type instead. + boolean legacyType = v.getClinicalFindingValueType() == null + || v.getClinicalFindingValueType() == ClinicalFindingValueType.VisitDocument; + if (legacyType && v.getDocumentTemplate() != null + && v.getDocumentTemplate().getType() == DocumentTemplateType.Prescription) { + vs.add(v); + } + } + return vs; } private List fillPlanOfAction(PatientEncounter encounter) { diff --git a/src/main/java/com/divudi/bean/clinical/PracticeBookingController.java b/src/main/java/com/divudi/bean/clinical/PracticeBookingController.java index 988e0556061..2c5ed2b5c23 100644 --- a/src/main/java/com/divudi/bean/clinical/PracticeBookingController.java +++ b/src/main/java/com/divudi/bean/clinical/PracticeBookingController.java @@ -366,6 +366,8 @@ public String issuePharmacyBill() { getPatientEncounterController().setEncounterPrescreption(firstPrescription); logger.log(Level.FINE, "Set encounterPrescreption from list. Prescription ID: {0}", firstPrescription.getId()); } else { + // Clear any prescription left over from a previously processed encounter + getPatientEncounterController().setEncounterPrescreption(null); logger.log(Level.FINE, "No prescriptions found in list"); } @@ -374,9 +376,11 @@ public String issuePharmacyBill() { logger.log(Level.FINE, "Prescription text status: {0}", hasPrescriptionText ? "found" : "not found"); if (hasPrescriptionText) { - getPharmacySaleController().setComment(prescriptionText); + getPharmacySaleController().setPrescriptionHtml(prescriptionText); + getPharmacySaleController().setComment(htmlToPlainText(prescriptionText)); logger.log(Level.FINE, "Set prescription text as comment (length: {0} characters)", prescriptionText.length()); } else { + getPharmacySaleController().setPrescriptionHtml(null); logger.log(Level.FINE, "No prescription text to set"); } @@ -417,6 +421,26 @@ private String getPrescriptionText() { } } + /** + * Converts prescription HTML (Quill rich text) to readable plain text for + * the bill comment field, keeping line breaks for paragraphs and headings. + */ + private String htmlToPlainText(String html) { + if (html == null) { + return null; + } + return html + .replaceAll("(?i)", "\n") + .replaceAll("(?i)", "\n") + .replaceAll("<[^>]+>", "") + .replace(" ", " ") + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replaceAll("\n{3,}", "\n\n") + .trim(); + } + public void opdVisitFromServiceSession() { // ////// // System.out.println("opd visit from service session "); if (billSession == null) { diff --git a/src/main/java/com/divudi/bean/common/UserPrivilageController.java b/src/main/java/com/divudi/bean/common/UserPrivilageController.java index 5979588c95f..33ae688322b 100644 --- a/src/main/java/com/divudi/bean/common/UserPrivilageController.java +++ b/src/main/java/com/divudi/bean/common/UserPrivilageController.java @@ -362,6 +362,7 @@ private TreeNode createPrivilegeHolderTreeNodes() { TreeNode pharmacyNode = new DefaultTreeNode(new PrivilegeHolder(null, "Pharmacy"), allNode); new DefaultTreeNode(new PrivilegeHolder(Privileges.Pharmacy, "Pharmacy Menu"), pharmacyNode); new DefaultTreeNode(new PrivilegeHolder(Privileges.PharmacyAdministration, "Pharmacy Administration"), pharmacyNode); + new DefaultTreeNode(new PrivilegeHolder(Privileges.PharmacyItemNameEdit, "Pharmacy Item Name Edit"), pharmacyNode); new DefaultTreeNode(new PrivilegeHolder(Privileges.PharmacyDonation, "Pharmacy Donation"), pharmacyNode); // Channelling Privileges diff --git a/src/main/java/com/divudi/bean/pharmacy/PharmacySaleController.java b/src/main/java/com/divudi/bean/pharmacy/PharmacySaleController.java index 8f9128f816a..3ff4d6321ab 100644 --- a/src/main/java/com/divudi/bean/pharmacy/PharmacySaleController.java +++ b/src/main/java/com/divudi/bean/pharmacy/PharmacySaleController.java @@ -230,6 +230,7 @@ public class PharmacySaleController implements Serializable, ControllerWithPatie boolean billPreview = false; boolean fromOpdEncounter = false; String opdEncounterComments = ""; + String prescriptionHtml; int patientSearchTab = 0; Staff toStaff; @@ -4467,6 +4468,7 @@ private void clearBill() { userStockContainer = null; fromOpdEncounter = false; opdEncounterComments = null; + prescriptionHtml = null; patientSearchTab = 0; errorMessage = ""; comment = null; @@ -4783,6 +4785,14 @@ public void setOpdEncounterComments(String opdEncounterComments) { this.opdEncounterComments = opdEncounterComments; } + public String getPrescriptionHtml() { + return prescriptionHtml; + } + + public void setPrescriptionHtml(String prescriptionHtml) { + this.prescriptionHtml = prescriptionHtml; + } + public int getPatientSearchTab() { return patientSearchTab; } diff --git a/src/main/java/com/divudi/core/data/Privileges.java b/src/main/java/com/divudi/core/data/Privileges.java index c754edad916..bb381b3becf 100644 --- a/src/main/java/com/divudi/core/data/Privileges.java +++ b/src/main/java/com/divudi/core/data/Privileges.java @@ -331,6 +331,7 @@ public enum Privileges { StoreReports("Store Reports"), StoreSummery("Store Summary"), StoreAdministration("Store Administration"), + PharmacyItemNameEdit("Pharmacy Item Name Edit"), // // @@ -1018,7 +1019,8 @@ public String getCategory() { case PharmacyGrnFinalize: case PharmacyGrnApprove: case PrintOriginalPoBillFromReprint: - case PrintOriginalGrnBillFromReprint: + case PrintOriginalGrnBillFromReprint: + case PharmacyItemNameEdit: return "Pharmacy"; diff --git a/src/main/java/com/divudi/ejb/PharmacyBean.java b/src/main/java/com/divudi/ejb/PharmacyBean.java index ab3077ec5c6..ac8822a0e47 100644 --- a/src/main/java/com/divudi/ejb/PharmacyBean.java +++ b/src/main/java/com/divudi/ejb/PharmacyBean.java @@ -1342,9 +1342,11 @@ public List findAmpsForVtm(Vtm vtm) { Map m = new HashMap<>(); m.put("vtm", vtm); m.put("ret", false); - String jpql = "select vpi from VirtualProductIngredient vpi " + + // Primary path: VirtualProductIngredient join table + String vpiJpql = "select vpi from VirtualProductIngredient vpi " + " where vpi.retired=:ret and vpi.vtm=:vtm"; - List vpis = virtualProductIngredientFacade.findByJpql(jpql, m); + List vpis = virtualProductIngredientFacade.findByJpql(vpiJpql, m); List allAmps = new ArrayList<>(); if (vpis != null) { for (VirtualProductIngredient vpi : vpis) { @@ -1356,6 +1358,22 @@ public List findAmpsForVtm(Vtm vtm) { } } } + if (!allAmps.isEmpty()) { + return allAmps; + } + + // Fallback: VirtualProductIngredient table is unpopulated on many deployments. + // VMPs carry a direct vtm reference (VMP.VTM_ID) — use that instead. + String vmpJpql = "select vmp from Vmp vmp where vmp.retired=:ret and vmp.vtm=:vtm"; + List vmps = vmpFacade.findByJpql(vmpJpql, m); + if (vmps != null) { + for (Vmp vmp : vmps) { + List amps = findAmpsForVmp(vmp); + if (amps != null) { + allAmps.addAll(amps); + } + } + } return allAmps; } diff --git a/src/main/java/com/divudi/service/clinical/FavouriteMedicineApiService.java b/src/main/java/com/divudi/service/clinical/FavouriteMedicineApiService.java index 15c3873d638..87e15044547 100644 --- a/src/main/java/com/divudi/service/clinical/FavouriteMedicineApiService.java +++ b/src/main/java/com/divudi/service/clinical/FavouriteMedicineApiService.java @@ -98,13 +98,30 @@ public PrescriptionTemplate createFavouriteMedicine(WebUser user, Map= toYears) { - throw new IllegalArgumentException("Invalid age range: fromYears must be less than toYears and both must be >= 0"); + boolean hasAgeRange = fromYears != null && toYears != null; + boolean hasWeightRange = fromKg != null && toKg != null; + + if (!hasAgeRange && !hasWeightRange) { + throw new IllegalArgumentException("Required fields missing: provide either fromYears+toYears (age range) or fromKg+toKg (weight range)"); + } + + if (hasAgeRange) { + if (fromYears < 0 || toYears < 0 || fromYears >= toYears) { + throw new IllegalArgumentException("Invalid age range: fromYears must be less than toYears and both must be >= 0"); + } + } + + if (hasWeightRange) { + if (fromKg < 0 || toKg < 0 || fromKg >= toKg) { + throw new IllegalArgumentException("Invalid weight range: fromKg must be less than toKg and both must be >= 0"); + } } // Find or create the item based on type @@ -181,9 +198,17 @@ public PrescriptionTemplate createFavouriteMedicine(WebUser user, Map= 0"); + } + template.setFromKg(fromKg); + } + if (toKg != null) { + if (toKg < 0) { + throw new IllegalArgumentException("toKg must be >= 0"); + } + template.setToKg(toKg); + } + if (template.getFromKg() != null && template.getToKg() != null + && template.getFromKg() >= template.getToKg()) { + throw new IllegalArgumentException("fromKg must be less than toKg"); + } + // Update other optional fields setOptionalFields(template, updateData); diff --git a/src/main/java/com/divudi/ws/clinical/FavouriteMedicineApi.java b/src/main/java/com/divudi/ws/clinical/FavouriteMedicineApi.java index 148fc9c8cd5..db80a8a3484 100644 --- a/src/main/java/com/divudi/ws/clinical/FavouriteMedicineApi.java +++ b/src/main/java/com/divudi/ws/clinical/FavouriteMedicineApi.java @@ -877,6 +877,10 @@ private Map convertTemplateToMap(PrescriptionTemplate template) map.put("fromYears", favouriteMedicineService.convertDaysToYears(template.getFromDays())); map.put("toYears", favouriteMedicineService.convertDaysToYears(template.getToDays())); + // Weight range + map.put("fromKg", template.getFromKg()); + map.put("toKg", template.getToKg()); + // Category if (template.getCategory() != null) { map.put("categoryName", template.getCategory().getName()); diff --git a/src/main/webapp/emr/opd_visit.xhtml b/src/main/webapp/emr/opd_visit.xhtml index 577f0cef6ba..9cc440590df 100644 --- a/src/main/webapp/emr/opd_visit.xhtml +++ b/src/main/webapp/emr/opd_visit.xhtml @@ -546,7 +546,7 @@ @@ -114,7 +114,7 @@ update="detailPanel btnAdd btnEdit btnDelete btnSave btnCancel acVmp" process="@this" styleClass="ui-button-warning m-1" - disabled="#{labVmpController.editable or labVmpController.current.id eq null}" + disabled="#{labVmpController.editable or labVmpController.current.id eq null or !webUserController.hasPrivilege('PharmacyItemNameEdit')}" title="Edit selected Lab VMP record"> @@ -332,7 +332,7 @@ process="@form" styleClass="ui-button-success" icon="fas fa-save" - disabled="#{!labVmpController.editable}" + disabled="#{!labVmpController.editable or !webUserController.hasPrivilege('PharmacyItemNameEdit')}" title="Save Lab VMP record"/> + disabled="#{labVtmController.editable or !webUserController.hasPrivilege('PharmacyItemNameEdit')}"> @@ -84,7 +84,7 @@ class="m-1 ui-button-info" update="vtmDetails acVtm actionButtons statusFilterPanel" process="@this" - disabled="#{labVtmController.editable or labVtmController.selectedVtmDto eq null}"> + disabled="#{labVtmController.editable or labVtmController.selectedVtmDto eq null or !webUserController.hasPrivilege('PharmacyItemNameEdit')}"> + disabled="#{storeAtmController.editable or !webUserController.hasPrivilege('PharmacyItemNameEdit')}"> @@ -84,7 +84,7 @@ class="m-1 ui-button-info" update="atmDetails acAtm actionButtons statusFilterPanel" process="@this" - disabled="#{storeAtmController.editable or storeAtmController.selectedAtmDto eq null}"> + disabled="#{storeAtmController.editable or storeAtmController.selectedAtmDto eq null or !webUserController.hasPrivilege('PharmacyItemNameEdit')}"> + disabled="#{storeVtmController.editable or !webUserController.hasPrivilege('PharmacyItemNameEdit')}"> @@ -84,7 +84,7 @@ class="m-1 ui-button-info" update="vtmDetails acVtm actionButtons statusFilterPanel" process="@this" - disabled="#{storeVtmController.editable or storeVtmController.selectedVtmDto eq null}"> + disabled="#{storeVtmController.editable or storeVtmController.selectedVtmDto eq null or !webUserController.hasPrivilege('PharmacyItemNameEdit')}"> + + + + + + + + + +
From 7ccfb741e5ac97a1b8495ef1a9e4ed22672d9153 Mon Sep 17 00:00:00 2001 From: Dr M H B Ariyaratne Date: Sat, 13 Jun 2026 06:26:57 +0530 Subject: [PATCH 6/8] fix(build): restore CI/CD JNDI placeholders in persistence.xml PR validation failed because local JNDI names (jdbc/coop, jdbc/rhAuditDS) were committed in the branch history instead of the CI/CD placeholders (${JDBC_DATASOURCE}, ${JDBC_AUDIT_DATASOURCE}). Co-Authored-By: Claude --- src/main/resources/META-INF/persistence.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml index b464de3b66a..1d0c6dfe595 100644 --- a/src/main/resources/META-INF/persistence.xml +++ b/src/main/resources/META-INF/persistence.xml @@ -2,7 +2,7 @@ org.eclipse.persistence.jpa.PersistenceProvider - jdbc/coop + ${JDBC_DATASOURCE} false @@ -51,7 +51,7 @@ - jdbc/rhAuditDS + ${JDBC_AUDIT_DATASOURCE} false From 192c3b15d673ceed7d8805acc6669153c008ba82 Mon Sep 17 00:00:00 2001 From: Dr M H B Ariyaratne Date: Sat, 13 Jun 2026 06:55:08 +0530 Subject: [PATCH 7/8] fix(channel,clinical): address CodeRabbit review comments on PR #21453 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChannelReportTempController.java: guard b.getClass() calls at lines 251-259 behind a null check — consistent with the existing null guard at line 280 - clinical_queue.xhtml: add onclick=this.disabled=true on btnOpdBill and btnPharmacyBill to prevent accidental double-issue - clinical_queue.xhtml: bind pharmacySaleController.patient from bsc.bill.patient (selected row) rather than the stale practiceBookingController.opdVisit.patient Co-Authored-By: Claude --- .../channel/ChannelReportTempController.java | 18 ++++++++++-------- src/main/webapp/clinical/clinical_queue.xhtml | 4 +++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/divudi/bean/channel/ChannelReportTempController.java b/src/main/java/com/divudi/bean/channel/ChannelReportTempController.java index b9fb4fab87a..b3b3fbcb036 100644 --- a/src/main/java/com/divudi/bean/channel/ChannelReportTempController.java +++ b/src/main/java/com/divudi/bean/channel/ChannelReportTempController.java @@ -248,14 +248,16 @@ public double fetchBillsTotal(BillType[] billTypes, BillType bt, Class[] bills, sql += " from Bill b " + " where b.retired=false "; - if (b.getClass().equals(BilledBill.class)) { - sql += " and b.singleBillSession.sessionDate between :fromDate and :toDate "; - } - if (b.getClass().equals(CancelledBill.class)) { - sql += " and b.createdAt between :fromDate and :toDate "; - } - if (b.getClass().equals(RefundBill.class)) { - sql += " and b.createdAt between :fromDate and :toDate "; + if (b != null) { + if (b.getClass().equals(BilledBill.class)) { + sql += " and b.singleBillSession.sessionDate between :fromDate and :toDate "; + } + if (b.getClass().equals(CancelledBill.class)) { + sql += " and b.createdAt between :fromDate and :toDate "; + } + if (b.getClass().equals(RefundBill.class)) { + sql += " and b.createdAt between :fromDate and :toDate "; + } } if (billTypes != null) { diff --git a/src/main/webapp/clinical/clinical_queue.xhtml b/src/main/webapp/clinical/clinical_queue.xhtml index 1924c68738a..dd15c05cf70 100644 --- a/src/main/webapp/clinical/clinical_queue.xhtml +++ b/src/main/webapp/clinical/clinical_queue.xhtml @@ -215,6 +215,7 @@ styleClass="ui-button-info me-1" ajax="false" title="Create OPD bill for #{bsc.bill.patient.person.nameWithTitle} - No #{bsc.serialNo}" + onclick="this.disabled=true;" action="#{practiceBookingController.issueServices()}"> @@ -226,11 +227,12 @@ styleClass="ui-button-info" ajax="false" title="Create pharmacy bill for #{bsc.bill.patient.person.nameWithTitle} - No #{bsc.serialNo}" + onclick="this.disabled=true;" action="#{practiceBookingController.issuePharmacyBill()}" actionListener="#{pharmacySaleController.resetAll()}"> - From 9e4fa86cad05e0495f84f6cf1cf026185e8c8e6b Mon Sep 17 00:00:00 2001 From: Dr M H B Ariyaratne Date: Sat, 13 Jun 2026 07:23:07 +0530 Subject: [PATCH 8/8] fix(clinical-queue): replace onclick disable anti-pattern with confirm guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace `onclick="this.disabled=true;"` on btnOpdBill and btnPharmacyBill with `onclick="if (!confirm('…')) return false;"` per the codebase UI guidelines (developer_docs/ui/comprehensive-ui-guidelines.md §358–363). Disabling the button synchronously in onclick before the form POST fires excludes the button's name/value from the request, potentially breaking JSF action dispatch on ajax=false PrimeFaces command buttons. Co-Authored-By: Claude --- src/main/webapp/clinical/clinical_queue.xhtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/clinical/clinical_queue.xhtml b/src/main/webapp/clinical/clinical_queue.xhtml index dd15c05cf70..8240f07a4e2 100644 --- a/src/main/webapp/clinical/clinical_queue.xhtml +++ b/src/main/webapp/clinical/clinical_queue.xhtml @@ -215,7 +215,7 @@ styleClass="ui-button-info me-1" ajax="false" title="Create OPD bill for #{bsc.bill.patient.person.nameWithTitle} - No #{bsc.serialNo}" - onclick="this.disabled=true;" + onclick="if (!confirm('Create OPD bill for this patient?')) return false;" action="#{practiceBookingController.issueServices()}"> @@ -227,7 +227,7 @@ styleClass="ui-button-info" ajax="false" title="Create pharmacy bill for #{bsc.bill.patient.person.nameWithTitle} - No #{bsc.serialNo}" - onclick="this.disabled=true;" + onclick="if (!confirm('Create pharmacy bill for this patient?')) return false;" action="#{practiceBookingController.issuePharmacyBill()}" actionListener="#{pharmacySaleController.resetAll()}">